/* gallery.mzophotos.com client surface.
   One stylesheet for the cover, the gate, the grid and the lightbox. Dark is
   the default theme; light is opt in per album via data-theme on <html>.

   The palette is lifted from the studio's marketing site so the gallery and the
   site read as one brand. The blacks are deliberately WARM (#0d0c0a, not a
   neutral #0d0d0f) and the ink is a warm cream: side by side with the old cold
   grey, that difference is what separates "a photographer's gallery" from
   "generic dark mode software". --accent is still overridden per album from
   brand.json, so these are the fallbacks, not the authority. */

:root {
  --accent: #c69d68;
  --accent-bright: #dab77f;
  --accent-ghost: rgba(198, 157, 104, 0.12);
  --bg: #0d0c0a;
  --bg-raised: #14110e;
  --bg-card: #191510;
  --fg: #f1eadd;
  --muted: rgba(241, 234, 221, 0.62);
  --faint: rgba(241, 234, 221, 0.45);
  --line: rgba(241, 234, 221, 0.14);
  --line-soft: rgba(241, 234, 221, 0.08);
  --scrim: rgba(0, 0, 0, 0.45);
  --gap: 14px;
  --radius: 2px;
  --ease: cubic-bezier(0.25, 0.1, 0.25, 1);
  --speed: 260ms;
}
html[data-theme="light"] {
  --bg: #faf8f5;
  --bg-raised: #f3efe8;
  --bg-card: #ece6dc;
  --fg: #1b1a18;
  --muted: rgba(27, 26, 24, 0.66);
  --faint: rgba(27, 26, 24, 0.45);
  --line: rgba(27, 26, 24, 0.16);
  --line-soft: rgba(27, 26, 24, 0.08);
  --scrim: rgba(0, 0, 0, 0.28);
}

* { box-sizing: border-box; }
/* min-height, NOT height. `height:100%` caps the body at the viewport, which
   ends a sticky child's containing block at the fold: the pinned gallery bar
   scrolled away with the page instead of staying put. min-height keeps the
   full-viewport behaviour the gates and cover rely on while letting the body
   grow with a long grid. */
html, body { margin: 0; padding: 0; min-height: 100%; }
body {
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.6 var(--body-font, "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif);
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; }
img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ================================================================== cover
   FIVE LAYOUTS, chosen per album by the photographer. No single cover suits
   every shoot: a bright high-key frame is unreadable under white type, and a
   tight portrait loses its composition when bled to the viewport. The style
   token is whitelisted in cover.php before it reaches the class attribute.

   Every style is sized from viewport HEIGHT so the title and the enter button
   clear the fold. Sizing from width alone pushed the call to action off screen,
   which is the bug that made the first cover feel broken. */

.cover { position: relative; min-height: 100vh; min-height: 100svh; }
.cover-eyebrow {
  margin: 0; font-size: 10.5px; font-weight: 600; letter-spacing: .34em;
  text-transform: uppercase; color: var(--accent);
}
.cover-title {
  font-family: var(--heading-font, "Cormorant Garamond", Georgia, serif);
  font-weight: 500; line-height: 1.06; letter-spacing: .005em; margin: 14px 0 0;
}
.cover-rule { width: 56px; height: 1px; background: var(--accent); margin: 20px 0; border: 0; }
.cover-hair { width: 100%; height: 1px; background: var(--line-soft); margin: 18px 0; border: 0; }
.cover-date {
  margin: 0; letter-spacing: .24em; text-transform: uppercase;
  font-size: 11px; color: var(--faint);
}
.cover-copy-text {
  margin: 12px 0 0; font-size: clamp(15px, 1.6vw, 19px);
  line-height: 1.55; color: var(--muted); max-width: 38ch;
}
.cover-count {
  margin: 22px 0 0; font-size: 12px; letter-spacing: .28em;
  text-transform: uppercase; color: var(--faint);
}
.cover-enter {
  display: inline-block; margin-top: 24px; padding: 11px 24px;
  border: 1px solid var(--line); border-radius: var(--radius);
  text-decoration: none; letter-spacing: 2.75px; text-transform: uppercase;
  font-size: 12.5px; font-weight: 600;
  transition: background var(--speed) var(--ease), border-color var(--speed) var(--ease), color var(--speed) var(--ease);
}
.cover-enter:hover, .cover-enter:focus { background: var(--accent); border-color: var(--accent); color: #17130d; }

/* ---- wall (default) · a museum hang -------------------------------------
   Wide margins, a modestly sized print, the studio credit set vertically like
   a wall label. Never fights the photograph, which is why it is the default. */
.cover-style-wall { display: grid; place-content: center; padding: 8vh clamp(20px, 6vw, 80px); }
.cover-rail {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: clamp(18px, 3vw, 44px); align-items: center; max-width: 1180px; margin: 0 auto;
}
.cover-credit {
  writing-mode: vertical-rl; transform: rotate(180deg);
  font-size: 10.5px; letter-spacing: .3em; color: var(--faint); white-space: nowrap;
}
.cover-pair { display: grid; grid-template-columns: 1fr; gap: clamp(20px, 3vw, 44px); align-items: center; }
@media (min-width: 860px) { .cover-pair { grid-template-columns: minmax(0, 1fr) 250px; } }
.cover-hang { width: 100%; max-height: 62svh; object-fit: cover; aspect-ratio: 4 / 3; }
.cover-style-wall .cover-title { font-size: clamp(26px, 2.8vw, 36px); font-weight: 400; }
.cover-style-wall .cover-rule { width: 38px; background: var(--line); margin: 16px 0; }

/* ---- plate · editorial, mounted print with type beside it --------------- */
.cover-style-plate { display: grid; place-content: center; padding: 7vh clamp(20px, 6vw, 88px); }
.cover-grid {
  display: grid; grid-template-columns: 1fr; align-items: center;
  gap: clamp(24px, 4vw, 64px); max-width: 1500px; margin: 0 auto;
}
@media (min-width: 900px) { .cover-grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr); } }
.cover-plate {
  position: relative; padding: clamp(10px, 1.1vw, 16px);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg-raised); box-shadow: 0 40px 80px -34px rgba(0,0,0,.9);
}
.cover-plate::after {
  content: ""; position: absolute; inset: clamp(10px, 1.1vw, 16px);
  border: 1px solid rgba(0,0,0,.5); pointer-events: none;
}
.cover-plate img { width: 100%; max-height: 70svh; aspect-ratio: 4 / 3; object-fit: cover; }
.cover-style-plate .cover-title { font-size: clamp(38px, 5.2vw, 72px); }

/* ---- bleed · the photograph IS the page, type over it -------------------
   The one style that can fail: white lettering over a bright photo. The scrim
   is the fix and is adjustable per album, because the photographer cannot know
   from a settings screen whether their image needs it. */
.cover-style-bleed { display: block; }
.cover-shot {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 45%;
}
.cover-veil { position: absolute; inset: 0; }
.cover-style-bleed[data-scrim="light"] .cover-veil {
  background: linear-gradient(180deg, rgba(13,12,10,.34) 0%, rgba(13,12,10,.06) 42%, rgba(13,12,10,.54) 100%);
}
.cover-style-bleed[data-scrim="medium"] .cover-veil {
  background: linear-gradient(180deg, rgba(13,12,10,.58) 0%, rgba(13,12,10,.16) 40%, rgba(13,12,10,.80) 100%);
}
/* For a high key or backlit photograph: a near solid wash, plus a slight
   darkening of the image itself, because a gradient alone cannot rescue white
   type over a white sky. */
.cover-style-bleed[data-scrim="heavy"] .cover-veil {
  background: linear-gradient(180deg, rgba(13,12,10,.78) 0%, rgba(13,12,10,.46) 42%, rgba(13,12,10,.92) 100%);
}
.cover-style-bleed[data-scrim="heavy"] .cover-shot { filter: brightness(.72); }
.cover-bleed-inner {
  position: relative; min-height: 100vh; min-height: 100svh;
  display: grid; place-content: center; text-align: center;
  padding: 8vh 24px calc(8vh + env(safe-area-inset-bottom));
}
.cover-style-bleed .cover-title { font-size: clamp(42px, 7.4vw, 96px); font-weight: 300; }
.cover-style-bleed .cover-date { margin-top: 18px; letter-spacing: .34em; color: var(--ink-soft, rgba(241,234,221,.78)); }
.cover-style-bleed .cover-copy-text { margin-left: auto; margin-right: auto; }
.cover-style-bleed .cover-enter { border-color: rgba(241,234,221,.55); }

/* ---- diptych · two photographs, type in the gutter ---------------------- */
.cover-style-diptych { display: grid; place-content: center; padding: 7vh clamp(20px, 5vw, 64px); }
.cover-duo {
  display: grid; grid-template-columns: 1fr; gap: clamp(16px, 2.4vw, 30px);
  align-items: center; max-width: 1240px; margin: 0 auto;
}
@media (min-width: 900px) { .cover-duo { grid-template-columns: 1fr auto 1fr; } }
.cover-duo-a, .cover-duo-b { width: 100%; object-fit: cover; }
.cover-duo-a { aspect-ratio: 4 / 5; max-height: 64svh; }
.cover-duo-b { aspect-ratio: 4 / 3; max-height: 50svh; }
.cover-duo-mid { text-align: center; padding: 0 clamp(8px, 1.6vw, 20px); }
.cover-style-diptych .cover-title { font-size: clamp(26px, 3.2vw, 42px); font-weight: 400; }
.cover-style-diptych .cover-copy-text { margin-left: auto; margin-right: auto; }

/* ---- leaf · a print tipped into an album page --------------------------- */
.cover-style-leaf { display: grid; place-content: center; padding: 5vh clamp(18px, 5vw, 56px); }
.cover-leaf {
  max-width: 900px; margin: 0 auto; padding: clamp(18px, 2.6vw, 36px);
  background: linear-gradient(180deg, #1b1610 0%, #15110d 100%);
  border: 1px solid var(--line-soft); border-radius: 3px;
  box-shadow: 0 60px 120px -40px rgba(0,0,0,.95), 0 1px 0 rgba(241,234,221,.05) inset;
  text-align: center;
}
html[data-theme="light"] .cover-leaf { background: linear-gradient(180deg, #fffdf9 0%, #f4efe6 100%); }
.cover-tip { position: relative; border: 1px solid rgba(0,0,0,.6); box-shadow: 0 14px 34px -12px rgba(0,0,0,.85); }
.cover-tip img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; max-height: 42svh; }
.cover-style-leaf .cover-title { font-style: italic; font-weight: 400; font-size: clamp(26px, 3.2vw, 40px); margin-top: 20px; }
.cover-style-leaf .cover-date { margin-top: 12px; }
.cover-style-leaf .cover-copy-text { margin-left: auto; margin-right: auto; }

/* ---------------------------------------------------------------- gate */
.gate { min-height: 100vh; min-height: 100svh; display: flex; align-items: center; justify-content: center; padding: 24px; }
.gate-card { width: 100%; max-width: 360px; display: flex; flex-direction: column; text-align: center; }
.gate-title { margin: 0 0 8px; font-weight: 300; font-size: clamp(24px, 5vw, 34px); letter-spacing: 0.04em; }
.gate-copy { margin: 0 0 20px; color: var(--muted); font-size: 15px; }
.gate-error {
  margin: 0 0 16px;
  padding: 10px 12px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  font-size: 14px;
}
.gate-label { text-align: left; font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
.gate-input {
  width: 100%;
  padding: 12px 14px;
  margin-bottom: 16px;
  font: inherit;
  color: var(--fg);
  background: transparent;
  border: 1px solid var(--muted);
  border-radius: var(--radius);
}
.gate-input:focus { border-color: var(--accent); outline: none; }
.gate-submit {
  padding: 13px 20px;
  background: var(--accent);
  color: #101010;
  border-radius: var(--radius);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-size: 12px;
}
.gate-help { margin: 14px 0 0; font-size: 12px; color: var(--muted); }

/* ---------------------------------------------------------------- grid */
.grid-header { padding: 8vh 24px 4vh; text-align: center; }
.grid-title { margin: 0; font-weight: 300; letter-spacing: 0.06em; font-size: clamp(26px, 5vw, 46px); }
.grid-date { margin: 8px 0 0; letter-spacing: 0.22em; text-transform: uppercase; font-size: 11px; color: var(--muted); }
.grid-empty { padding: 12vh 24px; text-align: center; color: var(--muted); }
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--gap);
  padding: 0 var(--gap) calc(10vh + env(safe-area-inset-bottom));
  max-width: 1600px;
  margin: 0 auto;
}
@media (min-width: 700px) { .grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); --gap: 20px; } }
.tile { position: relative; margin: 0; align-self: start; aspect-ratio: var(--tile-ratio, 3 / 2); background: rgba(128, 128, 128, 0.12); overflow: hidden; }
.tile-open { display: block; width: 100%; height: 100%; padding: 0; }
.tile-img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.45s ease, opacity 0.3s ease; }
.tile:hover .tile-img { transform: scale(1.02); }
.fav { position: absolute; right: 6px; bottom: 6px; width: 38px; height: 38px; padding: 8px; border-radius: 50%; }
.fav svg { width: 100%; height: 100%; fill: none; stroke: #fff; stroke-width: 1.7; filter: drop-shadow(0 1px 3px rgba(0,0,0,0.6)); }
.fav[aria-pressed="true"] svg { fill: var(--accent); stroke: var(--accent); }

/* ---------------------------------------------------------------- lightbox
   position:fixed so it covers the grid without scrolling it, and [hidden] is
   forced to display:none because the flex display below would otherwise win. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
  touch-action: pan-y;
}
.lightbox[hidden] { display: none; }
.lightbox-img { max-width: 100vw; max-height: 100vh; width: auto; height: auto; object-fit: contain; }
.lightbox-close, .lightbox-prev, .lightbox-next {
  position: absolute;
  padding: 12px 16px;
  color: #fff;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.75;
}
.lightbox-close { top: calc(8px + env(safe-area-inset-top)); right: 8px; }
.lightbox-prev { left: 4px; top: 50%; transform: translateY(-50%); }
.lightbox-next { right: 4px; top: 50%; transform: translateY(-50%); }
.lightbox-fav { position: absolute; left: 50%; bottom: calc(16px + env(safe-area-inset-bottom)); transform: translateX(-50%); }
.lightbox-close:hover, .lightbox-prev:hover, .lightbox-next:hover { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* Task 8: favorites filter, the phase 3 print button, and the consent footer.
   A hidden tile must be hidden even though .tile sets display, so the filter
   rule is explicit: [hidden] alone loses to a display declaration. */
.grid-tools {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  align-items: center;
  justify-content: center;
  padding: 0 1rem 1.25rem;
}
.filter-btn,
.order-btn {
  font: inherit;
  color: inherit;
  background: transparent;
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: .4rem 1rem;
  cursor: pointer;
  opacity: .7;
}
.filter-btn[aria-pressed="true"] {
  opacity: 1;
  border-color: var(--accent, currentColor);
  color: var(--accent, inherit);
}
.order-btn[disabled] { cursor: default; opacity: .4; }
.tile[hidden] { display: none; }
.grid-footer {
  padding: 2rem 1rem 3rem;
  text-align: center;
  font-size: .95rem;
  opacity: .85;
}
.consent {
  display: inline-flex;
  gap: .6rem;
  align-items: flex-start;
  text-align: left;
  max-width: 44rem;
  cursor: pointer;
}
.consent input { margin-top: .25rem; flex: 0 0 auto; }
.consent-note { margin: .5rem 0 0; opacity: .6; font-size: .85rem; }

/* Downloads. Rendered only while the album's download window is open. */
.downloads{max-width:52rem;margin:2.5rem auto 0;padding:0 1rem;text-align:center}
.downloads-title{font-size:1.1rem;letter-spacing:.08em;text-transform:uppercase;margin:0 0 .5rem}
.downloads-note{margin:0 0 1rem;opacity:.75;line-height:1.6}
.zip-form{display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end;justify-content:center}
.zip-form label{display:flex;flex-direction:column;gap:.25rem;text-align:left;font-size:.85rem}
.zip-form select,.zip-form input{padding:.5rem .6rem;border:1px solid rgba(255,255,255,.25);border-radius:.25rem;background:transparent;color:inherit;font:inherit}
.zip-submit{padding:.6rem 1.1rem;border:0;border-radius:.25rem;background:var(--accent);color:#111;font:inherit;cursor:pointer}
.zip-submit[disabled]{opacity:.6;cursor:progress}
.zip-pin-note{flex-basis:100%;margin:0;font-size:.8rem;opacity:.7}
.zip-status{flex-basis:100%;margin:.25rem 0 0;min-height:1.2em;font-size:.9rem}
.dl-single{position:absolute;left:1rem;bottom:1rem;color:inherit;font-size:.9rem;text-decoration:underline}

/* The site root. One name, one line of copy, nothing that names an album. */
.home{min-height:100svh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.9rem;padding:2rem 1.25rem;text-align:center}
.home-title{font-family:var(--heading-font);font-size:clamp(1.9rem,6vw,3.2rem);letter-spacing:.12em;text-transform:uppercase;margin:0;color:var(--accent)}
.home-copy{margin:0;max-width:34rem;line-height:1.7;opacity:.9}
.home-contact{margin:0;max-width:34rem;font-size:.9rem;line-height:1.7;opacity:.65}


/* ================================================================= admin
   The photographer's half of the product.

   Same brand tokens as the client surface so the two never read as different
   software, but a denser, more utilitarian layout: this is a work tool, not a
   gallery. Several rules here are functional rather than decorative and are
   locked by tests in ViewTest.php:

     .admin-covergrid  bounds the photo manager. Unbounded, a 300 image wedding
                       renders roughly 180000px tall, one 600px thumbnail per
                       line, and the page becomes unusable.
     .admin-focal      establishes the positioning context the focal dot's
                       inline left/top percentages need in order to move at all.
     .admin-photos     scrolls rather than growing without limit, for the same
                       reason as the cover grid.

   Local tokens, so the admin can be tuned without touching the client surface. */
.page-admin {
  --panel: rgba(255,255,255,.035);
  --panel-2: rgba(255,255,255,.055);
  --line: rgba(160,160,160,.22);
  --line-strong: rgba(160,160,160,.4);
  --ok: #4fb477;
  --warn: #e0574f;
  --r: 10px;
  background:
    radial-gradient(1100px 620px at 15% -10%, rgba(201,162,39,.07), transparent 60%),
    var(--bg);
  min-height: 100%;
}
html[data-theme="light"] .page-admin {
  --panel: rgba(0,0,0,.025);
  --panel-2: rgba(0,0,0,.045);
  --line: rgba(0,0,0,.14);
  --line-strong: rgba(0,0,0,.26);
}

/* ------------------------------------------------------------- app bar
   One frame on every admin screen, so the three pages read as one product
   rather than three documents. */
.admin-bar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  gap: .75rem 1.25rem;
  align-items: center;
  padding: .85rem clamp(1rem, 4vw, 2rem);
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
}
.admin-brand {
  display: flex;
  align-items: baseline;
  gap: .55rem;
  font-family: var(--heading-font);
  letter-spacing: .12em;
  text-transform: uppercase;
  font-size: .9rem;
  color: var(--accent);
  text-decoration: none;
}
.admin-brand span { font-family: var(--body-font); letter-spacing: .18em; font-size: .62rem; opacity: .55; color: var(--fg); }
.admin-bar nav { display: flex; gap: .3rem; margin-left: auto; align-items: center; }
.admin-bar nav a, .admin-bar nav button {
  padding: .45rem .8rem;
  border-radius: 999px;
  font-size: .82rem;
  color: var(--fg);
  text-decoration: none;
  opacity: .72;
  border: 1px solid transparent;
}
.admin-bar nav a:hover, .admin-bar nav button:hover { opacity: 1; background: var(--panel-2); }
.admin-bar nav a[aria-current="page"] { opacity: 1; border-color: var(--line-strong); background: var(--panel); }

/* --------------------------------------------------------------- shell */
.admin { max-width: 78rem; margin: 0 auto; padding: 2rem clamp(1rem, 4vw, 2rem) 5rem; }
.admin-login { max-width: 23rem; padding-top: min(14vh, 7rem); }
.admin h1 {
  font-family: var(--heading-font);
  font-size: clamp(1.5rem, 3.4vw, 2.1rem);
  letter-spacing: .01em;
  text-transform: none;
  margin: 0 0 .3rem;
  color: var(--fg);
}
.admin-login h1 { color: var(--accent); font-size: 1.5rem; letter-spacing: .06em; }
.admin h2 {
  font-size: .72rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 .9rem;
  padding: 0;
  border: 0;
}
.admin h3 { font-size: 1rem; margin: 0 0 .4rem; }
.admin-head {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  align-items: flex-end;
  justify-content: space-between;
  margin-bottom: 1.75rem;
}
.admin-head p { margin: 0; display: flex; gap: 1rem; font-size: .88rem; }
.admin-head a { color: var(--accent); text-decoration: none; border-bottom: 1px solid transparent; }
.admin-head a:hover { border-bottom-color: var(--accent); }
.admin-sub { margin: .15rem 0 0; color: var(--muted); font-size: .9rem; }
.admin-inline { margin: 0; }

/* --------------------------------------------------------------- panel */
.admin-panel {
  padding: 1.15rem 1.25rem;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--panel);
  margin: 0 0 1.25rem;
}
.admin-panel > :last-child { margin-bottom: 0; }

/* ---------------------------------------------------------- album cards
   A gallery product listing its galleries as a bare table of slugs was the
   single worst thing about the old screen. The cover is the identifier a
   photographer actually recognises. */
.admin-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1.1rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.admin-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--panel);
  overflow: hidden;
  transition: border-color .18s ease, transform .18s ease;
}
.admin-card:hover { border-color: var(--line-strong); transform: translateY(-2px); }
.admin-card-cover {
  display: block;
  aspect-ratio: 4 / 3;
  width: 100%;
  object-fit: cover;
  background: rgba(128,128,128,.14);
}
.admin-card-empty {
  display: flex;
  aspect-ratio: 4 / 3;
  align-items: center;
  justify-content: center;
  background: rgba(128,128,128,.1);
  color: var(--muted);
  font-size: .8rem;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.admin-card-body { padding: .85rem .95rem 1rem; display: flex; flex-direction: column; gap: .5rem; flex: 1; }
.admin-card-title { margin: 0; font-size: 1.02rem; line-height: 1.3; }
.admin-card-title a { color: inherit; text-decoration: none; }
.admin-card-title a:hover { color: var(--accent); }
.admin-card-meta { margin: 0; font-size: .78rem; color: var(--muted); display: flex; gap: .5rem; flex-wrap: wrap; }
.admin-card-meta code { font-size: .95em; opacity: .85; }
.admin-card-actions { margin-top: auto; display: flex; gap: .5rem; padding-top: .35rem; }
.admin-card-actions a {
  flex: 1;
  text-align: center;
  padding: .45rem .5rem;
  border: 1px solid var(--line-strong);
  border-radius: 7px;
  font-size: .8rem;
  color: var(--fg);
  text-decoration: none;
}
.admin-card-actions a:hover { border-color: var(--accent); color: var(--accent); }

/* ---------------------------------------------------------------- pills
   Status a photographer can read at a glance. Colour is the signal, but never
   the ONLY signal: each pill also carries its own words, so the meaning
   survives a colour blind operator and a greyscale screen. */
.admin-pills { display: flex; flex-wrap: wrap; gap: .35rem; margin: 0; }
.admin-pill {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .2rem .55rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  font-size: .7rem;
  letter-spacing: .04em;
  color: var(--muted);
  white-space: nowrap;
}
.admin-pill[data-state="on"] { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 55%, transparent); }
.admin-pill[data-state="off"] { color: var(--muted); }
.admin-pill[data-state="warn"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 55%, transparent); }
.admin-pill[data-state="accent"] { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 55%, transparent); }

/* ---------------------------------------------------------------- forms */
.admin-form { display: flex; flex-direction: column; gap: .3rem; }
.admin-form label { font-size: .85rem; margin-top: .8rem; color: var(--muted); }
.admin-form input[type="text"], .admin-form input[type="password"], .admin-form input[type="date"],
.admin-form input[type="email"], .admin-form textarea, .admin-link input {
  padding: .6rem .7rem;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--bg);
  color: inherit;
  font: inherit;
  width: 100%;
}
.admin-form input:focus, .admin-form textarea:focus { border-color: var(--accent); outline: none; }
.admin-form textarea { resize: vertical; }
.admin-form button[type="submit"], .admin-btn {
  align-self: flex-start;
  margin-top: 1.4rem;
  padding: .7rem 1.5rem;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: #111;
  font: inherit;
  font-weight: 600;
  letter-spacing: .03em;
  cursor: pointer;
}
.admin-form button[type="submit"]:hover, .admin-btn:hover { filter: brightness(1.08); }
.admin-btn-quiet {
  background: transparent;
  color: var(--fg);
  border: 1px solid var(--line-strong);
  font-weight: 500;
}
.admin-btn-quiet:hover { border-color: var(--accent); color: var(--accent); filter: none; }

/* Switch rows. The three that decide whether a client's private photographs
   stay private carry the accent; the rest are plain. */
.admin-form label:has(input[type="checkbox"]), .admin-form label:has(input[type="radio"]), .admin-switch {
  display: flex;
  gap: .6rem;
  align-items: center;
  margin-top: .5rem;
  padding: .65rem .8rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  color: var(--fg);
  font-size: .92rem;
  cursor: pointer;
}
.admin-form label:has(input[type="checkbox"]):hover, .admin-switch:hover { border-color: var(--line-strong); }
.admin-switch,
.admin-form label:has(input[name="passwordEnabled"]),
.admin-form label:has(input[name="downloadsEnabled"]),
.admin-form label:has(input[name="downloadPinEnabled"]) {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  font-weight: 600;
}
.admin-form input[type="checkbox"], .admin-form input[type="radio"] { flex: 0 0 auto; accent-color: var(--accent); width: 1.05rem; height: 1.05rem; }
.admin-radios { display: flex; gap: .6rem; }
.admin-radios label { flex: 1; margin-top: .5rem; }

/* --------------------------------------------------------- edit layout
   Two columns on a desktop: settings on the left, and a sticky sidebar on the
   right holding the client link, the delivery mail panel and the read back of
   what the client can actually see. One column on a phone, sidebar first,
   because the link and the live status are what a photographer opens on a
   phone to check. */
.admin-cols { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
@media (min-width: 62rem) {
  .admin-cols { grid-template-columns: minmax(0, 1.55fr) minmax(19rem, .95fr); align-items: start; }
  .admin-side { position: sticky; top: 5rem; }
}
.admin-side { display: flex; flex-direction: column; gap: 1.25rem; }
.admin-side .admin-panel { margin: 0; }

/* The plain English read back. An album can carry a PIN and a download window
   and still show the client no download button, because downloadsEnabled is a
   separate switch. Stating what is actually true is the fix. */
.admin-state { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: .55rem; }
.admin-state li { display: flex; gap: .6rem; align-items: flex-start; font-size: .88rem; line-height: 1.45; }
.admin-state li::before { content: "•"; color: var(--muted); line-height: 1.45; }
.admin-state li[data-on="yes"]::before { content: "●"; color: var(--ok); }
.admin-state li[data-on="no"]::before { content: "○"; color: var(--muted); }
.admin-state li[data-on="warn"]::before { content: "▲"; color: var(--warn); font-size: .8em; }
.admin-state strong { font-weight: 600; }

/* ------------------------------------------------------- client link */
.admin-link { margin: 0; }
.admin-link h2 { margin-top: 0; }
.admin-link input { font-size: .82rem; }
.admin-link button { margin-top: .6rem; padding: .5rem 1rem; border: 1px solid var(--accent); border-radius: 8px; color: var(--accent); background: transparent; cursor: pointer; }
.admin-link button:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.admin-link p { margin: .7rem 0 0; font-size: .85rem; }

/* ----------------------------------------------------------- delivery */
.admin-delivery { margin: 0; padding: 1.15rem 1.25rem; border: 1px solid var(--line); border-radius: var(--r); background: var(--panel-2); }
.admin-delivery h2 { margin-top: 0; }
.admin-delivery .admin-form button[type="submit"] { margin-top: 1rem; }
.admin-hint { margin: .35rem 0 0; font-size: .8rem; line-height: 1.5; color: var(--muted); }

/* ------------------------------------------------------------ uploads
   The drop zone and the queue. Progress is per file because the transport is
   one photo per request: the host caps max_file_uploads at 20 and drops the
   rest silently, so a single multi file POST would lose photos from any real
   wedding. */
.admin-upload { margin: 0 0 1.25rem; }
.admin-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .55rem;
  padding: 2rem 1.25rem;
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--r);
  background: var(--panel);
  text-align: center;
  cursor: pointer;
  transition: border-color .16s ease, background .16s ease;
}
.admin-drop:hover, .admin-drop:focus-visible { border-color: var(--accent); }
.admin-drop[data-dragging="yes"] { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.admin-drop strong { font-size: 1rem; font-weight: 600; }
.admin-drop span { font-size: .84rem; color: var(--muted); }
.admin-drop input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

.admin-queue { margin: 1rem 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: .4rem; }
.admin-queue li {
  display: flex;
  gap: .7rem;
  align-items: center;
  padding: .5rem .7rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: .84rem;
  background: var(--panel);
}
.admin-queue .admin-qname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.admin-queue .admin-qstate { font-size: .76rem; color: var(--muted); white-space: nowrap; }
.admin-queue li[data-state="done"] .admin-qstate { color: var(--ok); }
.admin-queue li[data-state="failed"] { border-color: color-mix(in srgb, var(--warn) 50%, transparent); }
.admin-queue li[data-state="failed"] .admin-qstate { color: var(--warn); }
.admin-queue button {
  padding: .28rem .7rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  font-size: .74rem;
  color: var(--fg);
  background: transparent;
  cursor: pointer;
}
.admin-queue button:hover { border-color: var(--accent); color: var(--accent); }
.admin-progress { margin: .9rem 0 0; font-size: .84rem; color: var(--muted); }
.admin-progress[hidden] { display: none; }

/* -------------------------------------------------- photos / cover pick
   Bounded height and a scroll: a 300 image album must not make the settings
   form unreachable. */
.admin-photos { max-height: 27rem; overflow-y: auto; padding: .2rem; border: 1px solid var(--line); border-radius: var(--r); background: var(--panel); }
.admin-covergrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: .5rem; margin: 0; }
.admin-coverpick { position: relative; display: block; cursor: pointer; border: 2px solid transparent; border-radius: 8px; overflow: hidden; }
.admin-coverpick img { width: 100%; height: 88px; object-fit: cover; background: rgba(128,128,128,.15); display: block; }
.admin-coverpick input[type="radio"] { position: absolute; top: .3rem; left: .3rem; z-index: 1; accent-color: var(--accent); }
.admin-coverpick:has(input:checked) { border-color: var(--accent); }
.admin-coverpick:has(input:checked)::after {
  content: "Cover";
  position: absolute;
  right: .3rem;
  bottom: .3rem;
  padding: .1rem .4rem;
  border-radius: 999px;
  background: var(--accent);
  color: #111;
  font-size: .6rem;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.admin-empty { margin: 0; padding: 2.5rem 1rem; text-align: center; color: var(--muted); font-size: .9rem; }

/* ----------------------------------------------------------- focal point */
.admin-focal { position: relative; display: inline-block; max-width: 100%; line-height: 0; cursor: crosshair; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.admin-focal img { width: 100%; height: auto; }
.admin-focal-dot { position: absolute; width: 18px; height: 18px; margin: -9px 0 0 -9px; border: 2px solid #fff; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 1px rgba(0,0,0,.6); pointer-events: none; }

/* -------------------------------------------------------------- messages */
.admin-error { margin: 0 0 1.25rem; padding: .8rem 1rem; border: 1px solid color-mix(in srgb, var(--warn) 55%, transparent); border-left-width: 3px; border-radius: 8px; background: color-mix(in srgb, var(--warn) 12%, transparent); color: #f0938c; }
.admin-notice { margin: 0 0 1.25rem; padding: .8rem 1rem; border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent); border-left-width: 3px; border-radius: 8px; background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); }

/* ------------------------------------------------------ tables and lists */
.admin-table { width: 100%; border-collapse: collapse; font-size: .88rem; }
.admin-table th { text-align: left; font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); padding: .5rem .6rem; border-bottom: 1px solid var(--line-strong); }
.admin-table td { padding: .6rem; border-bottom: 1px solid var(--line); vertical-align: top; }
.admin-table tr:last-child td { border-bottom: 0; }
.admin-table td a { color: var(--accent); margin-right: .8rem; }
.admin-when { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Activity: the event log reads as a timeline, and the raw type strings are
   translated in the template rather than shown to a photographer. */
.admin-activity .admin-panel { margin-bottom: 1.5rem; }
.admin-events { margin: 0; }
.admin-filter { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1rem; }
.admin-filter button {
  padding: .35rem .85rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  font-size: .78rem;
  color: var(--fg);
  background: transparent;
  cursor: pointer;
  opacity: .7;
}
.admin-filter button[aria-pressed="true"] { opacity: 1; border-color: var(--accent); color: var(--accent); }
.admin-clients { margin: 0; }
.admin-client { margin: 0 0 1rem; padding: 1rem 1.1rem; border: 1px solid var(--line); border-radius: var(--r); background: var(--panel); }
.admin-client:last-child { margin-bottom: 0; }
.admin-client textarea { width: 100%; margin-top: .4rem; padding: .55rem; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--bg); color: inherit; font: .82rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
.admin-consent { font-size: .88rem; color: var(--muted); margin: .2rem 0 .6rem; }
.admin-consent[data-consent="revoked"] strong { color: var(--warn); }
.admin-consent[data-consent="granted"] strong { color: var(--ok); }
.admin-favgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: .45rem; margin: .6rem 0; }
.admin-favgrid img { width: 100%; height: 84px; object-fit: cover; border-radius: 6px; background: rgba(128,128,128,.15); }

/* New album. A short inline form rather than a screen of its own: the whole
   point is that creating a gallery no longer needs an SSH session. */
.admin-new { margin: 0 0 1.5rem; }
.admin-new .admin-form { flex-direction: row; flex-wrap: wrap; gap: .6rem; align-items: flex-end; }
.admin-new .admin-field { display: flex; flex-direction: column; gap: .3rem; flex: 1 1 12rem; }
.admin-new .admin-field label { margin: 0; }
.admin-new .admin-form button[type="submit"] { margin-top: 0; }

/* Page scoped hooks. .admin-albums and .admin-edit exist so a future rule can
   target one screen without touching the others; they carry no declarations of
   their own beyond the shared shell, and are named here so the
   template-to-stylesheet parity test can see them. */
.admin-albums { display: block; }
.admin-edit { display: block; }

/* ------------------------------------------------------------ gallery bar
   Pinned so the album download is reachable from anywhere in a long gallery.
   It anchors to the real form below rather than duplicating it, so there is
   exactly one submit path, one PIN field and one CSRF token. */
.galbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 20px;
  align-items: center;
  padding: 13px clamp(14px, 4vw, 32px);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(12px);
  border-bottom: 1px solid var(--line);
}
.galbar-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.galbar-name {
  font-family: var(--heading-font, "Cormorant Garamond", Georgia, serif);
  font-size: 19px;
  line-height: 1.15;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.galbar-meta { font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--faint); }
.galbar-actions { margin-left: auto; display: flex; gap: 10px; align-items: center; }
.galbar-download {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 11px 22px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent);
  color: #17130d;
  text-decoration: none;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 2.75px;
  text-transform: uppercase;
  transition: background var(--speed) var(--ease), border-color var(--speed) var(--ease);
}
.galbar-download:hover, .galbar-download:focus {
  background: var(--accent-bright, var(--accent));
  border-color: var(--accent-bright, var(--accent));
}
.galbar-download svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.8; }
@media (max-width: 460px) {
  /* The label is the affordance; the icon alone is not. Shrink the padding
     rather than hiding the words. */
  .galbar-download { padding: 10px 14px; letter-spacing: 1.6px; }
}

/* The grid header and title, on the brand faces. */
.grid-title { font-family: var(--heading-font, "Cormorant Garamond", Georgia, serif); font-weight: 500; }

/* ---------------------------------------------------- admin: cover styles */
.admin-styles { display: grid; gap: 8px; margin-top: 12px; }
.admin-style {
  display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: start;
  padding: 12px 14px; border: 1px solid var(--line-soft); border-radius: var(--radius);
  cursor: pointer; transition: border-color var(--speed) var(--ease), background var(--speed) var(--ease);
}
.admin-style:hover { border-color: var(--line); }
.admin-style:has(input:checked) { border-color: var(--accent); background: rgba(198,157,104,.07); }
.admin-style input { margin-top: 3px; }
.admin-style-body { display: grid; gap: 3px; }
.admin-style-body strong { font-size: 14px; font-weight: 600; }
.admin-style-body span { font-size: 12.5px; line-height: 1.5; color: var(--faint); }

.admin-scrim { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.admin-scrim[hidden] { display: none; }
.admin-scrim h3 { margin: 0 0 4px; font-size: 13px; font-weight: 600; letter-spacing: .04em; }
.admin-scrim label { display: block; margin-top: 7px; font-size: 13px; }

/* The preview frame. The cover is designed for a full viewport, so it is
   rendered at desktop width and scaled down, rather than shown at sidebar
   width where every layout would collapse to its mobile form and none of them
   could be told apart. */
.admin-preview {
  position: relative; width: 100%; aspect-ratio: 16 / 10; overflow: hidden;
  border: 1px solid var(--line-soft); border-radius: var(--radius);
  background: var(--bg-raised);
}
.admin-preview-frame {
  position: absolute; top: 0; left: 0;
  width: 1440px; height: 900px; border: 0;
  transform: scale(.243); transform-origin: 0 0;   /* 1440 * .243 ≈ 350px */
  pointer-events: none;                            /* the frame is for looking at */
}
@media (min-width: 1500px) {
  .admin-preview-frame { transform: scale(.30); }
}

/* A hint that reports an active protection, rather than describing a field. */
.admin-hint-on { color: var(--ink); border-left: 2px solid var(--accent); padding-left: 10px; }

/* Delivery result, reported in place instead of by reloading the page. */
.admin-msg { margin: 10px 0 0; font-size: 13px; line-height: 1.5; }
.admin-msg-good { color: var(--accent); }
.admin-msg-bad { color: #e2856f; }
