/* VMKer web UI — plain and confident. Generous whitespace, one accent
   colour, images doing the talking. A working tool for one operator, not a
   marketing page. Palette follows prefers-color-scheme; no toggle needed. */

:root {
  --bg: #ffffff;
  --surface: #f4f4f5;
  --border: #e2e2e5;
  --text: #17181c;
  --muted: #6b6f76;
  --accent: #3457d5;
  --accent-contrast: #ffffff;
  --danger: #b3261e;
  /* A LIMIT is neither the accent (which means "go") nor --danger (which
     means "this failed"). It means "this works, and here is where it stops". */
  --warn: #a16207;
  --radius: 10px;
  /* The recessed field a picture sits on, so a 16:9 banner reads as wide
     and a 9:16 story reads as tall without anyone measuring. */
  --stage-field: #e8e8ec;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #121214;
    --surface: #1b1c1f;
    --border: #2c2d31;
    --text: #ececee;
    --muted: #9a9ca3;
    --accent: #6f8dff;
    --accent-contrast: #0b0c10;
    --danger: #ff6b61;
    --warn: #e3b341;
    --stage-field: #0d0e11;
  }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 1.5;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
}

.site-header .brand {
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--text);
  letter-spacing: -0.01em;
}

.site-nav {
  display: flex;
  gap: 1.25rem;
  align-items: center;
  flex-wrap: wrap;
}

.site-nav a {
  color: var(--muted);
  font-weight: 500;
}

.site-nav a.active,
.site-nav a:hover {
  color: var(--text);
}

main {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 4rem;
}

.page-title {
  font-size: 1.4rem;
  margin: 0 0 1rem;
}

.filters {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-bottom: 1.25rem;
}

.filters select,
.filters input,
form.login-form input {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 0.5rem 0.75rem;
  font-size: 0.95rem;
}

button, .btn {
  background: var(--accent);
  color: var(--accent-contrast);
  border: none;
  border-radius: var(--radius);
  padding: 0.55rem 1rem;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
}

button:hover, .btn:hover { opacity: 0.9; text-decoration: none; }

/* A submit that is working. **The box does not change** -- only the ink and the
   cursor -- because a control that resizes when clicked is Phase 12's clipped
   Delete button with a different trigger, and that fix made its own defect nine
   times worse while the CSS test passed through both states. What it is doing
   is said in `.form-busy` beside it, which is free to be any length. */
button.is-busy, .btn.is-busy {
  opacity: 0.72;
  cursor: progress;
}

.form-busy {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.55rem 0 0;
  font-size: 0.82rem;
  color: var(--muted);
}

.form-busy::before {
  content: "";
  width: 0.75rem;
  height: 0.75rem;
  flex: 0 0 auto;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: vmker-busy-spin 0.7s linear infinite;
}

@keyframes vmker-busy-spin { to { transform: rotate(360deg); } }

/* A spinner is decoration; motion sickness is not. The text still says it. */
@media (prefers-reduced-motion: reduce) {
  .form-busy::before { animation: none; opacity: 0.6; }
}

button.secondary, .btn.secondary {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border);
}

/* Width-responsive grid: no fixed column count, so a phone gets one or two
   columns and a desktop gets many, purely from available width. */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1rem;
}

.output-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* A deliverable is shown as its own shape.

   This rule used to be `aspect-ratio: 1 / 1; object-fit: cover`, which
   displayed every output as a square with its content cropped away - fifteen
   of the seventy-five real outputs, and for a 16:9 brochure banner the cropped
   part is exactly the clear space the brief asked to be left for headline
   copy. Someone approving a render was approving a picture that was not the
   deliverable.

   `object-fit: contain` in the same square box would have stopped the cropping
   and left every banner floating in dead space, which is the bug with its
   symptom removed rather than a design. Instead the card gets a stage: a fixed
   4:3 frame - neither of the two shapes in the data, so it favours neither -
   with the picture at its own aspect ratio inside it. The rows keep their
   uniform height and the grid stays aligned; the picture stops being the thing
   forced to be uniform. The letterbox is drawn on purpose rather than hidden,
   because it is what makes the shape legible at a glance. */
.output-card .stage {
  position: relative;
  display: block;
  aspect-ratio: 4 / 3;
  background: var(--stage-field);
  border-bottom: 1px solid var(--border);
  overflow: hidden;
}

/* The image fills the stage and `contain` letterboxes it from the picture's
   own intrinsic ratio, so the frame and the file cannot disagree.

   The first version of this rule sized the image from its intrinsic dimensions
   with `max-width/max-height: 100%` and an aspect-ratio supplied inline per
   row. It passed every test and still clipped: a percentage max-height on a
   grid item does not resolve against a grid area whose height comes from
   `aspect-ratio`, so a 9:16 story rendered 260x462 inside a 207px stage and
   `overflow: hidden` cropped 255px off it - the square crop, moved to a
   different shape, and invisible to the suite. Measured in a browser, not
   inferred. Filling the stage removes the disagreement entirely: there is one
   box, and the picture is fitted inside it.

   Filling it is done with `position: absolute; inset`, not `width/height:
   100%`. Measured in Chrome: the percentage form did not resolve either, and
   for the same underlying reason - a percentage height does not resolve
   against a containing block whose own height comes from `aspect-ratio`, with
   or without `place-items: center`. An absolutely positioned box resolves its
   insets against the padding box of its containing block, which is definite,
   so this form is the one that actually constrains both axes. `inset` also
   draws the letterbox margin, which is why the stage no longer has padding. */
.output-card .stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0.5rem;
  object-fit: contain;
  display: block;
}

/* A <video> in this stage meets both of the traps the image rule fell into.
   A percentage height does not resolve against a containing block whose height
   comes from `aspect-ratio`, and `width: auto` on an absolutely positioned
   replaced element takes its **intrinsic** size -- so a 768x768 clip would lay
   itself out at 768px inside a stage a third of that, and `overflow: hidden`
   would hide the evidence. Both axes are stated, and the box is absolutely
   positioned so its insets resolve against a definite padding box.

   No padding, unlike the image rule: `object-fit: contain` already letterboxes
   the frame, and the browser's own control bar is laid out inside the element
   -- padding would push it off the bottom edge. The field is black because a
   letterboxed video against a light grey reads as a rendering fault. */
.output-card .stage video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  background: #000;
}

.output-card .meta {
  padding: 0.65rem 0.75rem;
  font-size: 0.82rem;
  color: var(--muted);
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.output-card .meta-name {
  color: var(--text);
  font-weight: 600;
  overflow-wrap: anywhere;
}

.output-card .meta-facts {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
}

/* A format identifier and a pixel count are machine facts, so they are set in
   the machine face rather than pretending to be prose. */
/* The finished film, given precedence over the clips it was made from.

   A task's outputs are all real and all worth keeping -- someone checking
   which of three clips went wrong needs every one of them -- so the film is
   emphasised rather than the clips being hidden. An accent border and one
   line of type is the whole treatment: the page already says "clip" and
   "video" on each card, and the problem was never that it lied, only that it
   made the deliverable take reading to find. */
.output-card--film {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.output-card--film .film-flag {
  margin: 0;
  padding: 0.3rem 0.6rem;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

.output-card .shape-badge {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
  line-height: 1.5;
  padding: 0.05rem 0.34rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  background: var(--bg);
}

.output-card .actions {
  display: flex;
  gap: 0.5rem;
  padding: 0 0.75rem 0.75rem;
}

.output-card .actions .btn {
  flex: 1;
  text-align: center;
  padding: 0.4rem 0.5rem;
  font-size: 0.85rem;
}

/* A <form> would otherwise introduce its own box between .actions (the flex
   container) and the button — `display: contents` removes that box so the
   button lays out as a direct flex item, sized the same as the download
   link next to it. */
.inline-form {
  display: contents;
}

.empty-state {
  color: var(--muted);
  padding: 3rem 1rem;
  text-align: center;
}

.login-wrap {
  max-width: 360px;
  margin: 4rem auto;
  padding: 0 1rem;
}

form.login-form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.error-banner {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border: 1px solid var(--danger);
  color: var(--danger);
  border-radius: var(--radius);
  padding: 0.6rem 0.85rem;
  font-size: 0.9rem;
}

.concept-text {
  color: var(--muted);
  max-width: 60ch;
}

.job-list {
  list-style: none;
  margin: 0 0 1.25rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.job-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.9rem;
}

.job-kind { font-weight: 600; text-transform: capitalize; }

.badge {
  display: inline-block;
  padding: 0.1rem 0.55rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  background: var(--border);
  color: var(--text);
}

.badge-succeeded { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.badge-failed, .badge-rejected { background: color-mix(in srgb, var(--danger) 16%, transparent); color: var(--danger); }

.job-error { color: var(--danger); font-size: 0.85rem; }

.approval-actions {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 1rem;
}

.approval-form {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.ops-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1rem;
}

.ops-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
}

.ops-card h2 {
  margin: 0 0 0.6rem;
  font-size: 1rem;
}

.ops-card p {
  margin: 0.3rem 0;
  color: var(--muted);
}

.ops-figure {
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--text) !important;
}


/* ---- References -------------------------------------------------------- */
/* A referent is a product cutout, so it is shown `contain` rather than
   `cover`: cropping the object is exactly the information a person needs to
   judge whether they uploaded the right thing. */

.page-lede {
  color: var(--muted);
  max-width: 60ch;
  margin: -0.25rem 0 1.5rem;
}

.notice-banner {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid var(--accent);
  color: var(--accent);
  border-radius: var(--radius);
  padding: 0.6rem 0.85rem;
  font-size: 0.9rem;
}

form.reference-form,
form.source-form {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  max-width: 460px;
  padding: 1rem 1.15rem 1.25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 2.5rem;
}

form.reference-form label,
form.source-form label {
  font-weight: 600;
  font-size: 0.9rem;
  margin-top: 0.5rem;
}

form.reference-form input[type="file"],
form.source-form input[type="file"] {
  font-size: 0.9rem;
  padding: 0.4rem 0;
}

form.reference-form button,
form.source-form button {
  margin-top: 1rem;
}

.reference-group {
  margin-bottom: 2.25rem;
}

.group-title {
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 700;
  margin: 0 0 0.75rem;
}

.reference-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 1rem;
}

.reference-card {
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.reference-card img {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: contain;
  display: block;
  background: var(--bg);
  padding: 0.5rem;
}

.reference-card figcaption {
  padding: 0.6rem 0.75rem 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  border-top: 1px solid var(--border);
}

.ref-label {
  font-weight: 600;
  font-size: 0.92rem;
  overflow-wrap: anywhere;
}

.ref-meta {
  font-size: 0.78rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* .inline-form is display:contents, so this button is a direct flex child of
   the card and spans its width without a wrapper box in between. */
.reference-card .btn {
  margin: 0 0.75rem 0.75rem;
  text-align: center;
  padding: 0.35rem 0.5rem;
  font-size: 0.82rem;
}


.empty {
  color: var(--muted);
  margin-bottom: 2rem;
}

/* A fourth nav item (References) tipped the header onto two lines at phone
   width, and flex-wrap put the brand *below* the links. Stack deliberately
   instead of wrapping by accident. Found by looking at 390px, which is the
   only way this kind of thing is ever found. */
@media (max-width: 560px) {
  .site-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.65rem;
  }

  .site-nav {
    width: 100%;
    gap: 1rem;
  }
}


/* The render plan on a task page.

   A definition list rather than a table: it is a set of labelled facts, not a
   grid of comparable rows, and at 390px a table would either scroll sideways
   or crush the values. The grid collapses to stacked label-over-value below
   640px, which is where the label column stops earning its width. */
.plan-panel {
  margin: 1.5rem 0;
  padding: 1rem 1.15rem 1.15rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.plan-title {
  margin: 0;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.plan-note {
  margin: 0.4rem 0 0.9rem;
  font-size: 0.85rem;
  color: var(--muted);
  max-width: 60ch;
}

.plan-grid { margin: 0; }

.plan-row {
  display: grid;
  grid-template-columns: 8.5rem 1fr;
  gap: 0.75rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--border);
}

.plan-row dt {
  font-size: 0.85rem;
  color: var(--muted);
}

.plan-row dd {
  margin: 0;
  /* Plan values are model output and can be long; wrapping beats overflow. */
  overflow-wrap: anywhere;
}

.plan-row code {
  font-size: 0.9em;
  padding: 0.05rem 0.3rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
}

@media (max-width: 640px) {
  .plan-row {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }
}


/* The conditioning matrix (Phase 7). A wide grid must scroll inside itself
   rather than making the whole page scroll sideways. */
.matrix-scroll { overflow-x: auto; }
.matrix { border-collapse: collapse; width: 100%; min-width: 32rem; }
.matrix th, .matrix td { border: 1px solid rgba(128, 128, 128, .28); padding: .4rem;
  vertical-align: top; }
.matrix thead th, .matrix tbody th { font-size: .82rem; opacity: .8; white-space: nowrap; }
.matrix-cell { margin: 0; }
.matrix-cell img { width: 100%; height: auto; display: block; border-radius: 3px; }
.matrix-cell figcaption { font-size: .74rem; opacity: .7; padding-top: .25rem; }
.matrix-gap { font-size: .74rem; opacity: .45; padding: 1.5rem 0; text-align: center; }
.matrix-baseline-cell { max-width: 22rem; margin-top: .5rem; }

/* A plan that asked for a referent and did not get one (Phase 7). */
.warn { color: #b4540a; }

/* ------------------------------------------------------------------ /new
   The docket.

   A client brief arrives as one line of WhatsApp and leaves as a set of
   deliverables. So the brief is given the weight it actually has - a writing
   surface, not a four-row textarea - and everything under it is framed as a
   consequence with a price, which is what the previous version of this page
   said nothing about.

   Two type roles, no new assets. The system sans carries human words. The
   monospace carries machine facts: format identifiers like
   `banner_16x9_1920_jpg`, pixel dimensions, rupees. That split is true rather
   than decorative - those strings are machine identifiers, and setting them in
   the machine face stops the page pretending they are prose.

   Three bands, not numbered steps: the form can be filled in any order, and
   numbering it would encode a sequence that does not exist. */

.docket {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20rem;
  gap: 2rem;
  align-items: start;
  max-width: 68rem;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 4rem;
}

.docket-title {
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 1.5rem;
}

.docket .band {
  padding: 0 0 1.5rem;
  margin: 0 0 1.5rem;
  border-bottom: 1px solid var(--border);
}

.docket .band:last-child {
  border-bottom: 0;
  margin-bottom: 0;
}

/* The band label names what the fields under it decide, which is the only
   structural claim this page makes. */
.docket .band-label {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.11em;
  color: var(--muted);
  margin: 0 0 0.85rem;
}

.docket .band-note,
.field-help {
  font-size: 0.84rem;
  color: var(--muted);
  margin: 0.6rem 0 0;
  max-width: 46ch;
}

.docket .band-note {
  margin: -0.4rem 0 0.85rem;
}

/* **A note that follows a bordered card must not pull up into it.** The
   -0.4rem above is deliberate where a note sits under a heading, and it is
   wrong here: measured in Chrome, `/restyle`'s seam-guide card ended at
   1008.5px while the note below it began at 1002.1px, so the first line was
   drawn *inside* the card's border. `/restyle` is the only page with this
   adjacency, which is why nothing else ever showed it. Found by the owner
   reading his own deployed page. */
.docket .switch + .band-note {
  margin-top: 0.75rem;
}

/* The brief is the input; everything else on this page is a consequence. */
.docket .brief-field {
  width: 100%;
  font: inherit;
  font-size: 1.15rem;
  line-height: 1.55;
  padding: 1rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  resize: vertical;
  min-height: 8.5rem;
}

.docket .brief-field::placeholder {
  color: var(--muted);
  opacity: 1;
}

.docket .field-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin-top: 1rem;
}

.docket .field {
  flex: 1 1 12rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.docket .field-narrow {
  flex: 0 1 7rem;
}

.docket .field label {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
}

/* **Every control this engine offers, in one rule.** `form.reference-form`
   and `form.source-form` styled their container, their labels, their file
   input and their button -- and never their `select` or their text `input`,
   which were therefore Chrome's own: `border: inset`, `border-radius: 0`,
   13.3px against 15.2px everywhere else. Two of five surfaces on browser
   defaults while three were on the docket vocabulary.

   Extended here rather than copied under each form, because a second copy of
   these six declarations is this codebase's named recurring defect class in
   CSS: the day the radius changes, three surfaces would follow it and two
   would not. The file inputs keep their own rule -- a file control's inner
   button belongs to the browser and cannot take this treatment. */
.docket .field select,
.docket .field input,
form.reference-form select,
form.reference-form input[type="text"],
form.source-form select,
form.source-form input[type="text"] {
  font: inherit;
  font-size: 0.95rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  min-width: 0;
}

/* A format is a machine identifier and a shape. Both are shown, because the
   identifier alone requires knowing the convention to read. */
.docket .format-list {
  display: grid;
  gap: 0.4rem;
}

.docket .format-option {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.7rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  cursor: pointer;
}

.docket .format-option:hover {
  border-color: var(--accent);
}

.docket .format-option:has(input:checked) {
  border-color: var(--accent);
  background: var(--bg);
}

/* The shape reads as quiet monospace here, not as the bordered badge the
   gallery card uses. That card needs delineation because it sits against a
   picture; this column is already quiet, and a box round every row would be
   the accessory to take off before leaving the house. Two contexts, two
   treatments, two class names - reusing one name for both is how a design
   system starts lying about itself. */
.docket .shape-tag,
.ledger .shape-tag {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text);
}

.docket .format-id,
.docket .format-shape {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.82rem;
}

.docket .format-shape {
  color: var(--muted);
  white-space: nowrap;
}

/* Picking a picture to animate is the same decision shape as picking a format
   - a row you tick, that says what it is - so it is the same control with a
   thumbnail in it, rather than a fourth vocabulary. The grid column that held
   nothing on a format row holds the picture here, which is why .format-option's
   three columns are declared again with a width on the first: an auto column
   would size to the image's intrinsic width and every row would be a different
   height. */
.docket .picture-list {
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
}

.docket .picture-option {
  grid-template-columns: auto 3.25rem 1fr;
  gap: 0.6rem;
}

.docket .picture-option img {
  width: 3.25rem;
  height: 3.25rem;
  object-fit: cover;
  border-radius: 5px;
  display: block;
  background: var(--bg);
}

.docket .picture-body {
  display: grid;
  gap: 0.15rem;
  min-width: 0;
}

.docket .picture-body .format-id {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The project's name above its own pictures, quieter than a band label and
   tighter to what it names than a band note would be. */
.docket .picture-group-name {
  margin-top: 0.7rem;
  margin-bottom: 0.3rem;
  font-weight: 600;
  color: var(--text);
}

/* A switch is a decision with a consequence, so the consequence is part of
   the control rather than a footnote under it. */
.docket .switch {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.7rem;
  align-items: start;
  padding: 0.85rem 0.9rem;
  margin-top: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
}

.docket .switch:has(input:checked) {
  border-color: var(--accent);
  background: var(--surface);
}

/* The box may never leave its own text. Phase 4.1 found the opposite live at
   390px: the labels were inline, they wrapped, and banner_16x9_1920_jpg's
   checkbox rendered on thumb_1x1_800_jpg's line. Each row is a grid with the
   input in its own fixed column, and the input is pinned so it cannot be
   squeezed or pushed onto another line. */
.docket .format-option input[type="checkbox"],
.docket .switch input[type="checkbox"] {
  flex: none;
  margin: 0;
  width: 1.05rem;
  height: 1.05rem;
}

.docket .switch input {
  margin-top: 0.2rem;
}

.docket .switch-body {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.docket .switch-title {
  font-weight: 600;
  font-size: 0.95rem;
}

.docket .switch-note {
  font-size: 0.83rem;
  color: var(--muted);
  max-width: 52ch;
}

/* The ledger. The signature of this page: what pressing the button will do,
   in plain words and rupees, before it is pressed. */
.ledger {
  position: sticky;
  top: 1.5rem;
}

.ledger-inner {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 1.1rem 1.1rem 1.2rem;
}

.ledger-label {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.11em;
  color: var(--muted);
  margin: 0 0 0.75rem;
}

.ledger-figure {
  font-size: 1.75rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 0.6rem;
  line-height: 1.1;
}

.ledger-unit {
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--muted);
}

.ledger-shapes {
  list-style: none;
  margin: 0 0 0.75rem;
  padding: 0;
  display: grid;
  gap: 0.3rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.76rem;
  color: var(--muted);
}

.ledger-shapes li {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  overflow-wrap: anywhere;
}

.ledger-empty {
  font-size: 0.82rem;
  color: var(--muted);
  margin: 0 0 0.75rem;
}

.ledger-rows {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.3rem 0.75rem;
  margin: 0 0 1rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
}

.ledger-rows dt { color: var(--muted); }

.ledger-rows dd {
  margin: 0;
  text-align: right;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.render-button {
  width: 100%;
  font: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  padding: 0.7rem 1rem;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: var(--accent-contrast);
  cursor: pointer;
}

.render-button:hover { filter: brightness(1.08); }

.ledger-foot {
  font-size: 0.76rem;
  color: var(--muted);
  margin: 0.6rem 0 0;
  text-align: center;
}

/* Keyboard focus must always be visible; this is a floor, not a feature. */
.docket :focus-visible,
.render-button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* At 390 the rail stops being a rail. It sits under the form as a summary and
   the button travels with it, so the count and the cost are still the last
   thing read before submitting. */
@media (max-width: 60rem) {
  .docket {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.25rem;
    padding: 1.25rem 1rem 3rem;
  }

  .ledger {
    position: static;
  }

  .docket .format-option {
    grid-template-columns: auto 1fr;
    row-gap: 0.2rem;
  }

  .docket .format-shape {
    grid-column: 2;
  }
}

@media (prefers-reduced-motion: reduce) {
  .render-button { transition: none; }
}

/* ---- Notification readiness on /ops ------------------------------------

   A channel that will fail every time it is used must be visible while it is
   still cheap to fix. Phase 7 shipped an email channel whose configured
   recipient did not exist, and the only way to discover that was to read
   notification rows after the first failure. */

.ops-card-wide {
  grid-column: 1 / -1;
}

.channel-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
}

.channel {
  display: grid;
  grid-template-columns: 7rem auto 1fr;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
}

.channel-problem {
  border-color: var(--danger);
}

.channel-name {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 600;
}

.channel-target {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.82rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.channel-problem-text {
  font-size: 0.85rem;
  color: var(--text);
}

.badge-ready {
  background: var(--surface);
  color: var(--muted);
  border: 1px solid var(--border);
}

.badge-problem {
  background: var(--danger);
  color: var(--bg);
}

@media (max-width: 40rem) {
  .channel {
    grid-template-columns: 1fr;
    gap: 0.25rem;
  }
}

/* --- API keys -------------------------------------------------------------

   The reveal is the whole page: a key is shown once and a person has to
   notice it, so it is the one block on the site that carries the accent
   border and sits above the table rather than inside it. The table itself is
   deliberately quiet -- prefixes are public and dull, and the only thing that
   should draw the eye is the credential that is about to disappear. */

.key-reveal {
  border: 1px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  margin: 0 0 1.75rem;
}

.key-reveal h2 {
  font-size: 1.05rem;
  margin: 0 0 0.35rem;
  color: var(--accent);
}

.key-reveal-lede,
.key-reveal-note {
  color: var(--muted);
  font-size: 0.9rem;
  max-width: 62ch;
  margin: 0;
}

.key-reveal-note { margin-top: 0.85rem; }

/* `break-all`, not `overflow-x`: a key that needs scrolling to read is a key
   half of which gets copied. It wraps, in full, every character visible. */
.key-plaintext {
  display: block;
  margin: 0.85rem 0 0;
  padding: 0.7rem 0.85rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.95rem;
  line-height: 1.5;
  word-break: break-all;
  user-select: all;
}

.key-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}

.key-table th {
  text-align: left;
  font-weight: 600;
  color: var(--muted);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0 0.75rem 0.5rem 0;
  border-bottom: 1px solid var(--border);
}

.key-table td {
  padding: 0.7rem 0.75rem 0.7rem 0;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

.key-table code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.88rem;
}

.key-row-revoked td { color: var(--muted); }

.key-status {
  font-size: 0.78rem;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  white-space: nowrap;
}

.key-status-live {
  color: var(--accent);
  border-color: var(--accent);
}

.key-status-revoked { color: var(--muted); }

/* The table is the one wide thing on this page; it scrolls inside itself
   rather than making the page scroll sideways on a phone. */
@media (max-width: 40rem) {
  .key-table { display: block; overflow-x: auto; white-space: nowrap; }
}

/* Source videos — the upload surface (Phase 11, Task 13a).

   Deliberately the reference page's own vocabulary rather than a third one:
   the form rules above are shared by selector, and what is added here is only
   what a video needs and a still does not — the limit strip that has to be
   read *before* a file is picked, and a card whose stage is a <video> instead
   of an <img>. */

.limit-list {
  list-style: none;
  margin: 0 0 2rem;
  padding: 0;
  display: grid;
  gap: 0.55rem;
  max-width: 60ch;
  font-size: 0.88rem;
  color: var(--muted);
}

.limit-list li {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: 0.85rem;
  align-items: baseline;
  padding-left: 0.9rem;
  border-left: 2px solid var(--border);
}

.limit-figure {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  letter-spacing: -0.01em;
}

.source-group {
  margin-bottom: 2.25rem;
}

.source-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1rem;
}

.source-card {
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.source-card video {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: contain;
  display: block;
  background: var(--stage-field);
}

.source-card figcaption {
  padding: 0.6rem 0.75rem 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  border-top: 1px solid var(--border);
}

/* Two actions share the foot of a source card: the one a person came for and
   the one they will use once. Restyle takes the room, Delete takes the corner.
   `.inline-form` is display:contents, so its button is a direct grid child. */
.card-actions {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.5rem;
  margin: 0 0.75rem 0.75rem;
}

/* A reference card is 180px wide -- a source card is much wider -- and
   `1fr auto` sized the Delete column to its own content, which pushed its right
   edge 3px past a card whose `overflow` is `hidden`. Measured in a real browser
   with getBoundingClientRect: button right 338, card right 335. Two equal
   columns fit whatever the card is, and the buttons centre in them. */
.reference-card .card-actions {
  /* One column, stacked. Two buttons do not fit side by side on a 180px card
     and `1fr 1fr` does not make them: `1fr` is `minmax(auto, 1fr)`, and
     `min-width: auto` stops a grid item shrinking below its own content, so
     the columns resolved to 92.8 + 81.4 + 8 = 182px inside 154px of space and
     the clipping got WORSE -- 3px became 27px. Measured in a real browser
     both times; the CSS unit test passed through both, which is exactly why
     the browser is the thing that decides. */
  grid-template-columns: 1fr;
}

.reference-card .card-actions .btn,
.reference-card .card-actions .inline-form,
.reference-card .card-actions .inline-form .btn {
  width: 100%;
  text-align: center;
}

.source-card .btn {
  text-align: center;
  padding: 0.35rem 0.6rem;
  font-size: 0.82rem;
}

@media (max-width: 560px) {
  .limit-list li {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }
}


/* ---------------------------------------------------------------------------
   The asset pool (E1).

   Two classes only. Everything else on /assets is the vocabulary the reference
   and ledger surfaces already established -- a fourth one would be the defect
   this codebase is named after, written in CSS.
   --------------------------------------------------------------------------- */

/* A pool holds sound and video, which have no thumbnail. The tile keeps the
   grid's own shape rather than collapsing, so a row of mixed kinds still reads
   as a row. */
.asset-glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1 / 1;
  width: 100%;
  font-size: 2rem;
  color: #8a8f98;
  background: #f4f5f7;
}

/* `.docket .field-help` has said this for three surfaces already; the pool's
   upload form is not inside a docket, so the same sentence needs the same
   treatment standing on its own. */
.field-help {
  margin: 0.35rem 0 0;
  font-size: 0.85rem;
  line-height: 1.45;
  color: #6b7280;
}


/* The AI CTA (E2-4). Two classes, because the controls themselves are the
   existing `.secondary .btn` -- a generative control is not a new kind of
   button and giving it one would be the fourth vocabulary the picker's own
   test forbids. The row sits under the field it drives and wraps rather than
   scrolling, because four controls plus a sentence is wider than a phone. */
.ai-cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0.5rem 0 0;
}

/* The sentence the component says back. `role="status"` with `aria-live`, the
   same shape as `.form-busy`, so a screen reader hears the answer rather than
   only seeing a field change under it. */
.ai-cta-note {
  font-size: 0.85rem;
  color: var(--muted, #6b7280);
  min-height: 1.2em;
}


/* ---- Progressive disclosure, and the type scale that makes it work ------
   **The owner, 2026-08-20: "there is too much text and presentation is also
   like 1st year HTML student".** `/compose` was 805 lines of template carrying
   twelve `.field-help` prose blocks, several of them full paragraphs of
   measurement caveats, and the page ran to 6700px at 1440 wide.

   🔴 **Not one sentence of it is deleted, and none may be.** CLAUDE.md forbids
   a page that claims a capability with an open gap, so every caveat on this
   page was earned by a defect that shipped. The problem was never that the
   words are there -- it is that a hard-won qualification and a routine hint
   were drawn in the same 0.84rem grey, at the same level, all of it shouted at
   once, so a reader scrolling for a control met an undifferentiated wall and
   the important sentences were camouflaged by the ordinary ones.

   So the words move behind a native `<details>` and stay **one click away**.

   **Native `<details>`, deliberately, and not a script.** `pytest` cannot
   execute this page's JavaScript -- a standing trap in this repository, and
   three injections have already failed to fire for exactly that reason. A
   disclosure built from a click handler would be a control no test in this
   suite can open. `<details>` needs no JS, is keyboard-operable and
   screen-reader-addressable for free, and its content stays in the DOM and in
   the served HTML, so every absence assertion over this page keeps working
   unchanged and document order -- which four tests assert on -- is preserved. */

.docket details.why {
  margin: 0.5rem 0 0;
  max-width: 46ch;
}

/* The summary is the quietest thing that can still be seen to be a control:
   the accent colour and a pointer say "openable", the size says "not the
   point". The default disclosure triangle is removed on both engines because
   it sits on the text baseline and drags the row out of the left margin the
   fields are aligned to. */
.docket details.why > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  cursor: pointer;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--accent);
  list-style: none;
  padding: 0.15rem 0;
  border-radius: 6px;
}

.docket details.why > summary::-webkit-details-marker { display: none; }

/* Drawn rather than typed, so it rotates. A "+"/"-" swap needs a rule per
   state and a character that changes width between them. */
.docket details.why > summary::before {
  content: "";
  width: 0.42rem;
  height: 0.42rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 120ms ease;
  flex: none;
}

.docket details.why[open] > summary::before {
  transform: rotate(45deg);
}

.docket details.why > summary:hover {
  text-decoration: underline;
}

/* The prose inside sits against a rule rather than floating, so an open
   disclosure reads as a held-open aside and not as the page resuming. */
.docket details.why[open] > summary {
  margin-bottom: 0.4rem;
}

.docket details.why > :not(summary) {
  border-left: 2px solid var(--border);
  padding-left: 0.85rem;
  margin: 0;
}

.docket details.why > :not(summary) + :not(summary) {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
}

/* ---- A limit is not a hint, and must not be drawn like one --------------
   🔴 The sentences this page must never bury: a refusal, and a capability
   this engine does NOT have. "A film keeps your scene, not your person" was
   earned by four films that lost their subject, and it sits on the control
   that buys one. It stays in the flow, visible, and is now drawn as what it
   is -- so promoting the rest of the prose into disclosures makes the warnings
   MORE visible rather than less. */
.docket .note-limit {
  border-left: 2px solid var(--warn);
  background: color-mix(in srgb, var(--warn) 7%, transparent);
  padding: 0.6rem 0.75rem;
  border-radius: 0 6px 6px 0;
  color: var(--text);
  margin-top: 0.75rem;
}

/* ---- The band heading was quieter than the body text under it -----------
   The eyebrow stays an eyebrow -- it is the page's one structural claim and
   the mono/uppercase treatment is deliberate -- but at 0.7rem in `--muted` it
   was the *smallest and faintest* element on a page whose body text is 0.85rem
   grey, so a section header did not read as a section header. Full ink and a
   little more size, and the band below it gets the room to be a band. */
.docket .band-label {
  font-size: 0.75rem;
  color: var(--text);
  margin-bottom: 1rem;
}

.docket .band {
  padding-bottom: 2.25rem;
  margin-bottom: 2.25rem;
}

/* A `<details>` closing right above the band rule left the two hairlines a few
   pixels apart and reading as one thick line. */
.docket .band > details.why:last-child {
  margin-bottom: 0.25rem;
}

/* ---- A narrow field must not be stretched by a tall neighbour -----------
   `.field-row` is a flex row and `align-items` defaulted to `stretch`, so the
   7rem Strength column was drawn **296px tall** to match the Quality tier
   column beside it -- an empty box five times the height of its own input.
   Measured in Chrome, not guessed: the Strength `.field` returned
   `height: 296` against an input of 38.

   It cost nothing while the tier's explanation was a wall of prose filling
   that column. Collapsing the prose is what made the empty column visible --
   the same shape as HD revealing hands that were always broken. */
.docket .field-row {
  align-items: start;
}

/* E10-1 -- room for the pool search box. **One rule, and no new vocabulary.**
   `.field` already places a label over a control in this page's bands, and the
   scope note next to it reuses `.note-limit`, which is the page's OWN way of
   drawing a limit (above). Inventing a second limit style here would be the
   markup half of this codebase's named defect class, and it would mean two
   answers to "what does a limit look like". */
.docket .pool-search {
  margin: 0 0 1rem;
  max-width: 32rem;
}

/* ---- A disabled control has to LOOK disabled -----------------------------
   G-E9-24 turns the Strength dial off on a model whose graph has no strength
   in it. Nothing in this stylesheet had ever styled `:disabled`, so it looked
   exactly like a live field -- which is the defect wearing a new coat: a
   person would still try to move it. No new class; the state does the work. */
.docket input:disabled,
.docket select:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  background: var(--surface);
}


/* ---- E6-2: a word, and the second it was sung on -------------------------
   The only genuinely new shape this slice needed. Everything else on the
   transcript page reuses vocabulary that already existed -- `.group-title` for
   the headings, `.key-table` for the phrase table, `.ledger` for the figures --
   because a second answer to "what does a heading look like" is the markup half
   of this codebase's named defect class.

   **The time is drawn, not hidden in a tooltip.** A transcript that renders the
   words and puts the timings in `title=` looks completely correct and is
   worthless to a person checking whether 4.24 s is really where "kahaan"
   starts -- which is the same silent failure `transcribe.parse` refuses at the
   other end of the same document. So the number is on the chip, in tabular
   figures so a column of them lines up, and the tooltip carries the END time
   and the confidence as the extra rather than as the fact. */
.word-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0 0 1.5rem;
}

.word-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  padding: 0.3rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}

.word-chip:hover,
.word-chip:focus-visible {
  border-color: var(--accent);
  background: var(--bg);
}

.word-text {
  font-weight: 600;
}

.word-at {
  font-size: 0.72rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* The player is a real control a person drags, so it gets the page's width and
   not a browser default squeezed into a paragraph. */
audio#song {
  width: 100%;
  margin: 0 0 1.5rem;
}

/* ------------------------------------------------------------------ E12-1
   The script wizard.

   The owner's E11 diagnosis, applied to a page built after he made it: THEY
   put the output on the page and hid the controls; we put the controls on the
   page and there was no output on it at all. So this page opens on the thing
   he is making -- the timeline -- and the form that adds to it comes after.

   🔴 **There is no JavaScript here and that is D-E12-3.** An `innerHTML` swap
   silently eats every ticked control inside it (G-E9-28) and no test in this
   suite can execute a page script, so a wizard that repainted its scene list
   would carry a defect class only a human could ever find. Every mutation is a
   form POST and a full server render, and this stylesheet is the whole of the
   client side. */

.timeline-strip {
  display: flex;
  gap: 2px;
  width: 100%;
  height: 2.5rem;
  margin: 0 0 0.75rem;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--stage-field);
}

/* `flex-grow` carries the duration, so a 10-second scene is really ten times
   the width of a 1-second one -- the bar is a measurement rather than a
   decoration, which is the only reason it is on the page. */
.timeline-scene {
  flex-basis: 0;
  min-width: 3px;
  display: block;
}

/* One hue per region, in his order: the preface sets up, the song is the
   accent, the outro settles. Colour is never the ONLY carrier -- every scene's
   region is written out in the table underneath. */
.timeline-scene.seg-preface { background: var(--muted); }
.timeline-scene.seg-song    { background: var(--accent); }
.timeline-scene.seg-outro   { background: var(--warn); }

.scene-edit {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  margin: 0 0 0.5rem;
  background: var(--surface);
}

.scene-edit-title {
  cursor: pointer;
  font-weight: 600;
}

/* Collapsed by default, so 143 scenes are a table and not 143 open forms --
   and PRESENT in the document all the same, so nothing needs a repaint to
   reach one. */
.scene-edit[open] { background: var(--bg); }

.scene-edit label { display: block; margin: 0.35rem 0; }
.scene-edit input[type="text"],
.scene-edit textarea,
.scene-edit select { width: 100%; max-width: 42rem; }

/* 🔴 A PROSE list, and it exists because the real browser found the alternative
   broken. `.limit-list li` is a two-column grid — `7.5rem 1fr`, built for a
   `<span class="limit-figure">` beside a sentence — so a plain sentence in it
   renders as a 7.5rem-wide sliver with any `<strong>` flung into the next
   column. Every server-side check was green and the page was unreadable, which
   is [[only-the-browser-knew]] for the third time in this project. */
.script-notes {
  list-style: none;
  margin: 0 0 1.75rem;
  padding: 0;
  display: grid;
  gap: 0.6rem;
  max-width: 62ch;
  font-size: 0.9rem;
  color: var(--muted);
}

.script-notes li {
  padding-left: 0.9rem;
  border-left: 2px solid var(--border);
  line-height: 1.5;
}

/* A problem is neither an error (nothing failed) nor a limit (this is not where
   the engine stops) — it is a thing HE can fix, so it takes --warn and a
   heavier rule than a note. */
.script-problems li {
  border-left-color: var(--warn);
  color: var(--text);
}

/* The form. Fields that are all one width read as a form; fields that size
   themselves to their default value read as the "1st year HTML student" the
   owner named. */
.script-form label { display: inline-block; min-width: 9.5rem; }
.script-form input[type="text"],
.script-form textarea,
.script-form select { width: 100%; max-width: 42rem; }
.script-form p { margin: 0 0 0.75rem; }
.script-form .field-help { display: block; max-width: 42rem; margin-top: 0.2rem; }

/* ------------------------------------------------------------------ E12-1b
   THE STORYBOARD.

   🔴 **His words, having used the first cut:** *"the interface we are using is
   not that user friendly I got stuck myself on lot of places"*, *"something
   very user friendly"*, *"super easy to use"*, **"movie storyboard types"**.

   What was here: a table, a second list of 112 collapsed nine-field forms, and
   a nine-field add form. What is here: the film as SHOTS, one card each, and a
   box you write it in. Nothing was deleted — every field is on the shot's own
   page and every limit is still stated. It is the E11 diagnosis applied to a
   page young enough to fix: put the OUTPUT on the page. */

/* One strip carrying everything he has to know before he reads a single shot:
   how many, how long against what he declared, and what it costs to render. */
.runbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 0.65rem;
  padding: 0.85rem 1rem;
  margin: 0 0 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.runbar-figure {
  font-size: 1.15rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.runbar-unit { color: var(--muted); font-size: 0.85rem; }
.runbar-sep { flex: 0 0 1px; align-self: stretch; background: var(--border); }
.runbar-ok, .runbar-warn {
  margin-left: auto;
  font-size: 0.8rem;
  font-weight: 600;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
}
.runbar-ok { color: var(--accent); border: 1px solid var(--accent); }
.runbar-warn { color: var(--warn); border: 1px solid var(--warn); }

/* The board itself. `auto-fill` rather than a fixed count, so it is a
   contact sheet on a laptop and a strip on a phone with no breakpoint typed. */
.board {
  list-style: none;
  margin: 0 0 2rem;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13.5rem, 1fr));
  gap: 1rem;
}

.shot {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  display: flex;
  flex-direction: column;
}

/* The region, as a rule down the left edge. 🔴 Colour is NEVER the only
   carrier — every card writes its region out in `.shot-seg`. */
.shot.seg-preface { border-left: 4px solid var(--muted); }
.shot.seg-song    { border-left: 4px solid var(--accent); }
.shot.seg-outro   { border-left: 4px solid var(--warn); }

/* 16:9, because that is the shape the film is delivered in. A card that is a
   different shape from the deliverable teaches the wrong frame. */
.shot-frame {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  background: var(--stage-field);
  overflow: hidden;
}

.shot-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }

.shot-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 0.75rem;
  color: var(--muted);
}

.shot-no, .shot-len {
  position: absolute;
  top: 0.4rem;
  font-size: 0.7rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
}
.shot-no { left: 0.4rem; }
.shot-len { right: 0.4rem; font-weight: 600; }

.shot-time {
  margin: 0.55rem 0.75rem 0;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.shot-seg { text-transform: uppercase; letter-spacing: 0.04em; }

.shot-action {
  margin: 0.3rem 0.75rem 0.6rem;
  font-size: 0.85rem;
  line-height: 1.4;
  flex: 1 1 auto;
}

.shot-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  margin: 0;
  padding: 0.45rem 0.6rem;
  border-top: 1px solid var(--border);
}

.shot-tool {
  font: inherit;
  font-size: 0.72rem;
  line-height: 1;
  padding: 0.3rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
}
.shot-tool:hover { border-color: var(--accent); color: var(--accent); }
.shot-tool-cut:hover { border-color: var(--danger); color: var(--danger); }

/* The box the film is written in. Monospace, because a line IS a shot and the
   eye needs to see where one ends. */
.script-box {
  width: 100%;
  max-width: 54rem;
  font: 0.85rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  resize: vertical;
}

/* One shot's own page: the picture beside the fields, not above them. */
.shot-page {
  display: grid;
  grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
  gap: 1.75rem;
  align-items: start;
}

@media (max-width: 46rem) { .shot-page { grid-template-columns: 1fr; } }

.shot-page-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--stage-field);
  overflow: hidden;
}
.shot-page-frame img { width: 100%; height: 100%; object-fit: cover; }

.crumb { margin: 0 0 0.4rem; font-size: 0.85rem; }
.page-title-sub { font-weight: 400; color: var(--muted); }

/* 🔴 Found in the real browser, immediately: each tool is its OWN form (one
   POST per action is the whole of D-E12-3), and a block-level form inside a
   flex row takes the full width — so Edit, Repeat, up, down and Cut stacked
   into five rows and the card was twice as tall as its picture. `display:
   contents` makes each button a direct child of the flex row, which is the
   only way to keep five separate forms and one line of controls. */
.shot-tools form.inline-form { display: contents; }
.shot-tools .shot-tool { flex: 0 0 auto; }

/* ------------------------------------------------------------------ E12-1c
   INTERACTIVE, because he met the last two cuts and said so:
   *"this is useless no one can build like this"*, *"it should have a story
   board like presentation where we can add more scenes"*, *"something
   interactive not like a passport re issue form"*. */

/* The insertion point, between every pair of cards. It is a real form and it
   works with the page script off; what CSS does is make it a slot rather than
   a row of controls -- collapsed to a thin dashed gutter until it is wanted. */
.shot-add {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  display: grid;
  place-items: center;
  min-height: 3rem;
  transition: border-color 120ms ease, background 120ms ease;
}

.shot-add:hover, .shot-add:focus-within {
  border-color: var(--accent);
  background: var(--surface);
}

.shot-add form {
  display: grid;
  gap: 0.35rem;
  width: 100%;
  padding: 0.5rem;
  grid-template-columns: 1fr;
}

.shot-add-plus {
  font-size: 1.4rem;
  line-height: 1;
  color: var(--muted);
  text-align: center;
}
.shot-add:hover .shot-add-plus, .shot-add:focus-within .shot-add-plus {
  color: var(--accent);
}

/* Until it is hovered or focused, only the + shows: a hundred and thirteen
   insertion points, each carrying three controls, would otherwise be a wall. */
.shot-add form > :not(.shot-add-plus) { display: none; }
.shot-add:hover form > *, .shot-add:focus-within form > * { display: block; }
.shot-add input, .shot-add select { width: 100%; font-size: 0.78rem; }
.shot-add button { width: 100%; }

/* The card opens IN PLACE. The panel is in the document from the first render;
   the script only removes `hidden`, so nothing is fetched and nothing is
   repainted. */
.shot-open {
  border-top: 1px solid var(--border);
  padding: 0.6rem 0.75rem;
  background: var(--bg);
}
.shot-open .script-form label { display: block; margin: 0 0 0.35rem; }
.shot-open .script-form input,
.shot-open .script-form select,
.shot-open .script-form textarea { width: 100%; font-size: 0.8rem; }
.shot-open .script-form p { margin: 0 0 0.5rem; }
.shot .shot-frame { border: 0; padding: 0; width: 100%; cursor: pointer; }
.shot.is-open { border-color: var(--accent); }

/* Drag. The grip is the affordance; `draggable` on the <li> is the mechanism. */
.shot { transition: opacity 120ms ease, box-shadow 120ms ease; }
.shot.is-dragging { opacity: 0.4; }
.shot-grip {
  position: absolute;
  bottom: 0.4rem;
  right: 0.4rem;
  font-size: 0.7rem;
  letter-spacing: -2px;
  padding: 0.15rem 0.35rem;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  cursor: grab;
}

/* The button the whole page exists to reach. */
.shoot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  padding: 0.9rem 1rem;
  margin: 0 0 1.25rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
}
.shoot input[type="number"] { width: 5rem; }
.shoot .render-button { margin-left: auto; }
.shoot .field-help { flex: 1 0 100%; margin: 0; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
}

/* 🔴 The add card is always OPEN now — it is one card at the end of the board,
   not a hundred and thirteen gutters, so there is nothing to collapse. */
.shot-add form > :not(.shot-add-plus) { display: block; }
.shot-add { min-height: 0; align-self: stretch; }

/* The frame is a div with role=button, so it needs the affordances a button
   gave it for free — and none of the layout a button broke. */
.shot .shot-frame { cursor: pointer; }
.shot .shot-frame:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
