
/* The download screen: the moment just after someone has paid.
   It is a handover, so it is built to reassure first and act second. The
   receipt strip answers "did that work?" before the eye reaches the photos,
   and every tile is a single large target that says what it does. */
.receipt {
  /* Flex, not a fixed column count: a card order carries no reference, so the
     strip has two cells rather than three and a fixed grid left an empty panel
     hanging off the end. The cells divide whatever is there. */
  display: flex; flex-direction: column; gap: 1px;
  background: var(--color-line);
  border: 1px solid var(--color-line);
  border-radius: 12px;
  overflow: hidden;
}
@media (min-width: 560px) { .receipt { flex-direction: row; } }
.receipt__cell {
  flex: 1 1 0; min-width: 0;
  background: var(--color-surface); padding: 1rem 1.15rem;
  display: flex; flex-direction: column; gap: .3rem;
}
.receipt__k {
  font-size: .68rem; font-weight: 600; letter-spacing: .14em;
  text-transform: uppercase; color: var(--color-muted);
}
.receipt__v { font-family: var(--font-display); font-size: 1.05rem; font-weight: 600; font-variant-numeric: tabular-nums; }

.dl-grid { display: grid; gap: 1rem; grid-template-columns: repeat(2, 1fr); }
@media (min-width: 640px) { .dl-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1024px) { .dl-grid { grid-template-columns: repeat(4, 1fr); } }

.dl {
  border: 1px solid var(--color-line);
  border-radius: 10px; overflow: hidden;
  background: var(--color-surface);
}
.dl:focus-within { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.dl a { display: block; color: inherit; text-decoration: none; }
.dl__frame { position: relative; display: block; aspect-ratio: 4 / 5; overflow: hidden; background: var(--color-line); }
.dl__frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* A quiet index so a buyer working through twenty photos can keep their place.
   It is a label, not a control: the old "Download 1" made an index look like
   the name of the action. */
.dl__index {
  position: absolute; top: .4rem; left: .4rem;
  min-width: 1.35rem; height: 1.35rem; padding: 0 .3rem;
  display: grid; place-items: center;
  border-radius: 4px; font-size: .7rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: rgba(20, 19, 16, .72); color: var(--color-text);
  backdrop-filter: blur(4px);
}
.dl__action {
  display: flex; align-items: center; justify-content: center; gap: .4rem;
  min-height: 46px; padding: 0 .6rem;
  font-size: .88rem; font-weight: 600;
  border-top: 1px solid var(--color-line);
  color: var(--color-accent);
}
.dl__action svg { width: 15px; height: 15px; flex: none; }
.dl a:hover .dl__action { background: var(--color-accent); color: var(--color-ink); border-top-color: var(--color-accent); }

/* The remainder Tailwind cannot emit.
   Every Worker class now compiles via layouts/partials/tailwind-safelist.html,
   with one exception: bracketed arbitrary values and fractional scale steps
   (aspect-[4/5], min-h-[44px], h-1.5, tracking-[0.2em]) are present in
   hugo_stats.json but do not survive its JSON scan. Rather than leave them to
   fail silently a fourth time, they are real rules with real names. */
.photo-tile { aspect-ratio: 4 / 5; }
.wordmark { letter-spacing: .2em; }
.wordmark-dot { display: inline-block; width: .375rem; height: .375rem; background: var(--color-accent); }
.tap { display: inline-flex; align-items: center; min-height: 44px; }
.h-tight { letter-spacing: -.01em; }
.ref-code { letter-spacing: .06em; font-variant-numeric: tabular-nums; }

/* Event cards on the store index.
   Written here rather than as Tailwind utilities because this markup lives in
   the Worker, which Hugo does not scan: see the note in assets/css/main.css. */
.ecards { display: grid; gap: .75rem; grid-template-columns: 1fr; }
@media (min-width: 640px) { .ecards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .ecards { grid-template-columns: repeat(3, 1fr); } }

.ecard {
  border: 1px solid var(--color-line);
  background: var(--color-surface);
  border-radius: 12px;
  overflow: hidden;
}
.ecard a { display: block; color: inherit; text-decoration: none; }
.ecard:focus-within { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Fixed 4:3, image cropped to fill. Her library is near half landscape and half
   portrait, so honouring each frame's own shape would give a ragged grid. */
.ecard__frame { aspect-ratio: 4 / 3; overflow: hidden; background: var(--color-line); }
.ecard__frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
@media (prefers-reduced-motion: no-preference) {
  .ecard__frame img { transition: transform .5s ease; }
  .ecard:hover .ecard__frame img { transform: scale(1.04); }
}

.ecard__body { padding: 1rem; }
@media (min-width: 640px) { .ecard__body { padding: 1.15rem; } }
.ecard__title { font-family: var(--font-display); font-size: 1.15rem; font-weight: 600; line-height: 1.25; }
.ecard__meta { margin-top: .25rem; font-size: .875rem; color: var(--color-muted); }

/* Selection state on a gallery tile. */
/* --- the gallery tile ----------------------------------------------------
   Opening and choosing are two controls, because they were one and the tile
   is a crop. See the note in views.js. */
.tile { position: relative; }
.tile__open {
  display: block; width: 100%; padding: 0; border: 0; background: none;
  cursor: zoom-in; position: relative; border-radius: 6px; overflow: hidden;
}
.tile__open:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 2px; }
.tile input[data-pick]:checked ~ .tile__open img,
.tile input[data-pick]:checked + label + .tile__open img { outline: 3px solid var(--color-accent); outline-offset: -3px; }
.tile:has(input[data-pick]:checked) .tile__open img { outline: 3px solid var(--color-accent); outline-offset: -3px; }
/* Says the picture opens, without waiting for a hover that a phone never has. */
.tile__zoom {
  position: absolute; left: .5rem; bottom: .5rem; width: 30px; height: 30px;
  border-radius: 8px; display: grid; place-items: center;
  background: rgba(20, 19, 16, .55); color: var(--color-text);
  border: 1px solid rgba(244, 241, 234, .25); backdrop-filter: blur(2px);
}
/* Choosing: a real target with a word on it, not a decoration in a corner. */
.pick {
  position: absolute; top: .4rem; right: .4rem; display: inline-flex; align-items: center;
  gap: .35rem; min-height: 34px; padding: 0 .55rem 0 .45rem; border-radius: 999px;
  background: rgba(20, 19, 16, .62); border: 1px solid rgba(244, 241, 234, .28);
  color: var(--color-text); font-size: .78rem; font-weight: 600; cursor: pointer;
  backdrop-filter: blur(2px);
}
.pick:hover { border-color: var(--color-accent); }
.pick__box {
  width: 17px; height: 17px; border-radius: 999px; display: grid; place-items: center;
  border: 2px solid rgba(244, 241, 234, .75);
}
.tile input[data-pick]:checked ~ .pick { background: var(--color-accent); border-color: var(--color-accent); color: var(--color-ink); }
.tile input[data-pick]:checked ~ .pick .pick__box { border-color: var(--color-ink); background: var(--color-ink); }
.tile input[data-pick]:checked ~ .pick .pick__box::after {
  content: ""; width: 9px; height: 5px; margin-top: -2px;
  border-left: 2px solid var(--color-accent); border-bottom: 2px solid var(--color-accent);
  transform: rotate(-45deg);
}
.tile input[data-pick]:checked ~ .pick .pick__word::after { content: "n"; }
.tile input[data-pick]:focus-visible ~ .pick { outline: 3px solid var(--color-accent); outline-offset: 2px; }

/* --- the viewer ---------------------------------------------------------- */
.viewer {
  /* Viewport units, not percentages. A dialog is positioned against the
     viewport but height:100% had nothing definite to resolve against, so the
     picture grew past the bottom of the screen and took Choose, Previous and
     Next with it. dvh rather than vh so a phone's shrinking address bar does
     not do the same thing. */
  position: fixed; inset: 0; margin: 0;
  border: 0; padding: 0; background: transparent; color: var(--color-text);
  width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh;
  display: flex; flex-direction: column; gap: 0;
}
.viewer::backdrop { background: rgba(10, 10, 8, .92); }
.viewer:not([open]) { display: none; }
/* Flex, not grid, and this is the third attempt at it.
   As a grid with an auto row, the row sized itself to the picture and the
   picture sized itself to the row: circular, so the percentage was dropped and
   the image rendered at its intrinsic 1117px in an 805px window, clipped
   across the bottom. A flex container has a definite height here, which is
   what a percentage needs to resolve against. */
.viewer__stage {
  flex: 1 1 auto; min-height: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  padding: 3.5rem .75rem .75rem;
}
/* The image box fills the stage and the picture is scaled to fit inside it.
   max-height: 100% was the obvious way to write this and it did not hold: a
   percentage needs a definite height to resolve against, and the picture grew
   past the bottom of the window regardless. Filling a box that is already the
   right size needs no percentage at all.

   contain, never cover: the whole point of this screen is that nothing is
   cropped on it. */
.viewer__img {
  width: auto; height: auto; max-width: 100%; max-height: 100%;
  object-fit: contain; display: block;
}
.viewer__bar {
  flex: none; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: .6rem 1rem; padding: .75rem 1rem calc(.75rem + env(safe-area-inset-bottom));
  background: var(--color-surface); border-top: 1px solid var(--color-line);
}
.viewer__count { font-size: .85rem; color: var(--color-muted); font-variant-numeric: tabular-nums; }
.viewer__acts { display: flex; flex-wrap: wrap; gap: .4rem; }
.viewer__close {
  position: absolute; top: .6rem; right: .6rem; width: 44px; height: 44px;
  display: grid; place-items: center; border-radius: 999px; cursor: pointer;
  background: rgba(20, 19, 16, .7); border: 1px solid var(--color-line-strong);
  color: var(--color-text);
}
.viewer__close:hover { border-color: var(--color-accent); }

.cartbar {
  position: fixed; inset: auto 0 0 0; z-index: 60;
  background: var(--color-surface); border-top: 1px solid var(--color-line);
  padding: .75rem 1.25rem calc(.75rem + env(safe-area-inset-bottom));
}
.cartbar__inner {
  margin: 0 auto; max-width: 72rem;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
.cartbar__count { font-weight: 600; }
.cartbar__nudge {
  margin: .5rem auto 0; max-width: 72rem;
  font-size: .85rem; color: var(--color-accent); font-weight: 600;
}

/* --- Admin -------------------------------------------------------------
   Phone-first: Emma uploads from the sideline. Every control is a thumb
   target, primary actions are full-width, and 16px inputs stop iOS Safari
   zooming the page on focus. */
.afield {
  width: 100%; min-height: 48px; font-size: 1rem; border-radius: 8px;
  padding: .7rem .8rem; font-family: var(--font-body);
  background: var(--color-bg); color: var(--color-text);
  border: 1px solid var(--color-line-strong);
}
.afield:focus { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.admin__body { margin: 0 auto; max-width: 46rem; padding: 0 1.25rem 6rem; }
.admin-topbar { margin: 0 auto; max-width: 46rem; padding: 1rem 1.25rem 0; }
.admin-back {
  display: inline-flex; align-items: center; min-height: 44px;
  color: var(--color-muted); font-weight: 600;
}
.admin-back:hover { color: var(--color-text); }
.admin-h2 { font-family: var(--font-display); font-size: 1.15rem; font-weight: 600; margin: 2rem 0 .75rem; }
.admin-h2--flush { margin-top: 0; }
.admin-hint { color: var(--color-muted); font-size: .9rem; margin-bottom: 1rem; line-height: 1.5; }
.admin-empty { color: var(--color-muted); padding: 1.5rem 0; }
.admin-mt { margin-top: .6rem; }
.admin-mt-lg { margin-top: 2.5rem; }
.admin-danger { border: 1px solid var(--color-accent); color: var(--color-accent); background: none; }
.admin-danger:hover { background: var(--color-accent); color: var(--color-ink); }
.admin-code { word-break: break-all; font-size: .8rem; color: var(--color-text); }

.admin-card {
  border: 1px solid var(--color-line); border-radius: 12px;
  background: var(--color-surface); padding: 1.25rem; margin-bottom: 1.25rem;
}
.admin-card__summary {
  display: flex; align-items: center; justify-content: space-between;
  min-height: 44px; cursor: pointer; font-weight: 600; list-style: none;
}
.admin-card__summary::-webkit-details-marker { display: none; }
.admin-card__chev { color: var(--color-accent); font-size: 1.5rem; line-height: 1; }
details[open] .admin-card__chev { transform: rotate(45deg); }
.admin-form { display: grid; gap: .5rem; margin-top: 1rem; }
.admin-label { font-size: .85rem; font-weight: 600; margin-top: .5rem; }
.admin-btn-wide { width: 100%; margin-top: 1rem; }
/* A disabled primary button must not look tappable, or every tap that does
   nothing reads as the page being broken. */
.btn:disabled, .btn[disabled] { opacity: .45; cursor: not-allowed; }

/* Big, obvious file picker — the main thing she taps. */
.admin-file { display: block; }
.admin-file input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.admin-file__face {
  display: grid; place-items: center; min-height: 96px; cursor: pointer;
  border: 2px dashed var(--color-line-strong); border-radius: 12px;
  font-weight: 600; color: var(--color-text); text-align: center; padding: 1rem;
}
.admin-file__face:hover { border-color: var(--color-accent); }
.admin-file input:focus-visible + .admin-file__face { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.admin-progress { height: 6px; border-radius: 999px; background: var(--color-line); overflow: hidden; margin-top: 1rem; }
.admin-progress > div { height: 100%; width: 0; background: var(--color-accent); transition: width .2s; }
.admin-status { margin-top: .75rem; font-size: .9rem; color: var(--color-muted); }
.admin-failures { margin-top: .5rem; display: grid; gap: .35rem; font-size: .85rem; color: var(--color-accent); }

.admin-requests {
  border: 2px solid var(--color-accent); border-radius: 12px;
  background: var(--color-surface); padding: 1.25rem; margin-bottom: 1.25rem;
}
.admin-requests__acts { display: flex; gap: .4rem; flex-wrap: wrap; }

.admin-setup {
  border: 1px solid var(--color-accent); border-radius: 12px;
  background: var(--color-surface); padding: 1.25rem; margin-bottom: 1.25rem;
}
.admin-setup__list { display: grid; gap: .75rem; margin-top: .25rem; }
.admin-setup__list li { display: grid; grid-template-columns: 1.5rem 1fr; align-items: start; gap: .5rem; }
.admin-setup__mark {
  display: grid; place-items: center; width: 1.15rem; height: 1.15rem; margin-top: .15rem;
  border-radius: 999px; border: 2px solid var(--color-line-strong); font-size: .7rem; color: var(--color-ink);
}
.admin-setup__list .is-done .admin-setup__mark { background: var(--color-accent); border-color: var(--color-accent); }
.admin-setup__list .is-done .admin-setup__label { color: var(--color-muted); }
.admin-setup__label { display: block; font-weight: 600; }
.admin-setup__hint { display: block; margin-top: .15rem; font-size: .85rem; color: var(--color-muted); }

.admin-list { display: grid; gap: .5rem; }
.admin-row {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  min-height: 64px; padding: .85rem 1rem; border-radius: 10px;
  border: 1px solid var(--color-line); background: var(--color-surface);
}
.admin-row:hover { border-color: var(--color-line-strong); }
.admin-row__main { display: grid; gap: .15rem; min-width: 0; }
/* An address with no spaces in it is one long word. Let it break rather than
   push everything else out of shape. */
.admin-row__title { font-weight: 600; color: var(--color-text); overflow-wrap: anywhere; }
/* And whatever sits at the end of the row keeps its size: a long email used to
   squeeze the Remove button until the word "Remove" itself wrapped onto three
   lines. The text is what should give way, never the control. */
.admin-row > form, .admin-row > .btn, .admin-row > form > .btn { flex: none; }
.admin-row .btn { white-space: nowrap; }
.admin-row__meta { font-size: .85rem; color: var(--color-muted); }
.admin-pill {
  flex: none; font-size: .7rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  padding: .3rem .6rem; border-radius: 999px;
  border: 1px solid var(--color-line-strong); color: var(--color-muted);
}
.admin-pill--live { background: var(--color-accent); border-color: var(--color-accent); color: var(--color-ink); }

.admin-alert { border-radius: 10px; padding: .85rem 1rem; font-size: .9rem; font-weight: 600; margin-bottom: 1rem; }
.admin-alert--bad { border: 2px solid var(--color-accent); color: var(--color-accent); background: var(--color-surface); }
.admin-alert--good { border: 1px solid var(--color-line-strong); color: var(--color-text); background: var(--color-surface); }
.admin-alert[hidden] { display: none; }

.admin-orders { display: grid; gap: .75rem; }
.admin-order { border: 1px solid var(--color-line); border-radius: 10px; background: var(--color-surface); padding: 1rem; }
.admin-order__top { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.admin-order__who { font-weight: 600; word-break: break-all; }
.admin-order__amt { font-family: var(--font-display); font-weight: 600; flex: none; }
.admin-order__meta { margin-top: .25rem; font-size: .85rem; color: var(--color-muted); }
.admin-order__expired { color: var(--color-accent); font-weight: 600; }
.admin-order__link { display: grid; gap: .5rem; margin-top: .75rem; }
.admin-order__link input { font-size: .8rem; }

/* Copy and "New link" sit side by side and must share the row evenly.
   Copy is a bare button while "New link" is wrapped in a form, so giving
   only the form flex:1 let Copy's width:100% crush its neighbour into a
   one-letter-per-line column on a phone. Both children flex, so neither
   can starve the other. */
/* On a phone these wrap onto as many lines as they need, each button as wide
   as its own label. Sharing one row three ways left "New link" 87px, which
   wrapped the label onto two lines and stretched its neighbours to match, so a
   row of three buttons was half again as tall as it had any reason to be. */
.admin-order__acts { display: flex; gap: .5rem; flex-wrap: wrap; }
.admin-order__acts > * { flex: 1 1 auto; min-width: max-content; }
.admin-order__acts .btn { width: 100%; white-space: nowrap; }

/* Jump to a number.
   A shortcut into the gallery, never a claim to be all the photos of a child:
   the reading is right about three in four and only ever sees the most
   prominent player, so the way back to everything is always on screen. */
.jump { margin-bottom: 1.1rem; }
.jump__label { font-size: .72rem; font-weight: 600; letter-spacing: .13em; text-transform: uppercase; color: var(--color-muted); }
/* One line closed. See the note in views.js: open, this pushed the first
   photograph below the fold on the page people came to look at photographs. */
.jump__summary {
  display: flex; align-items: center; gap: .5rem; min-height: 44px;
  cursor: pointer; list-style: none; color: var(--color-muted);
  font-size: .82rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
}
.jump__summary::-webkit-details-marker { display: none; }
.jump__summary::after {
  content: ""; width: .45rem; height: .45rem; margin-left: .1rem;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px); transition: transform .15s ease;
}
details[open] > .jump__summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.jump__summary:hover { color: var(--color-text); }
.jump__summary:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.jump__count {
  font-size: .72rem; padding: .1rem .45rem; border-radius: 999px;
  border: 1px solid var(--color-line-strong); font-variant-numeric: tabular-nums;
}
/* What is on screen when a filter is on, so the state and the way out never
   depend on opening the fold. */
.filterbar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: .5rem 1rem; margin-bottom: .6rem; font-size: .9rem; color: var(--color-muted);
}
.filterbar__what strong { color: var(--color-text); }
.filterbar__all { color: var(--color-accent); text-decoration: underline; text-underline-offset: 4px; }
.jump__row { list-style: none; margin: .5rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: .4rem; }
.jump__chip {
  display: inline-flex; align-items: center; gap: .3rem; min-height: 38px;
  padding: 0 .7rem; border-radius: 999px; text-decoration: none;
  border: 1px solid var(--color-line-strong); color: var(--color-text);
  font-size: .88rem; font-weight: 600;
}
.jump__chip.is-on { background: var(--color-accent); color: var(--color-ink); border-color: var(--color-accent); }
.jump__chip:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.jump__n { font-size: .74rem; opacity: .7; font-variant-numeric: tabular-nums; }
.jump__hint { margin-top: .5rem; font-size: .8rem; color: var(--color-muted); }

/* Emma correcting a number on her own tile. */
.admin-photo__jersey {
  position: absolute; bottom: 44px; left: .4rem; width: 3.1rem;
  padding: .15rem .3rem; border-radius: 4px; border: 0;
  background: rgba(20,19,16,.78); color: var(--color-text);
  font-size: .8rem; font-weight: 600; text-align: center;
  font-variant-numeric: tabular-nums; backdrop-filter: blur(4px);
}
.admin-photo__jersey:focus { outline: 2px solid var(--color-accent); }

/* Asking for photos. */
.reqform { max-width: 34rem; }
.reqform__hint { margin-top: .35rem; font-size: .85rem; color: var(--color-muted); }
/* Hidden from people, not from bots: a bot fills it and gets the same reply a
   person gets, so nothing is learned by trying. Kept out of the accessibility
   tree as well as out of sight. */
.reqform__trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* After the downloads: the rest of the game.
   The highest-intent moment in the store is the one just after paying, and it
   used to end in a dead end. These are deliberately plainer than the download
   tiles, and carry no Download control, because they are not paid for. */
.more { margin-top: 3rem; border-top: 1px solid var(--color-line); padding-top: 1.75rem; }
.more__head { font-family: var(--font-display); font-size: 1.25rem; font-weight: 600; }
.more__hint { margin-top: .25rem; font-size: .88rem; color: var(--color-muted); }
.more__grid { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: .6rem; grid-template-columns: repeat(3, 1fr); }
@media (min-width: 640px) { .more__grid { grid-template-columns: repeat(6, 1fr); } }
@media (min-width: 1024px) { .more__grid { grid-template-columns: repeat(8, 1fr); } }
.more__item { border-radius: 8px; overflow: hidden; background: var(--color-line); }
.more__item img { display: block; width: 100%; aspect-ratio: 4/5; object-fit: cover; }
@media (prefers-reduced-motion: no-preference) {
  .more__item img { transition: opacity .25s ease; }
  .more__item:hover img { opacity: .82; }
}
.more__all { display: inline-flex; margin-top: 1rem; }

/* When she can shoot. */
.wd-row { display: grid; grid-template-columns: repeat(7, 1fr); gap: .35rem; }
.wd { position: relative; }
.wd input { position: absolute; inset: 0; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.wd span {
  display: grid; place-items: center; min-height: 46px; border-radius: 8px;
  border: 1px solid var(--color-line-strong); background: var(--color-surface);
  font-size: .82rem; font-weight: 600;
}
.wd input:checked + span { background: var(--color-accent); color: var(--color-ink); border-color: var(--color-accent); }
.wd input:focus-visible + span { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.dayblock { border: 1px solid var(--color-line); border-radius: 10px; margin-top: .75rem; overflow: hidden; }
.dayblock.is-off { opacity: .55; }
.dayblock__head {
  display: flex; align-items: center; justify-content: space-between; gap: .6rem;
  padding: .8rem .9rem; background: var(--color-surface);
}
.dayblock__name { display: block; font-weight: 600; }
.dayblock__meta { display: block; font-size: .8rem; color: var(--color-muted); }
.dayblock__games { list-style: none; margin: 0; padding: 0; }
.gm {
  display: flex; align-items: center; justify-content: space-between; gap: .6rem;
  padding: .6rem .9rem; border-top: 1px solid var(--color-line);
}
.gm.is-off { opacity: .5; }
.gm__title { display: block; font-size: .92rem; }
.gm__meta { display: block; font-size: .78rem; color: var(--color-muted); }

/* Somebody asking for photos. */
.req { border: 1px solid var(--color-line); border-radius: 10px; padding: 1rem; margin-top: .75rem; background: var(--color-surface); }
.req__what { font-weight: 600; }
.req__meta { margin-top: .2rem; font-size: .85rem; color: var(--color-muted); }
.req__contact { margin-top: .5rem; font-size: .9rem; }
.req__acts { margin-top: .7rem; }

/* --- find your number ----------------------------------------------------
   Flagged off for customers while the jersey pass has not been run. */
.numfind { margin-top: 2rem; max-width: 34rem; }
.numfind__label {
  display: block; font-size: .7rem; font-weight: 600; letter-spacing: .14em;
  text-transform: uppercase; color: var(--color-muted); margin-bottom: .5rem;
}
.numfind__row { display: flex; gap: .5rem; align-items: stretch; }
.numfind__input { flex: 1 1 auto; min-width: 0; font-variant-numeric: tabular-nums; }
.numfind__row .btn { flex: none; white-space: nowrap; }
.numfind__hint { margin-top: .6rem; font-size: .85rem; color: var(--color-muted); }
.numfind__found { margin-top: 2rem; color: var(--color-text); }
.numfind__caveat { display: block; margin-top: .3rem; font-size: .85rem; color: var(--color-muted); }
.numfind__none {
  margin-top: 2rem; padding: 1.25rem; border-radius: 12px;
  border: 1px solid var(--color-line); background: var(--color-surface);
  display: grid; gap: .75rem; justify-items: start; max-width: 40rem;
}
.numfind__none p { color: var(--color-muted); }
.numfind__none strong { color: var(--color-text); }
/* The badge that says this is not public yet. */
.numfind__only {
  margin-left: .5rem; font-size: .7rem; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--color-ink); background: var(--color-accent);
  padding: .15rem .45rem; border-radius: 999px; white-space: nowrap;
}
.numgroup { margin-top: 2.5rem; }
.numgroup__head { display: flex; flex-wrap: wrap; align-items: baseline;
                  justify-content: space-between; gap: .75rem; }
.numgroup__title { font-family: var(--font-display); font-size: 1.35rem; font-weight: 600; }
.numgroup__meta { margin-top: .2rem; font-size: .85rem; color: var(--color-muted); }
.numgrid {
  list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: .6rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 640px) { .numgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.numgrid img {
  display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: 8px; background: var(--color-surface);
}
/* The base rule sets no border at all, so a variant that only names a colour
   renders as bare text. Every variant carries its own full border. */
.admin-alert--note {
  border: 1px dashed var(--color-accent); color: var(--color-text);
  background: var(--color-surface); font-weight: 400;
}

/* Who is looking.
   All hand-written: Tailwind does not scan the Worker (see main.css), so a
   utility class used only here would ship as markup with no rule. */
.per-row { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
/* Repeated at the foot of the page, which is three screens from the top on a
   phone, so changing the window does not mean scrolling all the way back. */
.per-row--end { margin: 1.75rem 0 .5rem; }
.per {
  /* 44px, not 38: this is the only control on the screen and it is used with a
     thumb. */
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 1rem;
  border-radius: 999px; text-decoration: none; font-size: .86rem; font-weight: 600;
  border: 1px solid var(--color-line-strong); color: var(--color-text);
}
.per.is-on { background: var(--color-accent); color: var(--color-ink); border-color: var(--color-accent); }
.per:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.live { display: flex; align-items: baseline; gap: .5rem; margin-top: .25rem; }
.live__dot { width: .55rem; height: .55rem; border-radius: 999px; background: var(--color-line-strong); }
.live__dot.is-on { background: var(--color-accent); }
.live__n { font-family: var(--font-display); font-size: 2rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.live__k { color: var(--color-muted); font-size: .9rem; }
.live__feed { list-style: none; margin: .8rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .35rem; }
.live__feed li { display: flex; justify-content: space-between; gap: .6rem; font-size: .86rem; }
.live__what { color: var(--color-text); }
.live__meta { color: var(--color-muted); font-size: .78rem; white-space: nowrap; }

/* A change of "new" is not a percentage: change_pct is null when the previous
   window was zero, which happens constantly on a three-week-old site. */
.chg { font-size: .76rem; font-weight: 600; }
.chg--up { color: var(--color-accent); }
/* Down must not look like flat. They were the same grey, so a drop and a
   no-change week were literally indistinguishable. */
.chg--down { color: var(--color-text); opacity: .85; }
.chg--down::before { content: "↓ "; }
.chg--up::before { content: "↑ "; }
.chg--flat, .chg--new { color: var(--color-muted); }

/* A column chart, because 30 daily bars do not fit the row layout the sales
   screen uses for 12 months. */
/* --- admin navigation ---------------------------------------------------
   A strip that scrolls sideways rather than a row that wraps: six items do
   not fit across a phone, and a nav on two ragged lines reads as a bug. */
.anav-wrap {
  border-top: 1px solid var(--color-line); overflow-x: auto;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
  /* Shadows that appear only on the side there is more to see. The pairs are
     the standard trick: the cover gradients scroll WITH the content (local) and
     slide over the shadows, which are pinned to the frame (scroll). Without
     some hint, a strip that runs off the edge of a phone looks like a nav with
     four items rather than six. */
  background:
    linear-gradient(to right, var(--color-bg) 40%, rgba(20, 19, 16, 0)) left center / 32px 100% no-repeat local,
    linear-gradient(to left, var(--color-bg) 40%, rgba(20, 19, 16, 0)) right center / 32px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, .45), rgba(0, 0, 0, 0)) left center / 14px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, .45), rgba(0, 0, 0, 0)) right center / 14px 100% no-repeat scroll;
}
.anav-wrap::-webkit-scrollbar { display: none; }
.anav { display: flex; gap: .25rem; padding: 0 1.25rem; min-width: max-content; }
@media (min-width: 640px) { .anav { padding: 0 2rem; } }
.anav__item {
  position: relative; display: inline-flex; align-items: center;
  min-height: 48px; padding: 0 .75rem; white-space: nowrap;
  font-size: .92rem; font-weight: 600; text-decoration: none;
  color: var(--color-muted);
}
.anav__item:hover { color: var(--color-text); }
/* The marker is the underline, not a pill: it reads as "you are on this tab"
   rather than as a button waiting to be pressed. */
.anav__item.is-on { color: var(--color-text); }
.anav__item.is-on::after {
  content: ""; position: absolute; left: .75rem; right: .75rem; bottom: 0;
  height: 2px; background: var(--color-accent); border-radius: 2px 2px 0 0;
}
.anav__item:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }

/* The chart carries its own axis, because a title attribute needs a hover and
   the person reading this is on a phone. */
.spark { list-style: none; margin: .75rem 0 1.5rem; padding: 0; display: flex;
         align-items: flex-end; gap: 2px; height: 96px; }
/* The label sits OUTSIDE the plot, absolutely, so a bar's percentage height is
   measured against the plot alone. Laying the label out inside the column made
   100% mean "column including the label strip", and the baseline wobbled by
   three pixels between a tall bar and a short one. */
.spark__col { flex: 1 1 0; height: 100%; min-width: 2px; position: relative;
              display: flex; align-items: flex-end; }
.spark__bar { display: block; width: 100%; background: var(--color-accent);
              border-radius: 2px 2px 0 0; }
/* Zero is a grey stub on the baseline, not a short bar: a day nobody came is a
   different kind of fact from a day one person did, and at this scale a
   proportional bar for one person is a sliver. */
.spark__col.is-none .spark__bar { min-height: 2px; background: var(--color-line); }
.spark__col.is-peak .spark__bar { background: var(--color-accent); box-shadow: 0 0 0 1px var(--color-accent-soft); }
.spark__tick { position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
               padding-top: 6px; font-size: .68rem; line-height: 1;
               color: var(--color-muted); white-space: nowrap;
               font-variant-numeric: tabular-nums; }
.admin-strong { font-weight: 600; color: var(--color-text); }
.live__at { margin-left: auto; font-size: .78rem; color: var(--color-muted); }
.live-wrap { display: block; }

/* A row that goes somewhere says so, and gives a thumb the whole row to hit. */
.admin-row--link { padding: 0; }
.admin-row__hit { display: flex; align-items: center; gap: .75rem; width: 100%;
                  min-height: 44px; padding: .85rem 1rem; color: inherit; text-decoration: none; }
.admin-row__hit:hover { background: var(--color-surface-2, rgba(255,255,255,.04)); }
.admin-row__go { color: var(--color-muted); font-size: 1.25rem; line-height: 1; }
.stat--link { text-decoration: none; color: inherit; }
.stat--link:hover { border-color: var(--color-accent); }

.fun { list-style: none; margin: .75rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.fun__row { display: grid; grid-template-columns: 9.5rem 1fr auto; align-items: center; gap: .7rem; }
@media (max-width: 520px) { .fun__row { grid-template-columns: 7rem 1fr auto; gap: .45rem; } }
.fun__label { font-size: .84rem; color: var(--color-muted); }
.fun__track { height: 1.4rem; background: var(--color-surface); border-radius: 4px; overflow: hidden; }
.fun__bar { display: block; height: 100%; background: var(--color-accent); border-radius: 4px; }
.fun__n { font-weight: 600; font-variant-numeric: tabular-nums; font-size: .9rem; white-space: nowrap; }
.fun__pct { margin-left: .4rem; font-weight: 400; font-size: .78rem; color: var(--color-muted); }

/* Sales screen.
   The chart is divs, not a library: these pages carry no external scripts, and
   a bar chart of twelve rows does not need one. */
.stat-row { display: grid; gap: .75rem; grid-template-columns: repeat(2, 1fr); }
/* Three tiles in a two column grid left the third alone beside an empty cell. */
.stat-row > .stat:last-child:nth-child(odd) { grid-column: 1 / -1; }
@media (min-width: 560px) { .stat-row > .stat:last-child:nth-child(odd) { grid-column: auto; } }
@media (min-width: 560px) { .stat-row { grid-template-columns: repeat(3, 1fr); } }
.stat {
  background: var(--color-surface); border: 1px solid var(--color-line);
  border-radius: 10px; padding: .95rem 1rem;
  display: flex; flex-direction: column; gap: .25rem;
}
.stat__k { font-size: .7rem; font-weight: 600; letter-spacing: .13em; text-transform: uppercase; color: var(--color-muted); }
.stat__v { font-family: var(--font-display); font-size: 1.5rem; font-weight: 600; font-variant-numeric: tabular-nums; }

.chart { list-style: none; padding: 0; margin: .5rem 0 0; display: flex; flex-direction: column; gap: .5rem; }
.chart__row { display: grid; grid-template-columns: 6.5rem 1fr auto; align-items: center; gap: .7rem; }
@media (max-width: 480px) { .chart__row { grid-template-columns: 4.6rem 1fr auto; gap: .5rem; } }
.chart__label { font-size: .8rem; color: var(--color-muted); }
.chart__track { display: block; height: 1.5rem; background: var(--color-surface); border-radius: 4px; overflow: hidden; }
/* Scaled against her own best month, with a floor, so the quietest month is a
   visible bar rather than a hairline she cannot see. */
.chart__bar { display: block; height: 100%; background: var(--color-accent); border-radius: 4px; }
.chart__value { font-size: .85rem; font-weight: 600; font-variant-numeric: tabular-nums; }

.admin-row__amt { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* How many times a photograph has sold. */
.admin-photo__sold {
  position: absolute; top: .4rem; right: .4rem;
  min-width: 1.5rem; height: 1.5rem; padding: 0 .35rem;
  display: grid; place-items: center; border-radius: 4px;
  font-size: .72rem; font-weight: 700; font-variant-numeric: tabular-nums;
  background: var(--color-accent); color: var(--color-ink);
}

/* What was actually in an order. Small, because it is context on a row rather
   than a gallery: the point is recognising the shot, not studying it. */
.admin-order__photos { list-style: none; padding: 0; margin: .6rem 0 0; display: flex; flex-wrap: wrap; gap: .35rem; }
.admin-order__photos li { width: 60px; }
.admin-order__photos img { display: block; width: 100%; aspect-ratio: 4/5; object-fit: cover; border-radius: 4px; }
.admin-order.is-refunded { opacity: .72; }

/* Refunds. Deliberately quiet: never the primary action on a row, and armed by
   a first tap before a second one can move money. */
.admin-order__refund { color: var(--color-muted); }
.admin-order__refund[data-armed="yes"] { color: var(--color-ink); background: var(--color-accent); }
.admin-order__badge {
  display: inline-block; margin-left: .4rem; padding: .05rem .4rem; border-radius: 3px;
  font-size: .7rem; font-weight: 600; border: 1px solid var(--color-line-strong); color: var(--color-muted);
}
.admin-pager { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin-top: 1rem; }
.admin-pager__note { font-size: .85rem; color: var(--color-muted); font-variant-numeric: tabular-nums; }

.admin-photos { display: grid; grid-template-columns: repeat(2, 1fr); gap: .6rem; }

@media (min-width: 560px) {
  /* minmax(0, 1fr), not 1fr.
     A grid track sized 1fr will not shrink below its content's automatic
     minimum, and that content is a text input, whose minimum is around 24
     characters wide. So the input's column held 386px of a 664px card and the
     buttons column was left 268px for 446px of buttons. With flex: none and
     nowrap they did not wrap or shrink: they simply drew outside the card,
     Refund ending 178px past its right edge. */
  .admin-order__link { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
  .admin-order__link input { min-width: 0; }
  /* Sized to their labels here, rather than splitting the row evenly as they
     do on a phone. Copy kept width: 100%, which on a content-sized row meant
     it took the whole track and pushed the other two out. */
  .admin-order__acts > * { flex: none; }
  .admin-order__acts .btn { width: auto; }
  /* And if a card is ever narrower than its buttons anyway, they wrap instead
     of escaping. */
  .admin-order__acts { flex-wrap: wrap; justify-content: flex-end; }
  .admin-photos { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 860px) {
  .admin-photos { grid-template-columns: repeat(4, 1fr); }
}
.admin-photo { position: relative; border-radius: 8px; overflow: hidden; background: var(--color-surface); }
.admin-photo img { display: block; width: 100%; height: 100%; object-fit: cover; aspect-ratio: 4/5; }
.admin-photo.is-cover { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.admin-photo__cover {
  /* Top of the tile, opposite the delete button at the bottom, so the two
     cannot be confused by a thumb travelling up the grid. */
  position: absolute; top: .4rem; left: .4rem;
  display: inline-flex; align-items: center; min-height: 32px;
  padding: 0 .55rem; border-radius: 4px;
  font-size: .74rem; font-weight: 600; line-height: 1;
  border: 0; cursor: pointer;
  background: rgba(20, 19, 16, .78); color: var(--color-text);
  backdrop-filter: blur(4px);
}
.admin-photo__cover.is-cover {
  background: var(--color-accent); color: var(--color-ink); cursor: default;
}
.admin-photo__cover:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.admin-photo__del {
  /* 44px, the standard touch target. Safe to make this big because the
     first tap only arms it: the label becomes "Really delete?" and a
     second tap is required. */
  position: absolute; inset: auto 0 0 0; min-height: 44px;
  background: rgba(20,19,16,.82); color: var(--color-text);
  font-size: .8rem; font-weight: 600; border: 0; cursor: pointer;
}
.admin-photo__del:hover { background: var(--color-accent); color: var(--color-ink); }
