/* Generation UI: the style picker and the live generation cards. */

.style-picker-card {
  background: var(--paper-card);
  border: var(--border-hard);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  padding: var(--space-5);
  margin-top: var(--space-4);
  max-width: 34rem;
}

.style-picker-form {
  display: grid;
  gap: var(--space-4);
  margin-top: var(--space-3);
  /* stretch, not start (2026-09-03): the field groups shrink-wrapped, so
     the room select and the note sat narrower than the focus field.
     Buttons keep their intrinsic width inside the stretched groups. */
  justify-items: stretch;
}

.focus-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: var(--space-2);
  width: 100%;
}

.focus-option {
  position: relative;
  cursor: pointer;
  border: 1.5px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--paper);
  padding: 0.6em 0.75em;
  display: grid;
  gap: 0.3em;
  align-content: start;
}

.focus-option input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.focus-name {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.75rem;
  color: var(--ink);
}

.focus-hint {
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--ink-soft);
}

.focus-option:hover {
  border-color: var(--ink);
}

.focus-option:has(input:checked) {
  border: 1.5px solid var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}

.focus-option:has(input:checked) .focus-name {
  color: var(--accent);
}

.focus-option:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The focus modes as a disclosure (2026-07-24). Four cards with hint
   text cost ~280px of a rail whose whole job is to fit one screen, for
   a control most renders leave on the default. Collapsed, the summary
   still states the active mode, so nothing is hidden in effect: the
   row reads as an answer, not as a thing to go find. */

.focus-details {
  width: 100%;
}

.focus-details > summary {
  display: grid;
  gap: var(--space-2);
  cursor: pointer;
  list-style: none;
}

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

/* The collapsed value reads as a field, in the room select's own
   clothes, so the choice is as findable as the room above it (owner:
   the mono link was easy to miss, 2026-09-03). The chevron flips open. */
.focus-summary-value {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--ink);
  background: var(--paper);
  border: 1.5px solid var(--line);
  border-radius: var(--radius-md);
  padding: 0.5em 0.6em;
}

.focus-summary-value::after {
  content: "";
  flex: none;
  width: 0.42em;
  height: 0.42em;
  margin-right: 0.25em;
  border-right: 1.5px solid var(--ink);
  border-bottom: 1.5px solid var(--ink);
  transform: translateY(-0.15em) rotate(45deg);
  transition: transform 150ms ease;
}

.focus-details[open] > summary .focus-summary-value::after {
  transform: translateY(0.1em) rotate(-135deg);
}

.focus-details[open] > summary .focus-summary-value {
  border-color: var(--ink);
}

.focus-details > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.focus-details .focus-picker {
  margin-top: var(--space-2);
}

/* Workspace tool switcher (OVERHAUL.md Phase D): a three-way segmented
   control at the top of the form. Paid tools render as padlocked links
   for free users, same fence language as the quality tiers. */

.tool-switcher {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border: 1.5px solid var(--line-strong);
  width: 100%;
}

.tool-tab {
  position: relative;
  cursor: pointer;
  text-align: center;
  padding: 0.5em 0.3em;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.75rem;
  color: var(--ink-soft);
  text-decoration: none;
  background: var(--paper);
}

.tool-tab + .tool-tab {
  border-left: 1.5px solid var(--line);
}

.tool-tab input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.tool-tab:hover {
  color: var(--ink);
}

.tool-tab:has(input:checked) {
  background: var(--ink);
  color: var(--paper);
}

.tool-tab:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.tool-tab.is-locked {
  background: var(--paper-deep);
  padding-right: 1.1em;
}

.tool-tab.is-locked .quality-lock {
  top: 0.55em;
  right: 0.35em;
  width: 0.55rem;
}

.tool-tab.is-locked:hover {
  color: var(--accent);
}

.tool-tab.is-locked:hover .quality-lock {
  fill: var(--accent);
}

/* Room selector (OVERHAUL.md decision 4) and the optional focus note
   (decision 5): plain inputs in the rail's card language. */

.room-picker {
  position: relative;
  display: grid;
  gap: var(--space-2);
  width: 100%;
}

.room-select,
.room-detail,
.focus-note {
  width: 100%;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--ink);
  background: var(--paper);
  border: 1.5px solid var(--line);
  border-radius: var(--radius-md);
  padding: 0.5em 0.6em;
}

/* The select was the one control in the rail still wearing its OS
   chrome: a native double-chevron and platform metrics sitting between
   a custom segmented control and custom radio cards. appearance: none
   hands the affordance to a drawn caret so the rail reads as one
   system. The caret is CSS geometry, not an SVG data URI, because hard
   hairline angles are already this design's vocabulary. */
.room-select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.1em;
  cursor: pointer;
}

.room-picker::after {
  content: "";
  position: absolute;
  top: 1.15em;
  right: 0.85em;
  width: 0.42em;
  height: 0.42em;
  border-right: 1.5px solid var(--ink-soft);
  border-bottom: 1.5px solid var(--ink-soft);
  transform: translateY(-50%) rotate(45deg);
  pointer-events: none;
  transition: border-color 150ms ease;
}

.room-picker:hover::after {
  border-color: var(--ink);
}

/* Required and still on its prompt: the room gates every render, so the
   unpicked state reads as work outstanding rather than as a filled
   field. :invalid is exact here, the prompt option carries no value. */
.room-select:invalid {
  color: var(--ink-soft);
}

.room-select:hover,
.room-detail:hover,
.focus-note:hover {
  border-color: var(--ink);
}

.room-select:focus-visible,
.room-detail:focus-visible,
.focus-note:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.focus-note {
  resize: vertical;
  min-height: 3.2em;
}

.label-optional {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.75rem;
  color: var(--ink-soft);
  margin-left: 0.4em;
}

/* The free tier's fenced note field: same dashed lock language as the
   locked quality tiers, one tap from pricing. */
.focus-note-locked {
  position: relative;
  display: block;
  width: 100%;
  font-size: var(--text-xs);
  line-height: 1.45;
  color: var(--ink-soft);
  background: var(--paper-deep);
  border: 1.5px dashed var(--line);
  padding: 0.55em 1.8em 0.55em 0.6em;
  text-decoration: none;
}

.focus-note-locked em {
  font-style: normal;
  color: var(--ink);
}

.focus-note-locked:hover {
  border-color: var(--accent);
  border-style: solid;
}

.focus-note-locked:hover .quality-lock {
  fill: var(--accent);
}

/* Quality picker (OVERHAUL.md decision 3): same card language as the
   focus picker. For free users the paid tiers render as locked links
   into pricing: the fence lives inside the working UI, honestly. */

.quality-picker {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: var(--space-2);
  width: 100%;
}

.quality-option {
  position: relative;
  cursor: pointer;
  border: 1.5px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--paper);
  padding: 0.6em 0.75em;
  display: grid;
  gap: 0.3em;
  align-content: start;
  text-decoration: none;
}

.quality-option input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.quality-name {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.75rem;
  color: var(--ink);
}

.quality-hint {
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--ink-soft);
}

.quality-cost {
  justify-self: start;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.75rem;
  color: var(--ink-soft);
  border: 1px solid var(--line);
  padding: 0.1em 0.5em;
  border-radius: var(--radius-pill);
}

.quality-option:hover {
  border-color: var(--ink);
}

.quality-option:has(input:checked),
.quality-option.is-current {
  border: 1.5px solid var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}

.quality-option:has(input:checked) .quality-name,
.quality-option.is-current .quality-name {
  color: var(--accent);
}

.quality-option:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.quality-option.is-current {
  cursor: default;
}

/* The locked tiers: readable, real, and one tap from pricing. The lock
   sits where a checked state would, so the eye reads "this exists and
   is closed", not "this is broken". */
.quality-option.is-locked {
  background: var(--paper-deep);
  border-style: dashed;
}

.quality-option.is-locked .quality-name,
.quality-option.is-locked .quality-hint {
  color: var(--ink-soft);
}

.quality-lock {
  position: absolute;
  top: 0.6em;
  right: 0.65em;
  width: 0.7rem;
  height: auto;
  fill: var(--ink-soft);
}

.quality-option.is-locked:hover {
  border-color: var(--accent);
  border-style: solid;
}

.quality-option.is-locked:hover .quality-lock {
  fill: var(--accent);
}

.quality-option.is-locked:hover .quality-name {
  color: var(--accent);
}

.style-picker-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
  gap: var(--space-2);
  width: 100%;
  max-height: 24rem;
  overflow-y: auto;
  padding-right: var(--space-1);
}

.style-option {
  position: relative;
  cursor: pointer;
  overflow: hidden;
  border: 1.5px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--paper);
  display: grid;
}

.style-option input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.style-option img {
  width: 100%;
  height: auto;
  display: block;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.style-option span {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.75rem;
  padding: 0.35em 0.5em;
  color: var(--ink-soft);
}

.style-option:hover {
  border-color: var(--ink);
}

.style-option:has(input:checked) {
  border: 1.5px solid var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}

.style-option:has(input:checked) span {
  background: var(--accent);
  color: #fff;
}

.style-option:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Generation cards */

.generations {
  display: grid;
  gap: var(--space-4);
  margin-top: var(--space-5);
  max-width: 34rem;
}

.generation-card {
  background: var(--paper-card);
  border: var(--border-hard);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  overflow: hidden;
}

.generation-photo {
  width: 100%;
  height: auto;
  display: block;
}

.generation-meta {
  padding: var(--space-3) var(--space-4);
  display: grid;
  gap: var(--space-2);
}

.generation-style {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--text-sm);
  color: var(--ink-soft);
}

.generation-note {
  font-size: var(--text-sm);
  color: var(--ink-soft);
}

.generation-actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  align-items: center;
}

/* Four full-length labels wrapped onto a second row and stranded the
   last one. The labels are shorter now ("Regenerate", "Change style");
   this keeps the row honest at narrow canvas widths without letting a
   single button orphan itself. */
.generation-actions .btn-sm {
  flex: 0 1 auto;
}

/* An edit render is identified by what it changed, not by the tool that
   made it: every one of them read "ONE CHANGE" otherwise. The
   instruction is the user's own sentence, so it stays in sentence case
   under a mono kicker rather than being shouted in uppercase. */
.generation-edit-note {
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--ink);
  font-style: italic;
  max-width: 34rem;
}

.generation-meta-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-3);
}

.generation-when {
  font-size: var(--text-xs);
  color: var(--ink-soft);
}

.generation-before {
  text-align: right;
  flex-shrink: 0;
}

.generation-before img {
  width: 5rem;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  display: block;
}

.generation-before figcaption {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.75rem;
  color: var(--ink-soft);
  margin-top: 0.2em;
}

/* The finished design (BACKLOG B5, owner calls 2026-09-23). The card is
   a size container, so the toolbar can fold its labels by the card's own
   width (the workspace canvas is wide, the /designs grid is narrow). */
.generation-card {
  container: gcard / inline-size;
}

/* A finished card lets its menus rise past its edge, so the stage clips
   the picture's corners instead of the whole card clipping everything. */
.generation-card:has(.generation-toolbar) {
  overflow: visible;
}

.generation-stage {
  position: relative;
}

.generation-card:has(.generation-toolbar) .generation-stage {
  overflow: hidden;
  border-radius: calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0 0;
}

/* The design leads alone; the photo it started from is a thumbnail in
   the corner, no label. Hover shows it in place on a pointer device; a
   click or a tap opens it full size in the lightbox. */
.generation-before-peek {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 200ms var(--ease-out);
  pointer-events: none;
}

@media (hover: hover) {
  .generation-stage:has(.generation-before-thumb:hover) .generation-before-peek {
    opacity: 1;
  }
}

/* Without hover the peek never shows, and a lazy image with no box is
   never fetched, so a phone skips its 1200px download (review
   2026-09-25). */
@media (hover: none) {
  .generation-before-peek { display: none; }
}

.generation-before-thumb {
  position: absolute;
  right: var(--space-3);
  bottom: var(--space-3);
  z-index: 1;
  width: 5.5rem;
  aspect-ratio: 4 / 3;
  border: 2px solid var(--paper-card);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--tint);
  box-shadow: var(--shadow-soft);
}

.generation-before-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.generation-before-thumb:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@container gcard (max-width: 25rem) {
  .generation-before-thumb {
    width: 4.5rem;
    right: var(--space-2);
    bottom: var(--space-2);
  }
}

/* The toolbar: Download, Use as new photo, More. The menus are <details>
   whose panels drop below the toolbar at its full width, so a narrow
   card never pushes one off its edge; near the bottom of the screen the
   menu controller flips them above (opens-up). */
/* Was this what you wanted? (BACKLOG B38): two round buttons after the
   question, the chosen one filled; a down opens one line for the note. */
.design-rating { margin-top: var(--space-2); }
.design-rating-row { display: flex; align-items: center; gap: 6px; }
.design-rating-q { margin: 0 auto 0 0; font-size: 0.875rem; color: var(--ink-soft); }
.design-rating-form { margin: 0; }

.design-rating-btn {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--paper-card);
  color: var(--ink-soft);
  cursor: pointer;
  transition: background 160ms, color 160ms, border-color 160ms;
}

.design-rating-btn .studio-icon { width: 17px; height: 17px; }
.design-rating-btn:hover { color: var(--ink); border-color: var(--ink-mute); }
.design-rating-btn.is-on { background: var(--deep); border-color: var(--deep); color: #fff; }

.design-rating-note { display: flex; align-items: stretch; gap: var(--space-2); margin-top: var(--space-2); }

.design-rating-note input[type="text"] {
  flex: 1;
  min-width: 0;
  min-height: 34px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--paper-card);
  font: inherit;
  font-size: 0.875rem;
  color: var(--ink);
}

.generation-toolbar {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.generation-toolbar .toolbar-btn {
  gap: var(--space-2);
  /* Safari nudged the icon a fraction of a pixel sideways on hover (owner
     report, 2026-09-24): the ink's transform put the button on a new
     compositing layer mid-hover and the icon re-rasterized with other
     subpixel snapping. Each button and its ink hold their layer from the
     start, so nothing is re-layered when the pointer arrives. */
  will-change: transform;
}

.generation-toolbar .toolbar-btn::before {
  will-change: transform;
}

/* The primary's bloom fades in on hover: its layer is held too. */
.generation-toolbar .toolbar-btn::after {
  will-change: opacity;
}

.toolbar-icon {
  display: block;
  flex-shrink: 0;
  width: 1.125rem;
  height: 1.125rem;
}

.toolbar-menu > summary {
  list-style: none;
}

.toolbar-menu > summary::-webkit-details-marker {
  display: none;
}

.toolbar-more {
  margin-left: auto;
}

/* Icon-only controls are circles the height of their pill neighbors. */
.generation-toolbar .toolbar-icon-only {
  width: 2.875rem;
  padding: 0;
}

.toolbar-panel {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: 5;
  display: grid;
  gap: 2px;
  padding: var(--space-2);
  background: var(--paper-card);
  border: var(--border-hard);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-deep);
}

.generation-toolbar.opens-up .toolbar-panel {
  top: auto;
  bottom: calc(100% + var(--space-2));
}

/* Each card is its own stacking context (the size container), so the
   one with an open menu rises over the cards after it. */
.generation-card:has(.toolbar-menu[open]) {
  position: relative;
  z-index: 2;
}

.toolbar-panel-title {
  padding: var(--space-1) var(--space-2) var(--space-2);
  font-size: var(--text-xs);
  color: var(--ink-soft);
}

.toolbar-item-icon {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.toolbar-item-icon .toolbar-icon { width: 1.375rem; height: 1.375rem; color: var(--ink-soft); }
.toolbar-item-icon .toolbar-icon.is-wide { width: 1.75rem; height: 1.125rem; margin-inline: -0.1875rem; }

.toolbar-panel-hint {
  margin: -2px var(--space-3) var(--space-1);
  font-size: var(--text-xs);
  color: var(--ink-mute);
}

.toolbar-item-form {
  display: block;
  margin: 0;
}

.toolbar-item {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 2.5rem;
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.toolbar-item:hover,
.toolbar-item:focus-visible {
  background: var(--tint);
  color: var(--ink);
}

.toolbar-item.is-danger {
  color: var(--accent-deep);
}

/* The labels fold by the card's own width, one step at a time, so the
   bar never wraps: "Make a video" becomes "Video" first, then "Use as
   new photo" becomes "Reuse", then both become their icons (phones, the
   /designs grid) while Download keeps its label as the main action, and
   on the narrowest card (a 320px phone) Download folds too. Every
   control keeps its title and its accessible name whole. */
.toolbar-label-short {
  display: none;
}

@container gcard (max-width: 36.5rem) {
  .toolbar-video-btn .toolbar-label-long {
    display: none;
  }

  .toolbar-video-btn .toolbar-label-short {
    display: inline;
  }
}

@container gcard (max-width: 33rem) {
  .toolbar-reuse .toolbar-label-long {
    display: none;
  }

  .toolbar-reuse .toolbar-label-short {
    display: inline;
  }
}

/* Icon circles (owner report from Safari, 2026-09-24): the ghost pill's
   ink sweeps in from the left over 460ms while its colour flips in
   200ms, which on a 46px circle left the icon a different colour from
   what was behind it for a beat, both ways. A circle's ink grows from
   the middle, under the icon, and the icon turns as the ink reaches it:
   the ink covers the icon about 50ms into its 360ms, in and out, so the
   colour change is a short step around then. */
.generation-toolbar .toolbar-icon-only {
  transition: transform 380ms var(--ease-out), box-shadow 380ms var(--ease-out), color 100ms linear 30ms;
}

.generation-toolbar .toolbar-icon-only::before {
  border-radius: 50%;
  transform: scale(0);
  transform-origin: 50% 50%;
  transition: transform 360ms var(--ease-out);
}

.generation-toolbar .toolbar-icon-only:hover::before {
  transform: scale(1);
}

@container gcard (max-width: 28.5rem) {
  .generation-toolbar {
    gap: var(--space-1);
  }

  /* Folded to circles, the reuse and video buttons take the circle's
     ink too. */
  .generation-toolbar .toolbar-fold {
    transition: transform 380ms var(--ease-out), box-shadow 380ms var(--ease-out), color 100ms linear 30ms;
  }

  .generation-toolbar .toolbar-fold::before {
    border-radius: 50%;
    transform: scale(0);
    transform-origin: 50% 50%;
    transition: transform 360ms var(--ease-out);
  }

  .generation-toolbar .toolbar-fold:hover::before {
    transform: scale(1);
  }

  .toolbar-fold .toolbar-label-short {
    display: none;
  }

  .generation-toolbar .toolbar-fold {
    width: 2.875rem;
    padding: 0;
  }
}

@container gcard (max-width: 20.5rem) {
  .toolbar-download span {
    display: none;
  }

  .generation-toolbar .toolbar-download {
    width: 2.875rem;
    padding: 0;
  }

  /* A dark circle does not lift on hover (owner report from Safari,
     2026-09-24): the pill's 2px rise read as the icon jumping, next to
     circles that stay put. The bloom still answers the pointer. */
  .generation-toolbar .toolbar-download:hover {
    transform: none;
  }
}

.generations-empty {
  border: 1.5px dashed var(--line);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  color: var(--ink-soft);
}

/* The render wait: an architect's-ruler progress bar (OVERHAUL era,
   2026-07-24; replaces the spinner, which read as stuck). The track
   carries scale ticks every 10%, the fill is the IKB accent with a
   moving sheen so motion never dies even while the percentage crawls,
   and the stage line narrates the design brief in mono. */

.generation-pending {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4);
}

.render-progress {
  display: grid;
  gap: 0.45em;
}

.render-progress-track {
  height: 10px;
  border: 1.5px solid var(--line-strong);
  background:
    repeating-linear-gradient(90deg,
      transparent 0, transparent calc(10% - 1px),
      var(--line) calc(10% - 1px), var(--line) 10%),
    var(--paper-deep);
}

.render-progress-fill {
  position: relative;
  height: 100%;
  width: 0%;
  background: var(--accent);
  transition: width 0.5s linear;
  overflow: hidden;
}

/* The pen head: a paper hairline at the fill's leading edge with a soft
   glow, so the bar reads as a line being drawn rather than a box
   filling (frontend-design pass 2026-09-03). */
.render-progress-fill::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: 2px;
  background: var(--paper);
  box-shadow: -2px 0 8px rgba(251, 251, 248, 0.9);
  z-index: 1;
}

.render-progress-fill::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
    transparent 30%, rgba(251, 251, 248, 0.45) 50%, transparent 70%);
  transform: translateX(-100%);
  animation: render-sheen 1.8s ease-in-out infinite;
}

@keyframes render-sheen {
  to { transform: translateX(100%); }
}

.render-progress-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3);
  margin: 0;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.75rem;
}

.render-progress-stage {
  color: var(--ink);
  opacity: 0;
  transition: opacity 0.4s ease;
}

.render-progress-stage.is-in {
  opacity: 1;
}

/* The drafting caret after the stage line: the brief is being written. */
.render-progress-stage::after {
  content: "";
  display: inline-block;
  width: 0.55em;
  height: 0.95em;
  margin-left: 0.4em;
  vertical-align: -0.12em;
  background: var(--accent);
  animation: caret-blink 1.1s steps(2, start) infinite;
}

@keyframes caret-blink {
  to { visibility: hidden; }
}

/* The drafting stage: the customer's photo, slightly stilled, with a
   scan line travelling side to side, the plotter passing over their
   room (owner call 2026-09-10: across, not down). The line is a
   full-size layer carrying a 2px hairline at its centre inside a soft
   IKB halo, translated by half its own width each way, so the motion
   stays on the compositor. The z-index is
   load-bearing: the photo's filter makes it a stacking context painted
   in tree order, so without one the later sibling covered the line (it
   shipped invisible on 2026-09-03; caught by the owner 2026-09-10). */
.render-stage {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  max-height: 15rem;
  border: 1.5px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--ink);
}

.render-stage img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.55) brightness(0.82);
}

.render-stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  /* The halo is the accent (43, 60, 255) at low alpha, fading over a
     tenth of the stage each side of the line; the line is accent-soft,
     which reads on dark rooms and light ones alike. */
  background: linear-gradient(90deg,
    transparent 40%,
    rgba(194, 65, 12, 0.3) calc(50% - 1px),
    var(--accent-soft) calc(50% - 1px), var(--accent-soft) calc(50% + 1px),
    rgba(194, 65, 12, 0.3) calc(50% + 1px),
    transparent 60%);
  transform: translateX(-47%);
  animation: stage-pen 3.6s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
  will-change: transform;
}

/* The turn happens a hair inside the frame: the ease dwells at the ends,
   and at the exact edge the line dwelt under the border. */
@keyframes stage-pen {
  from { transform: translateX(-47%); }
  to { transform: translateX(47%); }
}

.render-progress-pct {
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

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

  .render-progress-fill::after {
    animation: none;
  }

  .render-stage::before,
  .render-progress-stage::after {
    animation: none;
  }

  .render-progress-stage {
    transition: none;
  }
}

/* Lightbox: the expanded view of a finished render */

.lightbox {
  /* The global `* { margin: 0 }` reset kills the UA rule that centers
     native dialogs; margin: auto restores it. */
  margin: auto;
  border: var(--border-hard);
  border-radius: var(--radius-lg);
  background: var(--paper);
  padding: var(--space-3);
  max-width: min(92vw, 70rem);
  box-shadow: var(--shadow-deep);
}

.lightbox::backdrop {
  background: rgba(16, 16, 16, 0.72);
  backdrop-filter: blur(4px);
}

.lightbox img {
  max-width: 100%;
  max-height: 76vh;
  width: auto;
  height: auto;
  margin-inline: auto;
  border-radius: var(--radius-md);
}

.lightbox figcaption {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  color: var(--ink-soft);
  padding-top: var(--space-2);
}

.lightbox-actions {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

/* Delete is the card's one destructive action (2026-09-02): it sits at
   the end of the action row, pushed right and toned down, so the four
   working actions keep their weight and the fifth reads as an exit.
   button_to wraps the button in a form, and the form is the flex item. */
.generation-actions .generation-delete-form {
  margin-left: auto;
}

.generation-actions .generation-delete {
  color: var(--ink-soft);
  /* Icon only (owner call 2026-09-09): a square the height of its text
     neighbors, the glyph centered, so the row keeps one baseline. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* A circle the height of its pill neighbors (owner report 2026-09-15:
     the pill's minimum height made a tall oval around the glyph). */
  width: 2.875rem;
  height: 2.875rem;
  min-height: 0;
  padding: 0;
  border-radius: 50%;
  line-height: 1;
}

/* 1em glyph: with the same padding and border as the ghost neighbors the
   square lands on their 37px height, so the row keeps one baseline. */
.generation-actions .generation-delete .trash-icon {
  width: 1.125rem;
  height: 1.125rem;
  display: block;
}

/* A quiet hover, not the ghost pill's ink fill: the fill met the icon's
   ink colour and the button went solid black (owner report 2026-09-15). */
.generation-actions .generation-delete::before { display: none; }

.generation-actions .generation-delete:hover,
.generation-actions .generation-delete:focus-visible {
  color: var(--ink);
  background: var(--tint);
  border-color: var(--ink);
  box-shadow: none;
  transform: none;
}

/* The held render (REVEAL.md decision 4, 2026-09-03): the server-side
   blurred preview under the reveal panel. The blur is in the file, so
   the overlay is presentation, not protection; the scale hides the
   blur's soft edges. */

.generation-locked {
  position: relative;
  overflow: hidden;
  background: var(--ink);
  min-height: 14rem;
}

.generation-locked-photo {
  display: block;
  width: 100%;
  height: auto;
  /* The cap (2026-09-07): a 9:19.5 phone render ran about 1,190px
     tall in the desktop canvas and pushed the reveal panel below a
     laptop's fold. Capped to the viewport the card crops to its middle
     band, which the panel covers anyway; the floor keeps the panel
     inside the card on a short landscape phone. */
  max-height: max(70vh, 22rem);
  object-fit: cover;
  transform: scale(1.04);
}

.generation-locked-overlay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--space-4);
  background: rgba(16, 16, 16, 0.22);
}

.generation-locked-panel {
  background: var(--paper-card);
  border: var(--border-hard);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  padding: var(--space-4) var(--space-5);
  display: grid;
  gap: var(--space-2);
  justify-items: center;
  text-align: center;
  max-width: 20rem;
}

.generation-locked-title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  line-height: 1.15;
}

.generation-locked-copy {
  font-size: var(--text-sm);
  color: var(--ink-soft);
}

.generation-locked-fine {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  color: var(--ink-soft);
}

/* In the grids (the home's recent designs and /designs, B17) a card is
   about 305px wide and its picture about 225px tall, shorter than the
   full panel: the fine print goes and the panel tightens, so the button
   stays on the picture. The workspace's wide card keeps both. */
.designs-grid .generation-locked-panel {
  padding: var(--space-3) var(--space-4);
}

.designs-grid .generation-locked-fine {
  display: none;
}


/* The padlock on the locked panel (2026-09-03): drawn in the icon
   family, accent, one size step above the title. */
.generation-locked-panel .lock-icon {
  width: 1.75rem;
  height: 1.75rem;
  color: var(--accent);
}

/* The scratch reveal (SCRATCH.md, 2026-09-10): the blurred preview under
   a canvas the visitor scratches. The stage carries the edge-hiding
   scale so the canvas and the photo share one box; the panel waits,
   hidden, until the controller adds is-asked (the budget spent, or the
   idle and no-touch timers). A spent card arrives with is-asked set. */

.generation-scratch-stage {
  position: relative;
  line-height: 0;
  transform: scale(1.04);
}

.is-scratch .generation-locked-photo {
  transform: none;
}

.generation-scratch-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  touch-action: none;
  cursor: pointer;
}

.is-asked .generation-scratch-canvas {
  pointer-events: none;
  cursor: default;
}

.generation-scratch-hint {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: rgba(255, 255, 255, 0.92);
  color: var(--ink);
  border: var(--border-hard);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.75rem;
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 200ms ease;
}

.generation-scratch-hint svg {
  width: 1.25rem;
  height: 1.25rem;
  color: var(--accent);
  animation: scratch-nudge 1.6s ease-in-out infinite;
}

@keyframes scratch-nudge {
  0%, 100% { transform: translate(0, 0); }
  50% { transform: translate(4px, -3px); }
}

.is-touched .generation-scratch-hint,
.is-asked .generation-scratch-hint {
  opacity: 0;
}

/* The keyboard path (decision 8): hidden until it has focus, then a
   ghost button in the card's corner. */
.generation-scratch-skip {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  border: 0;
  padding: 0;
  margin: 0;
  background: transparent;
}

.generation-scratch-skip:focus-visible {
  width: auto;
  height: auto;
  clip-path: none;
  padding: 0.6em 0.9em;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--ink);
  background: var(--paper-card);
  border: var(--border-hard);
  border-radius: var(--radius-md);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The panel waits while the budget lasts. */
.is-scratch:not(.is-asked) .generation-locked-overlay {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 400ms ease, visibility 0s linear 400ms;
}

.is-scratch .generation-locked-overlay {
  transition: opacity 400ms ease;
}

.is-scratch .generation-locked-panel {
  transform: translateY(10px);
  transition: transform 400ms ease;
}

.is-scratch.is-asked .generation-locked-panel {
  transform: none;
}

/* The panel's line follows the visitor (owner picks 2026-09-10): the plain
   offer until they scratch, "finish uncovering" once they have. Both lines
   share one cell so the panel is one size in every state. */
.generation-locked-copies {
  display: grid;
  justify-items: center;
}

.generation-locked-copies > .generation-locked-copy {
  grid-area: 1 / 1;
}

.generation-locked-copies .line-scratched {
  visibility: hidden;
}

.has-scratched .generation-locked-copies .line-untouched {
  visibility: hidden;
}

.has-scratched .generation-locked-copies .line-scratched {
  visibility: visible;
}

/* The button says what happens next: reveal before the budget is spent,
   the rest after. */
.generation-locked-panel .btn-rest {
  display: none;
}

.is-spent .generation-locked-panel .btn-reveal {
  display: none;
}

.is-spent .generation-locked-panel .btn-rest {
  display: inline;
}

@media (prefers-reduced-motion: reduce) {
  .generation-scratch-hint svg { animation: none; }
  .generation-scratch-hint,
  .is-scratch .generation-locked-overlay,
  .is-scratch .generation-locked-panel { transition: none; }
}

/* Narrow phones (2026-09-10, found in the scratch QC at 375): a 4:3 card
   is 249px tall there and the panel with a two-line copy ran 18px past
   it, clipping the fine print. Same panel, one size step down. */
@media (max-width: 480px) {
  .generation-locked-overlay {
    padding: var(--space-3);
  }

  .generation-locked-panel {
    padding: var(--space-3) var(--space-4);
    gap: var(--space-1);
  }

  .generation-locked-panel .lock-icon {
    width: 1.25rem;
    height: 1.25rem;
  }

  .generation-locked-title {
    font-size: var(--text-base);
  }

  .generation-locked-copy {
    font-size: var(--text-xs);
  }

  .generation-locked-panel .btn {
    font-size: var(--text-sm);
    padding: 0.8em 1.3em;
  }

}

/* The reveal screen (RELAUNCH.md section 2 step 5): one card, the width
   of the narrow column, under its heading. The 34rem cap is the studio
   canvas's.

   The card is the page's plate (owner call 2026-09-16: the studio card
   dropped in at column width read as unfinished). The photo runs edge to
   edge in every state: the wait's stage fills the column under the pen
   line, so the design lands in the same frame it was drawn in, and the
   ruler and the stage line sit in the caption band below. While the
   design is for sale the caption keeps the style name and the before
   and loses the studio's timestamp and delete; the card's own wait line
   goes too, the page head already says it. */
.reveal-page .generations.reveal-card { max-width: none; }

.reveal-page .generation-card {
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-deep);
}

.reveal-page .generation-pending {
  padding: 0 0 var(--space-4);
  gap: var(--space-3);
}

.reveal-page .generation-pending .render-stage {
  order: -1;
  max-height: none;
  aspect-ratio: auto;
  border: 0;
  border-radius: 0;
}

.reveal-page .generation-pending .render-stage img {
  height: auto;
  max-height: max(70vh, 22rem);
}

.reveal-page .generation-pending > :not(.render-stage) {
  margin-inline: var(--space-4);
}

.reveal-page .generation-pending .generation-note { display: none; }

.reveal-page .generation-locked + .generation-meta .generation-when,
.reveal-page .generation-locked + .generation-meta .generation-actions { display: none; }

/* The subscriber's one action, clear of the card (owner report
   2026-09-16: the button sat against it). */
.reveal-actions {
  display: flex;
  justify-content: center;
  margin-top: var(--space-5);
}
