/* Chroma Noir — Studio 1.29.0 (2026-10-27)
   ---------------------------------------------------------------------------
   The version is here rather than in the filename: deploy.sh stamps ?v=<build>
   onto this stylesheet's URL on the staged copy and deliberately leaves the
   repo's filenames alone — see chromanoir-site/stamp-assets.sh.

   The admin surface. Same language as the rest of the subdomain (black, Inter
   Tight, Fraunces italic for the one editorial line, a single gold accent) but
   at a different density: this is a working tool, not a page to look at, and it
   has to stay legible with sixty films in it.

   THE ONE RULE THAT SHAPES EVERYTHING: gold is the accent, and an accent stops
   being one the moment it is used twice on a screen. So gold marks published
   films and the thing the pointer is on. It is not used for borders, headings,
   or "important" buttons. Everything else is four greys.

   Type sizes are deliberately few: 11px tracked uppercase for labels, 13px for
   metadata, 15px for content, and one clamp() display size. A fifth size would
   have to earn its place.
*/

:root {
  --ink: #f4f3f1;
  --dim: #8c8a86;
  --faint: #565451;
  --gold: #c9a14a;
  --rule: rgba(244, 243, 241, 0.1);
  --rule2: rgba(244, 243, 241, 0.22);
  --surface: #0a0a0a;
  --surface2: #111110;
  --danger: #e0666a;
  --sans: "Inter Tight", -apple-system, "Segoe UI", sans-serif;
  --serif: "Fraunces", Georgia, serif;
  --pad: clamp(20px, 5vw, 56px);
  color-scheme: dark;
}

* { box-sizing: border-box; }
html, body { min-height: 100%; }

body {
  margin: 0;
  background: #000;
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  /* Only the weights that are actually loaded exist. Without this a browser
     fakes a missing weight by smearing the one it has — the rest of the site
     sets the same rule. */
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}

/* Chrome's autofill paints its own background over a dark field and turns the
   text near-black on near-white. The inset shadow trick is the only way to
   override it; -webkit-text-fill-color is the only way to set the colour. */
input:-webkit-autofill,
input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 40px #000 inset;
  -webkit-text-fill-color: var(--ink);
}

/* ---- shared atoms --------------------------------------------------------- */

.label {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--dim);
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border: 1px solid var(--rule2);
  border-radius: 999px;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.2s, color 0.2s, background 0.2s;
}
.btn:hover:not(:disabled) { border-color: var(--gold); color: var(--gold); }
.btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
.btn:disabled { opacity: 0.35; cursor: default; }
.btn--solid { border-color: var(--ink); background: var(--ink); color: #000; }
.btn--solid:hover:not(:disabled) { background: var(--gold); border-color: var(--gold); color: #000; }
.btn--quiet { border-color: transparent; color: var(--dim); padding-left: 10px; padding-right: 10px; }
.btn--quiet:hover:not(:disabled) { border-color: transparent; color: var(--ink); }
.btn--danger:hover:not(:disabled) { border-color: var(--danger); color: var(--danger); }
.btn--sm { padding: 6px 12px; font-size: 12px; }
/* A DOWNLOAD IS AN ANCHOR WEARING A BUTTON. The CSV exports have to be real
   links — an href with `download` is the one thing that lands a file named by
   the server in a spreadsheet — and an anchor arrives underlined. (#portalIn in
   the sign-in overlay carries its own inline text-decoration:none and keeps it;
   it is left alone rather than quietly relying on this.) */
a.btn { text-decoration: none; }
/* AN ANCHOR HAS NO `disabled`. A download link with nothing behind it drops its
   href, which makes it neither focusable nor activatable, and says why through
   aria-disabled — so the look has to come from the attribute rather than from
   :disabled, which will never match an <a>. */
.btn[aria-disabled="true"] { opacity: 0.35; cursor: default; }

.link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--dim);
  text-decoration: none;
  cursor: pointer;
  transition: color 0.25s, gap 0.25s;
}
.link:hover { color: var(--ink); gap: 15px; }
.link:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; }

.field {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--rule2);
  border-radius: 2px;
  background: #000;
  color: var(--ink);
  font: inherit;
  font-size: 14px;
}
.field:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }

[hidden] { display: none !important; }

/* Visible only to a screen reader. Live-region announcements go here, because
   a progress bar that only exists as a moving pixel says nothing out loud. */
.sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- chrome --------------------------------------------------------------- */

.bar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 20px var(--pad);
  background: rgba(0, 0, 0, 0.86);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--rule);
}
.bar__mark { font-size: 13px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--faint); }
.bar__mark b { font-weight: 500; color: var(--dim); }
/* The right-hand end of the bar. Two controls kept together so space-between
   still reads as back / title / actions rather than as four equal things. */
.bar__acts { display: flex; align-items: center; gap: 20px; }

main { max-width: 1140px; margin: 0 auto; padding: 0 var(--pad) 120px; }

.head { padding: 44px 0 30px; }
.head h1 { margin: 0 0 10px; font-size: 11px; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; color: var(--dim); }
.head p {
  margin: 0;
  max-width: 46ch;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(18px, 2.4vw, 25px);
  line-height: 1.35;
  color: var(--dim);
}

/* ---- capacity ------------------------------------------------------------- */
/* Films here are half a gigabyte each on a volume that also carries the main
   site's API and its database. "How much room is left" is the most useful
   number on this page and the one nobody thinks to check until an upload fails,
   so it is above the drop zone rather than in a corner. */

.cap { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 22px; padding: 16px 0 14px; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
.cap__n { font-size: 13px; color: var(--dim); font-variant-numeric: tabular-nums; }
.cap__n b { font-weight: 500; color: var(--ink); }
.cap__bar { flex: 1 1 220px; min-width: 160px; height: 3px; background: rgba(244, 243, 241, 0.09); overflow: hidden; }
.cap__fill { display: block; height: 100%; width: 0; background: var(--dim); transition: width 0.4s ease; }
/* Only ever gold when it is a warning. Gold as "normal" would make the one
   state worth noticing invisible. */
.cap__fill.warn { background: var(--gold); }
.cap__fill.full { background: var(--danger); }

/* ---- drop zone ------------------------------------------------------------ */

.drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  margin: 28px 0 0;
  padding: 46px 24px;
  border: 1px dashed var(--rule2);
  border-radius: 3px;
  background: var(--surface);
  text-align: center;
  transition: border-color 0.2s, background 0.2s;
}
.drop.over { border-color: var(--gold); background: var(--surface2); }
/* Once there are films, the library is the page and the drop zone is a control
   on it. Full height for an empty studio, one line for a working one. */
.drop.slim { padding: 18px 22px; flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 10px 18px; }
.drop.slim .drop__line { order: -1; }
.drop.slim .drop__hint { flex-basis: 100%; text-align: center; }
/* The "folder upload needs a desktop" line, when it is showing, is a sentence
   and not a control — so it takes a line of its own in the slim row exactly as
   the hint above it does, rather than sitting between two buttons. */
.drop.slim .drop__folderline { flex-basis: 100%; text-align: center; }
.drop__line { margin: 0; font-size: 14px; color: var(--dim); }
.drop__hint { margin: 0; font-size: 12px; color: var(--faint); }
.drop__hint b { font-weight: 500; color: var(--dim); }
.drop__picks { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
input[type="file"] { display: none; }

/* ---- the upload queue ----------------------------------------------------- */

.queue { margin: 26px 0 0; padding: 0; list-style: none; border-top: 1px solid var(--rule); }
/* No queue, no strip and no margin. An empty <ul> that still reserves its
   spacing leaves a band of nothing between the drop zone and the library. */
.queue:empty { margin: 0; border-top: 0; }

.job { display: grid; grid-template-columns: 84px 1fr auto; gap: 4px 16px; align-items: center; padding: 14px 0; border-bottom: 1px solid var(--rule); }
.job__still {
  grid-row: 1 / 3;
  width: 84px;
  aspect-ratio: 16 / 9;
  background: #000;
  box-shadow: inset 0 0 0 1px var(--rule);
  object-fit: cover;
  display: block;
}
.job__name { font-size: 14px; font-weight: 500; word-break: break-word; }
.job__state { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.job__state.err { color: var(--gold); }
.job__meta { grid-column: 2 / 4; display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
.job__track { grid-column: 2 / 4; height: 2px; margin-top: 8px; background: rgba(244, 243, 241, 0.09); }
.job__fill { display: block; height: 100%; width: 0; background: var(--gold); transition: width 0.25s linear; }
.job__acts { grid-column: 3; display: flex; gap: 6px; justify-self: end; }

/* ---- toolbar -------------------------------------------------------------- */

.tools { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 34px 0 0; padding-bottom: 14px; border-bottom: 1px solid var(--rule); }
.tools__search { flex: 1 1 220px; max-width: 320px; }
.tabs { display: flex; gap: 2px; }
.tab { padding: 7px 14px; border: 0; border-radius: 999px; background: none; color: var(--dim); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: color 0.2s, background 0.2s; }
.tab:hover { color: var(--ink); }
.tab[aria-pressed="true"] { background: rgba(244, 243, 241, 0.08); color: var(--ink); }
.tab:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.tools__spacer { flex: 1 1 auto; }
.tools__count { font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
/* .field is width:100%, which in this flex row would push the count and
   Reorder off the end. Sized to sit beside btn--sm rather than beside the
   search box it borrows its border from. */
.tools__all { width: auto; flex: 0 0 auto; padding: 6px 10px; font-size: 13px; cursor: pointer; }

/* ---- the film list -------------------------------------------------------- */

.films { margin: 0; padding: 0; list-style: none; }

.film { border-bottom: 1px solid var(--rule); transition: background 0.2s; }
.film:hover { background: rgba(244, 243, 241, 0.015); }
.film.dragging { opacity: 0.35; }
.film.dropTarget { box-shadow: inset 0 2px 0 var(--gold); }

.film__row { display: grid; grid-template-columns: auto 176px minmax(0, 1fr) auto; gap: 18px; align-items: start; padding: 18px 0; }

.film__grip {
  align-self: center;
  width: 22px; height: 30px;
  border: 0; padding: 0;
  background: none;
  color: var(--faint);
  cursor: grab;
  /* Not display:none when reordering is off — a button that appears and
     disappears reflows the whole list under the pointer. */
  visibility: hidden;
}
.reordering .film__grip { visibility: visible; }
.film__grip:active { cursor: grabbing; }
.film__grip:hover { color: var(--ink); }
.film__grip:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

.film__thumb {
  position: relative;
  width: 176px;
  aspect-ratio: 16 / 9;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--rule);
  overflow: hidden;
  display: grid;
  place-items: center;
}
.film__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.film__thumb .none { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); }
.film__dur {
  position: absolute;
  right: 6px; bottom: 6px;
  padding: 2px 6px;
  background: rgba(0, 0, 0, 0.78);
  font-size: 11px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.film__body { min-width: 0; display: flex; flex-direction: column; gap: 8px; }

/* THE TITLE IS THE FIELD. Not a label with an edit button beside it — the thing
   you read is the thing you type into, and it only looks like a field once you
   are in it. Renaming a film should not require finding a mode first. */
.film__title {
  width: 100%;
  padding: 4px 8px;
  margin-left: -8px;
  border: 1px solid transparent;
  border-radius: 2px;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 17px;
  font-weight: 500;
  line-height: 1.3;
}
.film__title:hover { border-color: var(--rule); }
.film__title:focus { border-color: var(--rule2); background: #000; outline: none; }
.film__title:focus-visible { outline: 2px solid var(--gold); outline-offset: 0; }
.film__title::placeholder { color: var(--faint); font-style: italic; }

.film__desc {
  width: 100%;
  padding: 5px 8px;
  margin-left: -8px;
  border: 1px solid transparent;
  border-radius: 2px;
  background: none;
  color: var(--dim);
  font: inherit;
  font-size: 14px;
  line-height: 1.55;
  resize: none;
  overflow: hidden;
  min-height: 30px;
}
.film__desc:hover { border-color: var(--rule); }
.film__desc:focus { border-color: var(--rule2); background: #000; color: var(--ink); outline: none; }
.film__desc:focus-visible { outline: 2px solid var(--gold); outline-offset: 0; }
.film__desc::placeholder { color: var(--faint); font-style: italic; }

.film__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
.film__state { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; }
.film__state::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.film__state.published { color: var(--gold); }
.film__state.restricted { color: #b98fd6; }
.film__state.staged { color: var(--faint); }

/* A FILM NOBODY HAS OPENED gets a plain phrase where its Views button would be.
   Not a button — there is nothing behind it — but it keeps a btn--sm's box so
   the action row does not shift the day the first view arrives and the words
   become a control.

   --dim, not --faint: this is a fact about the film, and --faint on black is
   about 2,8:1, under the 4,5:1 a 12px line needs. */
.film__noviews {
  display: inline-flex;
  align-items: center;
  padding: 6px 12px;
  border: 1px solid transparent;
  font-size: 12px;
  color: var(--dim);
  white-space: nowrap;
}

/* Three states on one row. The pressed one is the only one that is legible at
   a glance, because the question a row answers is "who can see this?" and not
   "what are the options?". */
.film__vis { display: inline-flex; border: 1px solid var(--rule); border-radius: 999px; overflow: hidden; }
.vis { padding: 6px 12px; border: 0; background: none; color: var(--faint); font: inherit;
       font-size: 11px; font-weight: 500; letter-spacing: .06em; cursor: pointer;
       transition: color .2s, background .2s; }
.vis:hover:not(:disabled):not(.on) { color: var(--ink); }
.vis.on { background: rgba(244, 243, 241, 0.1); color: var(--ink); }
.vis:disabled { opacity: .5; cursor: default; }
.vis:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

/* THE RATING USES THE SAME CONTROL, deliberately. Who may see this film and who
   is it for are one question asked twice, and giving the second one a shape of
   its own would say they are unrelated.

   width:0 with min-width:100% on the hint is not a trick for its own sake. With
   an ordinary auto width this paragraph's max-content — the whole sentence on
   one line — becomes the intrinsic width of the actions column, and in the
   middle layout that column sizes to its contents: one film turning 18+ would
   drag the title field a quarter of the page narrower. A definite width
   contributes nothing intrinsically; the min-width then stretches it back to
   whatever the column actually turned out to be, and in the two row layouts it
   forces the sentence onto a line of its own.

   It doubles as the progress line while a rating is saving, because changing
   one MOVES the file and that is seconds rather than milliseconds on a
   half-gigabyte film. Empty it disappears entirely, so a film rated for
   everyone costs the row no space at all. */
/* --dim, not the --faint the rest of this row's metadata uses. Everything else
   on the row is a fact you can glance past; this is the one sentence on it that
   changes what a visitor has to do, and --faint measures about 2,8:1 on black. */
.film__ratehint {
  width: 0;
  min-width: 100%;
  margin: 4px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--dim);
}
.film__ratehint.busy { color: var(--dim); }
.film__ratehint.bad { color: var(--gold); }
.film__ratehint:empty { display: none; }

/* ---- featured --------------------------------------------------------------
   A plain pressed/unpressed pill rather than a fourth segmented control. There
   are only two answers and one of them is the usual one, so a control showing
   both would spend a row of the card saying "not featured" sixty times. */
.film__feat { letter-spacing: .06em; }
.film__feat[aria-pressed="true"] { border-color: var(--gold); color: var(--gold); }
.film__feat[aria-pressed="true"]:hover:not(:disabled) { border-color: var(--ink); color: var(--ink); }

/* ---- premieres --------------------------------------------------------------
   A field, so it lives with the other fields rather than among the buttons:
   the title, the description, the collection and the release moment are the
   four things about a film that are typed rather than pressed. */
.film__prem { display: flex; flex-wrap: wrap; align-items: center; }
.film__prem > * + * { margin-left: 8px; }
.film__premk {
  font-size: 11px; font-weight: 500; letter-spacing: .2em; text-transform: uppercase;
  color: var(--dim); white-space: nowrap;
}
.film__premin {
  padding: 4px 8px; border: 1px solid transparent; border-radius: 2px;
  background: none; color: var(--dim); font: inherit; font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.film__premin:hover { border-color: var(--rule); }
.film__premin:focus { border-color: var(--rule2); background: #000; color: var(--ink); outline: none; }
.film__premin:focus-visible { outline: 2px solid var(--gold); outline-offset: 0; }
/* Chrome paints the calendar picker's glyph near-black on a dark field. */
.film__premin::-webkit-calendar-picker-indicator { filter: invert(1); opacity: .45; cursor: pointer; }
.film__premin::-webkit-calendar-picker-indicator:hover { opacity: .9; }
.film__premhint {
  width: 0; min-width: 100%;
  margin: 4px 0 0; font-size: 12px; line-height: 1.5; color: var(--dim);
}
.film__premhint.busy { color: var(--dim); }
.film__premhint.bad { color: var(--gold); }
.film__premhint:empty { display: none; }
/* An announced film wears the one state word its neighbours cannot: it is not
   Private, Gated or Live yet — it is dated. */
.film__state.upcoming { color: var(--gold); }

/* The collection, edited in place like the title and the description. Quiet:
   most films will not have one. */
.film__coll { width: 100%; max-width: 260px; padding: 3px 8px; margin-left: -8px;
              border: 1px solid transparent; border-radius: 2px; background: none;
              color: var(--faint); font: inherit; font-size: 12px;
              letter-spacing: .1em; text-transform: uppercase; }
.film__coll:hover { border-color: var(--rule); }
.film__coll:focus { border-color: var(--rule2); background: #000; color: var(--dim); outline: none; }
.film__coll:focus-visible { outline: 2px solid var(--gold); outline-offset: 0; }
.film__coll::placeholder { color: rgba(244,243,241,.18); font-style: italic; text-transform: none; letter-spacing: 0; }
.film__saved { font-size: 12px; color: var(--dim); opacity: 0; transition: opacity 0.25s; }
.film__saved.on { opacity: 1; }
.film__saved.bad { color: var(--gold); opacity: 1; }

.film__acts { display: flex; flex-direction: column; align-items: stretch; gap: 6px; min-width: 118px; }

/* ---- the frame picker ----------------------------------------------------- */

.frame { margin: 0 0 20px; padding: 18px; border-left: 2px solid var(--gold); background: var(--surface); display: grid; gap: 14px; }
/* The panel is tabindex="-1" so focus can be moved into it when it opens from
   the More sheet. Where a browser then paints a focus ring it paints its own
   white one over black; this is the same gold every other ring on the page
   uses. :focus-visible only — a ring on a programmatic focus the user did not
   drive with a key is noise, and this is exactly the case that pseudo-class
   was added to tell apart. */
.frame:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.frame__top { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
.frame__stage { display: grid; grid-template-columns: minmax(0, 1fr) 176px; gap: 18px; align-items: start; }
.frame__stage video { width: 100%; aspect-ratio: 16 / 9; background: #000; display: block; box-shadow: inset 0 0 0 1px var(--rule); }
.frame__side { display: grid; gap: 10px; }
.frame__side img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; background: #000; box-shadow: inset 0 0 0 1px var(--rule); display: block; }
.frame__side .label { font-size: 10px; }
.frame__scrub { display: flex; align-items: center; gap: 12px; }
.frame__scrub input[type="range"] { flex: 1; accent-color: var(--gold); }
.frame__t { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; min-width: 92px; text-align: right; }
.frame__note { margin: 0; font-size: 12px; line-height: 1.55; color: var(--faint); }
.frame__row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }

/* ---- who watched -----------------------------------------------------------
   A MODAL OVER THE STUDIO. It was an inline region under the card until the
   owner asked for a popup, and he was right: four hundred rows unfolding inside
   one card of a two-column grid pushed the rest of the library half a screen
   down and were read at half the page's width.

   The dialog itself is the backdrop; .sheet__box is the surface. Same
   construction as .overlay and .ask below, so there is one way this page does
   modals, not three.

   The list is deliberately not a table and not a set of cards: hairline rows on
   the page's own black, two lines each — who and when on the first, the machine
   facts on the second. The only colour is the gold on the views figure and the
   gold on a failed load, which is the gold every other message here uses. */
.sheet {
  position: fixed;
  inset: 0;
  /* Above the sticky bar (20), BELOW the sign-in overlay (40) and the confirm
     dialog (50). If the session dies while this is open, api() raises the
     sign-in overlay — and an overlay painted underneath this one would be a
     form nobody could reach. */
  z-index: 35;
  display: grid;
  place-items: center;
  padding: 20px;
  background: rgba(0, 0, 0, 0.86);
  backdrop-filter: blur(10px);
  /* Neutralised by the prefers-reduced-motion block further down, which zeroes
     every duration on the page — no second media query needed. */
  animation: sheetIn 0.18s ease-out;
}
@keyframes sheetIn { from { opacity: 0; } to { opacity: 1; } }

/* A fixed head over a scrolling list: with four hundred rows in it, the film
   this is about must not be the first thing to leave the screen. */
.sheet__box {
  width: min(880px, 100%);
  /* svh is the SMALL viewport — the one with the URL bar expanded — which is
     the only safe ceiling for a box that must not be clipped on a phone. The
     plain vh goes first so an engine without svh keeps a ceiling rather than
     dropping the declaration and getting none. */
  max-height: 80vh;
  max-height: 80svh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  border: 1px solid var(--rule);
  border-left: 2px solid var(--gold);
  background: var(--surface);
}
.sheet__head { display: flex; align-items: flex-start; gap: 16px; padding: 20px 22px 14px; border-bottom: 1px solid var(--rule); }
.sheet__id { min-width: 0; }
.sheet__title { margin: 0; font-size: 17px; font-weight: 500; line-height: 1.3; color: var(--ink); overflow-wrap: anywhere; }
.sheet__sum { display: flex; flex-wrap: wrap; align-items: baseline; gap: 5px; margin: 6px 0 0; font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; }
.sheet__sum b { font-weight: 500; color: var(--gold); }
.sheet__sum .dot { color: var(--faint); }
.sheet__head .btn { margin-left: auto; flex: none; }
.sheet__body { overflow-y: auto; overscroll-behavior: contain; padding: 4px 22px 20px; }

/* Shared with the registrations dialog below: one sentence under one list,
   saying whether it is loading, empty, capped or broken. */
.views__note, .regs__note { margin: 12px 0 0; font-size: 12px; line-height: 1.55; color: var(--faint); }
.views__note.bad, .regs__note.bad { color: var(--gold); }
.views__list { margin: 0; padding: 0; list-style: none; }
.views__list:empty { display: none; }
.views__row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; padding: 8px 0; border-top: 1px solid var(--rule); }
/* The head already draws one. Two hairlines four pixels apart read as a mistake. */
.views__row:first-child { border-top: 0; }
.views__who { font-size: 13px; font-weight: 500; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
/* "You" is played down by dropping the weight, not by going grey enough to be
   hard to read — it is still a row somebody has to be able to identify. */
.views__who.dim { font-weight: 400; color: var(--dim); }
.views__when { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* --dim rather than the --faint the rest of the page's metadata uses. This line
   is the only place an address, a browser and a referer are ever read, and
   --faint measures about 2,8:1 on black — fine for a filename you glance past,
   not for a string somebody has to compare character by character. --faint is
   left to .views__note, which is a sentence rather than data. */
.views__facts { grid-column: 1 / 3; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; font-size: 12px; color: var(--dim); }
/* An address is read digit by digit and compared with another one. That is the
   one thing on this page a proportional face is bad at. overflow-wrap because
   an IPv6 address is 39 characters and this dialog is 880px at its widest and
   a phone's width at its narrowest — a broken address is ugly, a dialog that
   scrolls sideways is worse. */
.views__ip { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11px; letter-spacing: 0; overflow-wrap: anywhere; }
.views__plays { font-variant-numeric: tabular-nums; }
.views__ref, .views__ua { overflow-wrap: anywhere; }
/* The address of a viewing whose registration was deleted. Played down with
   italics and not with grey — --dim, the same as the rest of this line, for the
   same reason "You" above keeps its colour and drops its weight. */
.views__gone { font-style: italic; color: var(--dim); }

/* ---- who registered ---------------------------------------------------------
   The viewings list one line taller. A viewing is an event and fits on two
   lines; a registration is a PERSON, so the way to reach them gets a line of
   its own between the name and the machine facts — an email and a phone number
   are the reason this record exists, and burying them among user agents would
   be filing the point under the evidence.

   Deliberately not folded into the .views__ rules above. The two lists agree
   today and will not stay that way: this one has a destructive button on every
   row and that button is the reason to keep them apart. */
.regs__list { margin: 0; padding: 0; list-style: none; }
.regs__list:empty { display: none; }
.regs__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px 12px;
  padding: 10px 0;
  border-top: 1px solid var(--rule);
}
/* The head already draws one. Two hairlines four pixels apart read as a mistake. */
.regs__row:first-child { border-top: 0; }
.regs__who { font-size: 13px; font-weight: 500; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
.regs__when { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* --ink, not --dim. An address someone has to read back over the phone is the
   most important string on the row, and the metadata grey is the wrong weight
   for it. */
.regs__contact {
  grid-column: 1 / 3;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 14px;
  font-size: 12px; color: var(--ink); overflow-wrap: anywhere;
}
/* --dim rather than the --faint the rest of the page's metadata uses, for the
   same reason the viewings list gives: --faint measures about 2,8:1 on black,
   which is fine for a filename you glance past and not for a string somebody
   compares character by character. */
.regs__facts {
  grid-column: 1;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px;
  font-size: 12px; color: var(--dim);
}
.regs__num { font-variant-numeric: tabular-nums; }
.regs__mono {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11px; letter-spacing: 0; overflow-wrap: anywhere;
}
/* A FLEX ROW NOW, because there are two controls on it: the per-person CSV
   export and Delete. Was a single grid cell holding one button, where two
   inline-flex children would have touched. */
.regs__acts {
  grid-column: 2; align-self: center; justify-self: end;
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
}

/* ---- audience ---------------------------------------------------------------
   Wider than the other two sheets because these are small charts that have to
   sit BESIDE each other: a daily shape, a ranking and two splits read as one
   picture of a month, and stacked in a column they are four separate scrolls.

   Every figure here is Danish-formatted through dk(); every label is English.
   The bars are real SVG built with createElementNS, never a string — the
   titles in them are the owner's and a film called `<script>` is a film the
   Studio has to survive. */
.sheet__box--wide { width: min(1120px, 100%); }
.aud__ctl { display: flex; flex-wrap: wrap; align-items: center; margin-left: auto; }
.aud__ctl > * + * { margin-left: 12px; }
.sheet__head .aud__ctl + .btn { margin-left: 12px; }
/* SCOPED, and it has to be. `.sheet__head .btn { margin-left: auto }` is two
   classes and beat the one-class rule above, so "Include my own plays" was
   shoved hard against "90 days". */
.sheet__head .aud__ctl > .btn { margin-left: 12px; }
/* Also scoped. Unqualified, this turned every pressed .btn on the page gold —
   the Reorder toggle in the toolbar — and outweighed .film__feat's own pressed
   colour, which is a one-class rule. */
.aud__ctl .btn[aria-pressed="true"] { border-color: var(--gold); color: var(--gold); }

.aud {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(270px, 100%), 1fr));
  gap: clamp(20px, 3vw, 40px);
  padding: 10px 0 4px;
}
.aud__wide { grid-column: 1 / -1; }
.aud__k {
  margin: 0 0 14px; padding-bottom: 10px; border-bottom: 1px solid var(--rule);
  font-size: 9.5px; font-weight: 500; letter-spacing: .24em; text-transform: uppercase;
  color: var(--dim);
}
.aud__big {
  margin: 0; font-family: var(--serif); font-weight: 300;
  font-size: clamp(30px, 4vw, 44px); letter-spacing: -.02em; color: var(--ink);
  font-variant-numeric: tabular-nums; line-height: 1;
}
.aud__sub { margin: 6px 0 0; font-size: 12px; color: var(--dim); }
.aud__chart { width: 100%; height: auto; display: block; overflow: visible; }
.aud__none { margin: 0; font-size: 13px; line-height: 1.6; color: var(--dim); }

.rank { margin: 0; padding: 0; list-style: none; }
.rank li { display: flex; align-items: center; padding: 9px 0; border-bottom: 1px solid var(--rule); }
.rank li > * + * { margin-left: 12px; }
/* A FIXED SHARE OF THE ROW, not the title's natural width. With `auto` every
   bar sat in a track of a different length — a short title left a long track
   and a long title a stub — so the fills were percentages of five different
   scales and the longest bar belonged to the shortest name. Equal tracks are
   the whole reason to draw bars at all. */
.rank__t {
  font-family: var(--serif); font-weight: 300; font-size: 15px; color: var(--ink);
  min-width: 0; flex: 0 0 46%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* overflow:hidden so a fill that ever exceeds its track is clipped rather than
   painted across the count beside it. Belt to the braces on the max above. */
.rank__bar { flex: 1 1 40px; height: 1px; background: var(--rule); position: relative;
             min-width: 26px; overflow: hidden; }
.rank__bar i { position: absolute; top: 0; left: 0; bottom: 0; background: rgba(244, 243, 241, .55); }
/* The most-watched film is the only gold in the panel. */
.rank li:first-child .rank__bar i { background: var(--gold); }
.rank__n { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; flex: 0 0 auto; }

.split { height: 3px; display: flex; margin-bottom: 14px; border-radius: 2px; overflow: hidden; }
.split i { display: block; height: 100%; }
.legend { margin: 0; padding: 0; list-style: none; }
.legend li {
  display: flex; align-items: baseline; padding: 7px 0;
  border-bottom: 1px solid var(--rule); font-size: 12.5px; color: var(--dim);
}
.legend li > * + * { margin-left: 10px; }
.legend em { font-style: normal; flex: 1 1 auto; color: var(--ink); font-size: 13px;
             min-width: 0; overflow-wrap: anywhere; }
.legend b { font-weight: 400; font-variant-numeric: tabular-nums; color: var(--dim); flex: 0 0 auto; }
.swatch { width: 9px; height: 9px; border-radius: 1px; flex: 0 0 auto; align-self: center; }

/* ---- two controls in one sheet head -----------------------------------------
   `.sheet__head .btn { margin-left: auto }` pins ONE button to the right edge.
   With two of them the free space is SPLIT between them, so one lands in the
   middle and one at the edge — and hiding one does not change which selector
   the other matches, so the layout would move depending on which was on screen.
   These two heads push the title instead: .sheet__id takes the free space and
   the buttons bunch at the right in the head's own gap, which reads identically
   with one button or two. Scoped by id so the four heads that already work are
   not touched. */
#pb .sheet__head .sheet__id,
#act .sheet__head .sheet__id { margin-right: auto; }
#pb .sheet__head .btn,
#act .sheet__head .btn { margin-left: 0; }

/* ---- playback: where people stop ---------------------------------------------
   The library's completion rates and one film's drop-off curve, in one sheet at
   the audience page's width. The chart itself is .aud__chart — the same block,
   because it is the same kind of object and a second one would be a second set
   of label colours to keep in step.

   THE TABLE IS NOT .rank, deliberately. .rank golds its first row, which is
   right there (the most-watched film) and wrong here: this list is ordered by
   how many STARTED and the bars measure how many FINISHED, so the first row is
   not the longest bar and golding it would mark the wrong thing. */
.pbtable { margin: 0; padding: 0; list-style: none; }
.pbrow {
  display: flex; align-items: center; width: 100%;
  padding: 9px 0; border: 0; border-bottom: 1px solid var(--rule);
  background: none; color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.pbrow > * + * { margin-left: 12px; }
.pbrow:hover { background: rgba(244, 243, 241, 0.03); }
.pbrow:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }
/* A FIXED SHARE OF THE ROW, not the title's natural width — the same reason
   .rank__t gives: with `auto` every bar sits in a track of a different length
   and the fills become percentages of five different scales. */
.pbrow__t {
  font-family: var(--serif); font-weight: 300; font-size: 15px; color: var(--ink);
  min-width: 0; flex: 0 0 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pbrow__bar {
  flex: 1 1 40px; min-width: 26px; height: 1px;
  background: var(--rule); position: relative; overflow: hidden;
}
.pbrow__bar i { position: absolute; top: 0; left: 0; bottom: 0; background: rgba(244, 243, 241, 0.55); }
.pbrow__n {
  flex: 0 0 auto; font-size: 12.5px; color: var(--ink);
  font-variant-numeric: tabular-nums; min-width: 42px; text-align: right;
}
.pbrow__s { flex: 0 0 auto; font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }

/* ---- one invited person's activity -------------------------------------------
   One row per film: what it is, when it was last touched, how far the furthest
   sitting got as a bar, and the facts under it. Not .views__row — that row is
   one event, this one is a film with a history. */
.actlist { margin: 0; padding: 0; list-style: none; }
.actfilm {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 6px 12px; padding: 11px 0; border-bottom: 1px solid var(--rule);
}
.actfilm__id { display: flex; flex-wrap: wrap; align-items: baseline; min-width: 0; }
.actfilm__id > * + * { margin-left: 8px; }
.actfilm__t {
  font-family: var(--serif); font-weight: 300; font-size: 15px; color: var(--ink);
  min-width: 0; overflow-wrap: anywhere;
}
.actfilm__when { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* HOW FAR THE FURTHEST SITTING GOT. Drawn only when a duration was reported to
   measure it against — see the comment in actFilmRow(): null is not nought, and
   a zero-width fill would claim nobody watched anything. */
.actfilm__bar {
  grid-column: 1 / 3; height: 2px; background: var(--rule);
  position: relative; overflow: hidden;
}
.actfilm__bar i { position: absolute; top: 0; left: 0; bottom: 0; background: rgba(244, 243, 241, 0.55); }
/* --dim rather than the --faint the rest of the page's metadata uses, for the
   reason the viewings list gives: --faint measures about 2,8:1 on black, which
   is fine for a filename you glance past and not for a figure being compared. */
.actfilm__facts {
  grid-column: 1 / 3; display: flex; flex-wrap: wrap; align-items: baseline;
  font-size: 12px; color: var(--dim);
}
.actfilm__facts > * { margin: 0 12px 0 0; }
/* WHERE THEY PICK UP NEXT, which is a different fact from how far a sitting got
   — so it is the one on the row that is allowed to be read first. */
.actfilm__now { color: var(--ink); }

/* ---- the audit log ------------------------------------------------------------
   The filters live in the body: four of them and a download link do not fit in
   a head at 390px, and each one needs a label rather than a guess. Built on the
   invitation form's own atoms (.invform__f, .label, .field) because it is the
   same object — a row of labelled fields over a list. */
.adtctl { padding: 4px 0 18px; border-bottom: 1px solid var(--rule); margin-bottom: 16px; }
.adtctl__row { display: flex; flex-wrap: wrap; align-items: flex-end; }
.adtctl__row > * { margin: 0 14px 10px 0; }
.adtctl__acts { display: flex; flex-wrap: wrap; align-items: center; }
.adtctl__acts > * { margin: 0 14px 0 0; }
/* WHAT THE DOWNLOAD WILL CONTAIN, beside the button that starts it. Gold only
   when the export's own ceiling would clip the file, which is the one case here
   worth a warning — every other state is a fact. */
.adtctl__hint { margin: 0; flex: 1 1 260px; font-size: 12px; line-height: 1.5; color: var(--dim); }
.adtctl__hint.bad { color: var(--gold); }

/* One sitting. Two lines: what was watched and when, then everything measured
   about it. Deliberately not folded into .views__row — that list is always
   about ONE film and has no column for naming it, and this one is mostly about
   which film it was. */
.adtlist { margin: 0; padding: 0; list-style: none; }
.adtlist:empty { display: none; }
.adtrow {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px 12px; padding: 8px 0; border-top: 1px solid var(--rule);
}
.adtrow:first-child { border-top: 0; }
.adtrow__film { font-size: 13px; font-weight: 500; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
.adtrow__when { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
.adtrow__facts {
  grid-column: 1 / 3; display: flex; flex-wrap: wrap; align-items: baseline;
  font-size: 12px; color: var(--dim);
}
.adtrow__facts > * { margin: 2px 12px 0 0; }
/* HOW THEY GOT IN is the one fact on the row this log exists for, so it is the
   one allowed to be read before the others. */
.adtrow__how { color: var(--ink); }
.adtrow__who { color: var(--ink); overflow-wrap: anywhere; }
/* An address is read digit by digit and compared with another one — the one
   thing on this page a proportional face is bad at. Its own rule rather than
   .views__ip's, the way .regs__mono already has its own: the three lists agree
   today and the one with a destructive button on it will not stay that way. */
.adtrow__mono {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11px; letter-spacing: 0; overflow-wrap: anywhere;
}
/* The address of a sitting whose person was deleted. Italic rather than grey,
   because it is still a fact about the row and has to be readable. */
.adtrow__gone { font-style: italic; color: var(--dim); }
.adtrow__num { font-variant-numeric: tabular-nums; }
.adtrow__ref { overflow-wrap: anywhere; }

/* ---- invitations -------------------------------------------------------------
   The form and the list in one sheet: making an invitation and looking at the
   ones that exist are the same sitting, and a separate "new" surface would be
   a second dialog to open for a job that takes fifteen seconds. */
.invform { padding: 4px 0 20px; border-bottom: 1px solid var(--rule); margin-bottom: 18px; }
.invform__row { display: flex; flex-wrap: wrap; align-items: flex-end; margin-bottom: 14px; }
.invform__row > * { margin: 0 14px 10px 0; }
.invform__f { display: block; flex: 1 1 220px; min-width: 0; }
.invform__f--sm { flex: 0 0 140px; }
.invform__f .label { display: block; margin-bottom: 5px; }
.invform__find { max-width: 320px; margin: 6px 0 10px; }
/* A checklist rather than a multi-select: twenty-six films is too many for a
   native list box and the rating has to be visible while choosing, because
   who an invitation is for decides what may be on it. */
.invpick {
  max-height: 208px; overflow-y: auto; overscroll-behavior: contain;
  border: 1px solid var(--rule); border-radius: 2px; padding: 6px 4px;
}
.invpick__i { display: flex; align-items: center; padding: 5px 8px; cursor: pointer; }
.invpick__i:hover { background: rgba(244, 243, 241, 0.04); }
.invpick__i > * + * { margin-left: 9px; }
.invpick__i input { flex: 0 0 auto; width: 15px; height: 15px; accent-color: var(--gold); }
.invpick__t {
  flex: 1 1 auto; min-width: 0; font-size: 13px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.invpick__none { padding: 10px 8px; font-size: 12px; color: var(--dim); }
.invform__hint { margin: 10px 0 0; font-size: 12px; line-height: 1.5; color: var(--dim); }
.invform__hint.bad { color: var(--gold); }
/* The rule the view limits on every card obey, said once above the list instead
   of repeated beside each of them. It sits under the form's own hairline, so it
   gives back the top margin .invform__hint carries for its place inside the
   form. */
#invRule { margin: 0 0 16px; max-width: 72ch; }
.invform__acts { display: flex; flex-wrap: wrap; align-items: center; margin-top: 14px; }
.invform__acts > * + * { margin-left: 10px; }

.invlist { margin: 0; padding: 0; list-style: none; }
.invlist:empty { display: none; }
.invrow {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px 12px; padding: 12px 0; border-top: 1px solid var(--rule);
}
.invrow:first-child { border-top: 0; }
.invrow__who { font-size: 14px; font-weight: 500; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
.invrow__when { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
.invrow__note { grid-column: 1 / 3; font-size: 12px; color: var(--dim); overflow-wrap: anywhere; }
.invrow__facts {
  grid-column: 1 / 3; display: flex; flex-wrap: wrap; align-items: baseline;
  font-size: 12px; color: var(--dim);
}
.invrow__facts > * { margin: 2px 12px 0 0; }
.invrow__films { grid-column: 1 / 3; display: flex; flex-wrap: wrap; align-items: baseline; }
.invrow__films > * { margin: 4px 8px 0 0; }
.invrow__film {
  font-size: 11.5px; color: var(--dim); padding: 2px 7px;
  border: 1px solid var(--rule); border-radius: 2px;
}
.invrow__acts { grid-column: 1 / 3; display: flex; flex-wrap: wrap; align-items: center; margin-top: 8px; }
.invrow__acts > * { margin: 0 8px 0 0; }
/* HOW MUCH OF THE LIMIT HAS GONE. --ink rather than the --dim of the facts
   around it: it is the figure the field under it is about, and the one number on
   the row that cannot be undone. */
.invrow__used { color: var(--ink); font-variant-numeric: tabular-nums; }

/* ---- the view limit, on the card ----------------------------------------------
   A labelled field on its own line rather than another pill among the actions:
   it takes a value instead of performing an action, and the two must not look
   alike on a row that also carries Revoke and Delete. */
.invrow__limit {
  grid-column: 1 / 3;
  display: flex; flex-wrap: wrap; align-items: center; margin-top: 8px;
}
.invrow__limit > * { margin: 0 10px 0 0; }
.invrow__limitk { font-size: 11px; font-weight: 500; letter-spacing: 0.14em;
                  text-transform: uppercase; color: var(--dim); flex: 0 0 auto; }
/* Narrow on purpose: the useful values are one, two and three digits, and a
   full-width field for a number that small reads as a text box for a sentence.
   The 16px floor and the 44px target under (pointer: coarse) come from .field. */
.invrow__limitin { width: 108px; flex: 0 0 auto; }
.invrow__limithint { flex: 1 1 200px; font-size: 12px; line-height: 1.5; color: var(--faint); }
.invrow__limithint.busy { color: var(--dim); }
.invrow__limithint.bad { color: var(--gold); }
.invrow__limithint:empty { display: none; }

/* ---- withdraw every invitation ------------------------------------------------
   At the BOTTOM, behind its own hairline and its own sentence, and labelled with
   the number it is about to act on. Nothing in this service un-revokes an
   invitation, so this is the only control in the Studio whose effect reaches
   people who will never be told why their link stopped working — it is meant to
   take a moment to arrive at. */
.invwipe { margin-top: 26px; padding-top: 18px; border-top: 1px solid var(--rule); }
.invwipe__p { margin: 0 0 12px; font-size: 12px; line-height: 1.55; color: var(--dim); max-width: 62ch; }

/* The one word that says what this invitation is. Gold only for the state
   that still works — expired and revoked are facts, not warnings. */
.pill {
  font-size: 10px; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase;
  padding: 2px 7px; border: 1px solid var(--rule); border-radius: 2px; color: var(--dim);
  white-space: nowrap;
}
.pill--active { color: var(--gold); border-color: rgba(201, 161, 74, 0.45); }
.pill--revoked { color: var(--danger); border-color: rgba(224, 102, 106, 0.4); }
/* SPENT IS NOT A FAULT. The link did exactly what it was told to do, and the
   owner can give it more viewings with one number — so it is marked as notable
   (full ink, a visible edge) and not as a warning. Gold is taken: it means "this
   one still works", which a spent invitation does not.
   `expired` has no rule of its own and keeps the base grey, which is the
   treatment the comment above asks for — written down here so the next edit does
   not read the absence as an omission. */
.pill--spent { color: var(--ink); border-color: var(--rule2); }

/* ---- resumable strip ------------------------------------------------------ */

.resume { margin: 26px 0 0; padding: 16px 18px; border-left: 2px solid var(--gold); background: var(--surface); }
.resume h2 { margin: 0 0 4px; font-size: 11px; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; color: var(--dim); }
.resume p { margin: 0 0 12px; font-size: 13px; line-height: 1.55; color: var(--faint); }
.resume ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 10px; }
.resume li { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; font-size: 13px; color: var(--dim); }
.resume b { font-weight: 500; color: var(--ink); word-break: break-word; }
.resume .pct { font-variant-numeric: tabular-nums; color: var(--faint); }

/* ---- empty + messages ----------------------------------------------------- */

.empty { padding: 64px 0; font-family: var(--serif); font-style: italic; font-weight: 300; font-size: clamp(17px, 2.2vw, 22px); color: var(--dim); }
.note { margin: 16px 0 0; font-size: 13px; line-height: 1.6; color: var(--gold); }
/* Collapsed rather than held open at min-height: this line is empty almost all
   of the time, and a permanent reserved row is a permanent gap. */
.note:empty { display: none; }

/* ---- sign-in overlay ------------------------------------------------------ */

/* place-items:start center, NOT center, with the box centring itself through
   auto margins. Measured at 844x390 — a phone held sideways — the form is
   432px tall in a 390px window, and a centred grid item that does not fit
   overflows in BOTH directions: the Sign in button was off the bottom and
   scrolling could not reach it, because the top had gone off the screen too.
   Auto margins resolve to zero when there is no free space, so the box sits at
   the top and the overlay scrolls to the button; with room to spare they
   centre it exactly as place-items:center did. */
.overlay { position: fixed; inset: 0; z-index: 40; display: grid; place-items: start center; overflow-y: auto; padding: 20px; background: rgba(0, 0, 0, 0.9); backdrop-filter: blur(12px); }
.overlay__box { width: min(380px, 100%); margin-top: auto; margin-bottom: auto; padding: 26px; border: 1px solid var(--rule); border-left: 2px solid var(--gold); background: var(--surface); }
.overlay__box h2 { margin: 0 0 6px; font-size: 11px; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; color: var(--dim); }
.overlay__box p { margin: 0 0 18px; font-size: 13px; line-height: 1.55; color: var(--faint); }

/* ---- confirm dialog ------------------------------------------------------- */
/* Replaces window.confirm for deletion. Not decoration: the native dialog cannot
   say WHICH film, cannot say that it is published and reachable right now, and
   cannot be styled to look like it belongs to this site. */

.ask { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 20px; background: rgba(0, 0, 0, 0.86); backdrop-filter: blur(10px); }
.ask__box { width: min(440px, 100%); padding: 24px; border: 1px solid var(--rule); border-left: 2px solid var(--danger); background: var(--surface); }
.ask__box h2 { margin: 0 0 10px; font-size: 16px; font-weight: 500; }
.ask__box p { margin: 0 0 20px; font-size: 14px; line-height: 1.6; color: var(--dim); }
.ask__box p b { color: var(--ink); font-weight: 500; }
.ask__acts { display: flex; gap: 10px; justify-content: flex-end; }

/* ---- wide: the library in two columns --------------------------------------
   Above 1100px the films are laid out two abreast, so twice as many are on
   screen at once. This is a working tool with sixty films in it and one column
   spent half the page on nothing.

   THE CARD PAYS FOR IT by giving up a little of its density: a smaller still,
   tighter gutters, and the buttons on their own line under the body rather than
   in a right-hand column. At half width that column would have squeezed the
   title field down to about a hundred pixels. It is the same rearrangement the
   narrow layout below already makes, for the same reason, and the DOM is
   identical in all three — only the grid changes.

   Reading order is row-major, left to right, which is exactly the order the ids
   are written back in. Dropping a film onto another still puts it in front of
   THAT film whichever column it is in, because the drop target is the element
   under the pointer and never a guess from a vertical offset. */
@media (min-width: 1100px) {
  /* Wider, but with a ceiling. Two columns unconstrained on a 2560px screen
     would be two 1.200px cards with a title field the width of a paragraph. */
  main { max-width: 1560px; }

  .films {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: clamp(28px, 3.4vw, 52px);
  }

  .film__row { grid-template-columns: auto 132px minmax(0, 1fr); gap: 10px 14px; }
  .film__thumb { width: 132px; }
  .film__acts { grid-column: 2 / 4; flex-direction: row; flex-wrap: wrap; align-items: center; min-width: 0; gap: 6px 8px; }
  /* In the right-hand column Delete was the last thing on its own line. On one
     line it would land against the Live segment of the visibility control, and
     those two are a bad pair of neighbours: one publishes a film, the other
     destroys it. Pushed to the far end instead. */
  .film__acts .btn--danger { margin-left: auto; }
  .film__coll { max-width: 220px; }

  /* Both panels live inside a card, so both have to survive at half width. */
  .frame__stage { grid-template-columns: minmax(0, 1fr) 132px; }
}

/* ---- narrow --------------------------------------------------------------- */

/* THE BREAKPOINTS THIS FILE USES, listed once because they cannot be tokens: a
   custom property is illegal inside a media-query condition and there is no
   preprocessor here, so the only way to keep them honest is to write them down.

     (min-width: 1100px)   the library two abreast
     (max-width:  900px)   one column of cards, the actions under the body.
                           Widened from 760: an iPad Pro 11" in portrait is
                           834px and fell between the two layouts, taking the
                           four-column desktop row in 750px of content
     (max-width:  560px)   a phone — the bar wraps, the card is two columns,
                           and everything but the two segmented controls moves
                           into the More sheet
     (hover: none)         nothing to drag FROM: the drop zone asks for a tap,
                           and the folder picker and the reorder grip say out
                           loud that they need a desktop browser
     (pointer: coarse)     a finger: 44px targets and the 16px input floor

   Width answers "how much room is there"; hover and pointer answer "what is
   doing the pointing". They are deliberately not mixed — a 13" laptop with a
   touchscreen needs 44px targets at 1440px wide, and a 400px desktop window
   needs the card collapsed without pretending it is a phone. */
@media (max-width: 900px) {
  .film__row { grid-template-columns: auto 116px minmax(0, 1fr); gap: 12px; }
  .film__thumb { width: 116px; }
  .film__acts { grid-column: 2 / 4; flex-direction: row; flex-wrap: wrap; min-width: 0; }
  .frame__stage { grid-template-columns: minmax(0, 1fr); }
  .job { grid-template-columns: 64px 1fr; }
  .job__still { width: 64px; }
  .job__state { grid-column: 2; text-align: left; }
  .job__meta, .job__track, .job__acts { grid-column: 1 / 3; }
  .job__acts { justify-self: start; }
}

/* ---- the phone and the tablet ----------------------------------------------
   Both Studios are documented desktop-only and that is still what they are
   best at. What follows is the part that has to work anyway: the owner reads
   this page on a phone, and until now a film card was a five-row pile of pills
   with Delete beside Live, every field zoomed the page on focus, and the bar
   ran 700px of controls through 390px of screen.

   NOTE FOR WHOEVER COPIES FROM HERE: these rules use flex `gap` and `inset`
   like the rest of this file, which is safe because the Studios are never
   served to the wall television's Chrome 79. The two public stylesheets must
   not inherit that habit — see the note at the top of
   films/upload/photos/photos.css. Keep these rules in the Studios.
   ------------------------------------------------------------------------- */

/* ---- the two strings a drop zone needs ----------------------------------- */
/* A phone has no file manager to drag FROM, so the zone asks for a tap
   instead. Both strings are in the markup and CSS picks one — no JS, nothing
   to get out of step, and the dashed border stays either way because a dashed
   box still reads as "an intake" when the thing behind it is a file picker. */
.drop__tap { display: none; }

/* A CAVEAT, NOT A VERDICT — and the folder button stays.
   This hid #pickFolder under (hover:none) on the grounds that webkitdirectory
   does nothing in iOS Safari. But (hover:none) is also Chrome on Android,
   where picking a folder works: a working control vanished behind a sentence
   that named the wrong reason, which is the opposite of this page's rule that
   an unavailable feature must say so accurately.

   There is no honest capability test to gate it on either. `"webkitdirectory"
   in input` reports the IDL attribute, not whether the platform's picker can
   return a directory, and it is identically true in Chromium on a desktop and
   in Chromium pretending to be a Pixel (measured). It would answer yes on iOS
   and tell us nothing. A UA test is the only thing that could separate them,
   and the house rule is that UA decides size and never behaviour.

   So the button is shown everywhere and the line says what is actually known.
   The worst case is a control that may not do what it offers on one platform,
   next to a sentence that warned about it — which beats a control that is
   silently gone on a platform where it works.

   --dim rather than the --faint of .drop__hint beside it: this line is the one
   in the zone that has to be read rather than glanced past. */
.drop__folderline { display: none; margin: 8px 0 0; font-size: 12px; line-height: 1.5; color: var(--dim); }

/* The same contract for the two reordering gestures. Each renders a line in
   place of the control it replaces, so nothing is ever just missing. */
.tools__note,
.igrid__note { display: none; margin: 10px 0 0; font-size: 12px; line-height: 1.5; color: var(--dim); }

/* ---- More ------------------------------------------------------------------
   The card's own secondary controls, relocated into a sheet for as long as it
   is open and put back exactly where they came from when it closes. Relocated
   and not rebuilt: a second copy of Share, Describe, Featured and Delete would
   be a second set of handlers, a second set of labels and a second set of
   disabled states to keep true.

   Built on .sheet / .sheet__box / .sheet__head / .sheet__body above, because
   that is how this page already does modals four times over. */
.film__more { display: none; }

.more__list { margin: 0; padding: 0; list-style: none; }
/* One decision per row, with a hairline between them — the same construction
   as .views__row and .regs__row, for the same reason: this is a list of
   things, not a panel of cards. */
.more__row { padding: 14px 0; border-top: 1px solid var(--rule); }
.more__row:first-child { border-top: 0; }
.more__k { margin: 0 0 4px; font-size: 11px; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; color: var(--dim); }
/* WHAT THE title ATTRIBUTE USED TO SAY. Every explanation on a film card is a
   tooltip, and a tooltip is unreachable with a finger — so the sheet prints
   the button's own title under its label rather than inventing a second
   wording that could drift from it. */
.more__d { margin: 0 0 10px; font-size: 12px; line-height: 1.5; color: var(--dim); }
.more__ctl { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
/* The collection field is drawn to sit flush with the title above it on the
   card. In here it is a labelled field in a dialog, so it gives back the
   negative margin and takes the width it is given. --dim rather than --faint
   for the same reason: on the card it is a whisper under the title, here it is
   the only thing in its row. */
.more__ctl .film__coll { margin-left: 0; max-width: none; color: var(--dim); border-color: var(--rule); }
.more__ctl .film__prem { flex: 1 1 100%; }
/* The row's own heading already says "Premiere", and the field's label says
   "Premieres" two pixels under it. Hidden from the eye and kept for the screen
   reader, because it is the input's <label for> and the only name it has —
   the same clip the .sr class above uses, scoped rather than reused because a
   class cannot be put on a node this file does not create. */
.more__ctl .film__premk {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
/* Delete is deliberately borderless on the card, where it sits among six other
   pills and must not compete with them. In here it is alone in its row with a
   heading over it, and a word with no box around it is not something a finger
   can aim at. It gets .btn's own box back — not its colour: the confirm dialog
   is what says this one is different. */
.more__ctl .btn--quiet { border-color: var(--rule2); padding-left: 16px; padding-right: 16px; }

/* ---- a phone ---------------------------------------------------------------
   KEYED ON WIDTH, not on touch: a 390px desktop window has exactly the same
   problem, and the fix is the same fix. */
@media (max-width: 560px) {
  /* The bar wraps and its actions scroll sideways — the pattern the film wall's
     filter rail already uses. Five controls, a back link and a wordmark do not
     fit across 390px in any arrangement, and the wordmark is the one of the
     seven the page itself already says. */
  .bar { flex-wrap: wrap; gap: 10px; padding: 14px var(--pad); }
  .bar__mark { display: none; }
  .bar__acts {
    flex: 1 1 100%;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 18px;
    padding-bottom: 2px;
    scrollbar-width: thin;
    scrollbar-color: rgba(244, 243, 241, 0.16) transparent;
  }
  .bar__acts::-webkit-scrollbar { height: 4px; }
  .bar__acts::-webkit-scrollbar-thumb { background: rgba(244, 243, 241, 0.16); }
  .link { min-height: 44px; }

  /* Two columns: the still and everything else. The grip column goes entirely
     — at this width there is no room for a drag gesture and the note under the
     toolbar says where reordering lives instead. (The comment on .film__grip
     above argues against display:none because a button appearing under the
     pointer reflows the list; that argument is about a pointer, and this is
     the branch where there is not one.) */
  .film__row { grid-template-columns: 96px minmax(0, 1fr); gap: 10px 12px; }
  .film__thumb { width: 96px; }
  .film__grip { display: none; }

  /* The fields that are a whisper on a wide card are a cramped line on a
     narrow one. Both move into the More sheet, where they get a label.
     Scoped to the body on purpose: once relocated they are children of
     .more__ctl and must come back into view. */
  .film__body > .film__coll,
  .film__body > .film__prem { display: none; }

  .film__acts { grid-column: 1 / 3; flex-direction: column; align-items: stretch; gap: 8px; }
  /* THE TWO THINGS THE CARD KEEPS. Private/Gated/Live and All/16+/18+ are the
     questions this page exists to answer, so they get the full width and a
     real target each instead of a 70px pill. */
  .film__vis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border-radius: 2px; }
  .vis { min-height: 44px; font-size: 12px; padding: 0 6px; }
  /* Everything else is behind More. Not hidden-and-forgotten: the JS moves
     these exact nodes into the sheet, so the sheet shows the real labels and
     the real disabled states. */
  .film__acts > .film__xtra { display: none; }
  .film__more { display: inline-flex; }
  .film__acts > .btn { min-height: 44px; justify-content: center; }
  .more__ctl > .btn { min-height: 44px; }

  /* Reordering is unavailable here whatever is doing the pointing, because the
     grip's column is gone — so the toggle goes and the note arrives. */
  #reorder { display: none; }
  .tools__note { display: block; }

  /* THE TWO HEADS THAT CARRY A SECOND CONTROL. At 390px a title, "All films"
     and "Close" do not fit across one line, so the title takes a line of its
     own and the buttons sit under it. */
  #pb .sheet__head,
  #act .sheet__head { flex-wrap: wrap; }
  #pb .sheet__head .sheet__id,
  #act .sheet__head .sheet__id { flex: 1 1 100%; margin-right: 0; }

  /* A playback row is a title, a bar, a percentage and a count. The bar is the
     first thing to give up its width — below about 420px it is a stub measuring
     nothing — so the row becomes two lines and the bar spans the lower one. */
  .pbrow { flex-wrap: wrap; }
  .pbrow__t { flex: 1 1 100%; white-space: normal; }
  .pbrow > * + * { margin-left: 0; }
  .pbrow__bar { flex: 1 1 60px; order: 3; margin-left: 0; }
  .pbrow__n { order: 1; text-align: left; min-width: 0; }
  .pbrow__s { order: 2; margin-left: 12px; }

  /* The audit filters go one per line: four selects side by side at this width
     are four 80px boxes with eight-character labels over them. */
  .adtctl__row { display: block; }
  .adtctl__row > * { margin: 0 0 12px; }
  .adtctl__row .invform__f--sm { flex: none; }

  /* The view limit on an invitation card: the label, the field and the hint
     each take their own line rather than being squeezed into a third of 390px. */
  .invrow__limitin { width: 100%; flex: 1 1 100%; }
  .invrow__limithint { flex: 1 1 100%; margin-top: 6px; }
}

/* ---- nothing to drag from --------------------------------------------------
   KEYED ON CAPABILITY, not on width: an iPad Pro in landscape is 1024px wide
   and still has no file manager to drag out of and no native drag-and-drop at
   all, while a 13" laptop with a touchscreen reports hover:hover and keeps
   every desktop affordance it has earned. */
@media (hover: none) {
  .drop__drag { display: none; }
  .drop__tap { display: inline; }
  .drop { padding: 30px 20px; }
  .drop .btn { min-height: 44px; }
  /* #pickFolder is NOT hidden here — see the note on .drop__folderline above.
     Only the caveat arrives, and only on touch, because on a desktop browser
     there is nothing to warn about. */
  .drop__folderline { display: block; }

  /* HTML5 drag-and-drop does not fire on a touchscreen in any mobile browser,
     so the grip and its toggle are not "small here" — they do not work here.
     Said in a line rather than left as a control that answers nothing. */
  .film__grip,
  #reorder { display: none; }
  .tools__note { display: block; }
}

/* ---- a finger --------------------------------------------------------------
   THE 16px INPUT FLOOR IS THE WHOLE REASON THIS BLOCK EXISTS. iOS Safari zooms
   the page whenever a focused field's COMPUTED size is under 16px and it never
   zooms back out, so a phone tap on the search box left the Studio magnified
   and sideways. Keyed on pointer:coarse and not on a width: an iPad at 1024px
   is still iOS Safari and still zooms.

   .film__title is deliberately absent — it is already 17px, and listing it
   here would quietly make it smaller. */
@media (pointer: coarse) {
  .field,
  .film__desc,
  .film__coll,
  .film__premin { font-size: 16px; }
  /* .1em of tracking is drawn for 12px uppercase. At 16px the same ratio opens
     the word into separate letters. */
  .film__coll { letter-spacing: 0.06em; }

  /* 44px of target on everything that can be pressed. Grown rather than
     redrawn: the padding and the type stay as they are and the box gets a
     floor, so nothing about the design changes on a desktop. */
  .btn,
  .link,
  .tab,
  .vis { min-height: 44px; }
  .film__noviews { min-height: 44px; }
  /* A field is a target too. These are wide, so the miss that actually happens
     is vertical — a 28px collection field between a title and a date is three
     chances to land on the wrong one. .film__desc keeps growing past this:
     autoGrow() sets an inline height and min-height only ever raises it. */
  .field,
  .film__title,
  .film__desc,
  .film__coll,
  .film__premin { min-height: 44px; }

  /* A 15px checkbox is a third of a target. The row is a <label>, so growing
     the row grows the thing a finger can actually hit — and the list grows
     with it, because four and a half rows of a six-film invitation is a
     scroll nobody notices is there. */
  .invpick__i { min-height: 44px; padding: 8px; }
  .invpick__i input { width: 22px; height: 22px; }
  .invpick { max-height: 264px; }

  /* The frame picker's scrubber. A 2px track with a native thumb is the
     hardest thing on the page to catch with a thumb of your own. */
  input[type="range"] { height: 44px; }

  /* A playback row IS a button — the whole row opens that film's curve — so it
     takes the same floor every other control on the page gets. The hairline it
     is separated by stays where it is; only the box grows. */
  .pbrow { min-height: 44px; }
}

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

/* ---- the gated bar ---------------------------------------------------------
   Above the library, and only when something is behind a portal grant. It says
   the one thing that is easy to get wrong: a session outlives the grant, so
   removing someone in the portal does not end the visit they are already
   having — the per-person Sign out here does. */
.gatedbar { margin: 30px 0 0; padding: 14px 16px;
            border-left: 2px solid #b98fd6; background: var(--surface); }
.gatedbar__n { margin: 0 0 10px; font-size: 13px; color: var(--dim); }
.gatedbar__none { margin: 0; font-size: 12px; color: var(--faint); }
.viewers { margin: 0; padding: 0; list-style: none; display: grid; gap: 8px; }
.viewer { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 14px; font-size: 13px; }
.viewer__name { font-weight: 500; color: var(--ink); }
.viewer__when { font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
.viewer__acts { margin-left: auto; }
