/* Palette and type tokens live in packages/tokens/tokens.css so the marketing
   site (apps/site) can share them without depending on this whole file. */
* {
  box-sizing: border-box;
}

html {
  /* Reserve the scrollbar's width whether or not it's needed. Without this,
     switching to a shorter format (e.g. notecard) can drop the vertical
     scrollbar, widening the viewport by its ~15px and nudging every
     centered element sideways — including the recipe-detail header, which
     is otherwise pinned to a fixed width across formats. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
}

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

::selection {
  background: color-mix(in srgb, var(--accent) 35%, var(--bg));
  color: var(--fg);
}

input,
textarea {
  caret-color: var(--accent);
}

.skip-link {
  position: absolute;
  top: -3rem;
  left: 1rem;
  z-index: 10;
  padding: 0.5rem 0.875rem;
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--line);
}

.skip-link:focus {
  top: 1rem;
}

.app-shell {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding: var(--space-2) clamp(var(--space-4), 4vw, 3rem);
  border-bottom: 1px solid var(--divider);
}

.app-header .brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  letter-spacing: -0.01em;
}

.app-header .brand:hover .brand-word {
  color: var(--accent);
}

.app-header .brand .brand-mark {
  flex: none;
  /* The mark is a fixed stamp, not a second interactive color, see
     --accent2's Named Rule in DESIGN.md. */
  color: var(--accent2);
}

/* The wordmark beside the compact mark: per brand-mark.tsx, the compact
   ring drops its own lettering because the wordmark is meant to carry the
   name at header size. Plain --fg, not --accent2: that color is reserved
   for the mark itself (see .brand-mark above). */
.app-header .brand .brand-word {
  font-size: 1.25rem;
  transition: color 0.15s ease;
}

.app-header nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.nav-toggle {
  display: none;
}

.app-header a,
.app-footer a {
  color: inherit;
  text-decoration: none;
}

.app-header nav a:hover {
  color: var(--accent);
}

.app-header nav a[aria-current="page"] {
  color: var(--accent-strong);
  font-weight: var(--font-heading-weight);
}

/* Utility nav items (theme toggle, sign out) borrow the Dateline/meta voice
   (DESIGN.md Typography) rather than inventing a new size step, so the
   brand and the primary action still lead the header. */
.app-header .nav-secondary {
  border: none;
  background: transparent;
  padding: 0;
  gap: 0;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

.app-header .nav-secondary:hover {
  border-color: transparent;
  color: var(--accent);
}

/* .app-header a { color: inherit } otherwise outranks .btn-primary by
   specificity, silently dropping the fill button back to header ink text. */
.app-header .btn-primary,
.app-header .btn-primary:hover {
  color: var(--accent-contrast);
}

@media (max-width: 40rem) {
  .app-header {
    position: relative;
    z-index: 20;
    flex-wrap: nowrap;
  }

  .app-header .brand-word {
    display: none;
  }

  .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border-color: var(--line);
  }

  /* Collapses into a dropdown panel anchored under the header, so the logo
     and nav stop competing for the same row and never stack vertically. */
  .app-header nav {
    display: none;
    position: absolute;
    top: 100%;
    right: 0;
    left: 0;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    padding: var(--space-3) clamp(var(--space-4), 4vw, 3rem);
    background: var(--bg);
    border-bottom: 1px solid var(--divider);
  }

  .app-header nav[data-open="true"] {
    display: flex;
  }

  .app-header nav a,
  .app-header nav button {
    width: 100%;
  }

  .app-header .nav-secondary {
    font-size: 0.875rem;
    padding: 0.5rem 0;
  }
}

.app-main {
  flex: 1;
  padding: clamp(2rem, 6vw, 4rem) clamp(1rem, 4vw, 3rem);
}

.app-footer {
  padding: 1rem clamp(1rem, 4vw, 3rem);
  border-top: 1px solid var(--divider);
  color: var(--muted);
  font-size: 0.8125rem;
}

.stack {
  max-width: 42rem;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

h1,
h2,
h3 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
}

h1 {
  margin: 0;
  font-size: clamp(1.75rem, 4vw, 2.5rem);
  letter-spacing: -0.02em;
}

.lede {
  margin: 0;
  color: var(--muted);
  font-size: 1.0625rem;
  font-style: italic;
}

.muted {
  color: var(--muted);
  font-size: 0.8125rem;
  font-family: var(--font-mono);
}

.source-link {
  display: inline-flex;
  align-items: baseline;
  gap: 0.25em;
}

.source-link svg {
  align-self: center;
  flex-shrink: 0;
}

.checks {
  margin: 0;
  display: grid;
  gap: 0.5rem;
}

.check {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--divider);
  border-radius: var(--radius);
}

.check dt {
  color: var(--muted);
  font-family: var(--font-mono);
}

.check dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

.check.ok dd {
  color: var(--ok);
}

.check.warn dd {
  color: var(--warn);
}

a {
  color: var(--accent);
}

button,
.button-link {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.5rem 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--fg);
  font: inherit;
  cursor: pointer;
  text-decoration: none;
}

button:hover,
.button-link:hover {
  border-color: var(--accent);
  color: var(--accent);
}

button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* The one filled control in an otherwise flat, hairline-bordered system,
   reserved for the single primary action on a page (e.g. "+ New recipe").
   Filled with --accent-strong, not --accent: white text on the base light-
   theme cyan measures only 4.08:1, below the 4.5:1 floor for this text
   size; the deeper step measures 6.4:1. Dark theme's --accent-strong is
   the same pale step as --accent (see :root[data-theme="dark"]), already
   paired with dark --accent-contrast text at ~9:1. */
.btn-primary {
  border-color: var(--accent-strong);
  background: var(--accent-strong);
  color: var(--accent-contrast);
  font-weight: var(--font-heading-weight);
}

.btn-primary:hover {
  border-color: var(--accent-strong);
  color: var(--accent-contrast);
  opacity: 0.88;
}

input,
select,
textarea {
  font: inherit;
  padding: 0.5rem 0.625rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
}

/* The OS-drawn "Choose files" control ignores the input's own border/padding,
   so it's styled to match the app's button system directly rather than left
   at browser default. */
input[type="file"] {
  padding: 0.375rem;
}

input[type="file"]::file-selector-button {
  font: inherit;
  margin-right: 0.625rem;
  padding: 0.5rem 0.875rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--fg);
  cursor: pointer;
}

input[type="file"]::file-selector-button:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.form-error {
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--warn);
  border-radius: var(--radius);
  color: var(--warn);
}

.field {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  flex: 1;
}

.field span {
  font-size: 0.75rem;
  color: var(--muted);
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.field-row {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.field-group {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem;
}

.field-group > * + * {
  margin-top: 0.75rem;
}

.field-group legend {
  padding: 0 0.375rem;
  color: var(--muted);
  font-size: 0.75rem;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.recipe-form {
  max-width: 48rem;
}

.form-actions {
  display: flex;
  gap: 0.75rem;
}

.draft-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.draft-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.draft-row .drag-handle {
  cursor: grab;
  color: var(--muted);
}

.draft-row .qty {
  width: 4rem;
}

.draft-row .unit {
  width: 5rem;
}

.draft-row .name {
  flex: 2;
}

.draft-row .group {
  flex: 1.5;
}

.draft-row .step-number {
  color: var(--muted);
  min-width: 1.5rem;
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.draft-row .step-text {
  flex: 1;
  resize: vertical;
}

.remove-row,
.row-move {
  border: none;
  padding: 0.25rem 0.5rem;
  color: var(--muted);
}

.row-actions {
  display: flex;
  flex-direction: column;
}

.row-move:disabled {
  opacity: 0.35;
}

.checkbox-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
}

.checkbox-option {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.9375rem;
}

.category-picker {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.category-chip-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.category-chip {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.25rem 0.5rem 0.25rem 0.625rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 0.875rem;
}

.category-chip button {
  border: none;
  background: none;
  padding: 0;
  font-size: 1rem;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
}

.category-chip button:hover {
  color: var(--fg);
}

.category-combobox {
  position: relative;
}

.category-listbox {
  position: absolute;
  z-index: 1;
  top: calc(100% + 0.25rem);
  left: 0;
  right: 0;
  margin: 0;
  padding: 0.25rem;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  max-height: 14rem;
  overflow-y: auto;
}

.category-listbox li {
  padding: 0.375rem 0.5rem;
  border-radius: var(--radius);
  cursor: pointer;
}

.category-listbox li.active {
  background: var(--accent);
  color: var(--accent-contrast);
}

.photo-preview {
  max-width: 20rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
}

/* Recipe photo, shared across every display format (see
   formats/recipe-photo.tsx). Each format frames the same image differently,
   so only the things true everywhere live here: never wider than its
   container, and never stretched. The rest is per-variant, in each
   format's own section below. */
.recipe-photo {
  display: block;
  max-width: 100%;
  object-fit: cover;
}

/* One treatment for both reading views, so switching between them never
   reframes the dish. 34rem is cook mode's own column width (.cook-mode), so
   the photo is literally the same size in each: the traditional view's
   column is wider, and a 42rem-wide 3:2 hero would swamp the recipe under
   it. The ratio is explicit rather than a max-height, because clamping
   height and letting object-fit sort it out gives a crop box whose shape
   follows the photo's, cropping a portrait shot to its middle while leaving
   a wide one untouched. */
.recipe-photo-traditional,
.recipe-photo-cook {
  width: min(100%, 34rem);
  aspect-ratio: 3 / 2;
  border-radius: var(--radius);
  border: 1px solid var(--line);
}

/* .recipe-detail overrides .stack's 42rem cap: on a wide or ultrawide
   monitor, traditional and flowchart both have real use for the extra
   width (the flowchart especially, since every inch here is an inch it
   doesn't need to horizontal-scroll for). The header (see
   .recipe-detail-header) always spans this full width regardless of
   format, and cook mode / the notecard keep their own narrower widths
   regardless (see their max-width rules) — this only frees up
   traditional and flowchart. */
.recipe-detail {
  max-width: min(78rem, 94vw);
}

.recipe-list-header,
.recipe-detail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.recipe-list-header {
  width: 100%;
}

.recipe-detail-header {
  /* Always .recipe-detail's full width, deliberately, so the format
     picker and the action buttons sit in the same place on screen no
     matter which format is active: .recipe-detail's own width doesn't
     change with the format (only the content below it does), so this
     keeps the header's left/right edges — and the picker/actions
     anchored to them via justify-content: space-between — fixed too.
     Narrower formats (34–58rem of centered content below a full 78rem
     header) read as extra open space beside the buttons rather than
     matching their width, which is the traded-off look: the alternative
     was the buttons shifting position on every format switch. */
  width: 100%;
}

@media (max-width: 40rem) {
  /* A flex row can't reliably cap the picker's width: a flex item's basis
     falls back to its content's intrinsic size (for a <select>, that's
     driven by its widest *option*, e.g. a long unavailable-reason string)
     whenever the percentage width it's given can't resolve against a
     definite parent size, and `flex-wrap` then wraps the actions to a new
     line rather than shrinking the picker to fit. A grid track sized
     `minmax(0, 1fr)` has no such fallback: it is always capped by the
     track size, so the picker (and the select inside it) is forced to fit
     the remaining row regardless of how long an option's text gets. */
  .recipe-detail-header {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.5rem;
  }

  .recipe-detail-actions {
    flex-wrap: nowrap;
  }
}

.recipe-list-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Homepage: recipe library, index/TOC ------------------------------------
   The front page of the book: chapters (the household's own categories)
   numbered and named like a printed cookbook's table of contents, recipes
   listed under each as index entries. Direction 1b/2a of
   design_handoff_recipe_library; see index.html's impeccable:direction
   comment. */

.home {
  max-width: 64rem;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.home-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.home-toolbar input {
  flex: 1;
  min-width: 12rem;
}

.home-actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* The page itself: bounded and filled a step off the surrounding ground,
   so the masthead and index read as a leaf sitting on a table rather than
   floating loose in the app shell. Border-only, no shadow, per this
   system's flat elevation model. */
.home-page {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--space-2) var(--space-8) var(--space-8);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

@media (max-width: 40rem) {
  .home-page {
    padding: var(--space-2) var(--space-4) var(--space-6);
  }
}

/* Cookbook detail reuses .home-page's look but isn't wrapped in .home, so
   the page-width cap has to be repeated here to match the homepage. */
.cookbook-detail-page {
  max-width: 64rem;
  margin: 0 auto;
  width: 100%;
}

.home-masthead {
  display: flex;
  flex-direction: column;
}

/* The nameplate: the page's own title, set like a newspaper masthead
   name sitting directly above the rule it brackets with the dateline. */
.home-nameplate {
  margin: 0 0 var(--space-2);
  font-size: clamp(1.75rem, 1.4rem + 1.5vw, 2.5rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  text-align: center;
}

/* A thick-then-thin double rule, no gap: one <hr>, both borders. */
.home-rule-double {
  height: 0.3125rem;
  margin: 0;
  border: 0;
  border-top: 2px solid var(--fg);
  border-bottom: 1px solid var(--fg);
}

.home-rule-thin {
  height: 0;
  margin: 0;
  border: 0;
  border-top: 1px solid var(--fg);
}

.home-dateline {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem var(--space-6);
  margin: 0;
  padding: var(--space-2) 0;
  font-size: 0.75rem;
  font-style: italic;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

.home-chapters {
  display: flex;
  flex-direction: column;
  padding-top: var(--space-3);
}

.home-chapter {
  margin-bottom: var(--space-6);
  animation: home-chapter-rise 0.4s ease both;
  animation-delay: calc(var(--stagger, 0) * 60ms);
}

@keyframes home-chapter-rise {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .home-chapter {
    animation: none;
  }
}

.home-chapter-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.home-chapter-num {
  font-size: 1.25rem;
  font-family: var(--font-heading);
  font-variant-numeric: oldstyle-nums;
  color: var(--accent-strong);
}

.home-chapter-name {
  margin: 0;
  font-size: 1.375rem;
}

.home-chapter-count {
  font-size: 0.75rem;
  /* 70%, not the handoff's literal 55%: measured contrast at 55% is
     3.65:1 against the light ground, below the 4.5:1 AA floor for text
     this size. 70% (matching .home-dateline) measures 5.81:1. */
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

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

.home-index-row {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  color: inherit;
  text-decoration: none;
  margin: 0 0 var(--space-2);
}

/* Halftone dot-screen: a duotone filter plus a fine repeating dot overlay,
   standing in for the handoff's `.halftone` treatment (there is no real
   image-processing step in v1, see PRODUCT.md's Evidence on Hand). */
.home-index-thumb {
  position: relative;
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  margin-top: 0.125rem;
  overflow: hidden;
  border-radius: var(--radius);
}

.home-index-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(0.4) contrast(1.15);
}

.home-index-thumb::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, var(--bg) 0.5px, transparent 1px);
  background-size: 3px 3px;
  mix-blend-mode: overlay;
  opacity: 0.6;
}

.home-index-body {
  flex: 1;
  min-width: 0;
}

.home-index-title-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0;
  font-size: 0.96875rem;
  line-height: 1.75rem;
}

.home-index-title {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.home-index-leader {
  flex: 1;
  min-width: 1.75rem;
  align-self: flex-end;
  margin-bottom: 0.34em;
  border-bottom: 1px dotted color-mix(in srgb, var(--fg) 45%, transparent);
}

.home-index-time {
  font-size: 0.9375rem;
  font-family: var(--font-heading);
  font-variant-numeric: oldstyle-nums;
  white-space: nowrap;
}

.home-index-servings {
  margin: 0;
  font-size: 0.75rem;
  /* 70%, not the handoff's literal 55%, see .home-chapter-count. */
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

.home-index-row:hover .home-index-title {
  color: var(--accent);
}

@media (min-width: 40rem) {
  /* Desktop has room the fixed 2.5rem thumb wastes; mirrors the mobile
     shrink below so the row still reads as one baseline-aligned line. */
  .home-index-thumb {
    width: 7rem;
    height: 7rem;
  }
}

@media (max-width: 40rem) {
  /* The handoff's mobile dateline drops Updated/Sort, keeping only the
     count fields. */
  .home-dateline-desktop-only {
    display: none;
  }

  .home-chapter-num {
    font-size: 0.8125rem;
  }

  .home-chapter-name {
    font-size: 1.125rem;
  }

  .home-index-thumb {
    width: 2rem;
    height: 2rem;
  }

  .home-index-title-row {
    font-size: 0.84375rem;
    line-height: 1.5rem;
    gap: var(--space-1);
  }

  .home-index-time {
    font-size: 0.78125rem;
  }

  .home-index-servings {
    font-size: 0.65625rem;
  }
}

.recipe-detail-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.recipe-detail-actions button,
.recipe-detail-actions .button-link {
  /* Matches .format-picker-options's min-height — see its comment. */
  min-height: 2.75rem;
}

.confirm-delete,
.confirm-inline {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  color: var(--warn);
  font-size: 0.875rem;
}

@media (max-width: 40rem) {
  /* Icon-only actions: four labeled buttons ("Made this", "Edit
     flowchart", ...) wrapped onto several full-width-feeling rows on a
     phone. The label stays in the accessible name (sr-only, not
     display:none) and resurfaces as a native hover tooltip via `title`. */
  .recipe-detail-actions .action-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .recipe-detail-actions button,
  .recipe-detail-actions .button-link {
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    justify-content: center;
  }

  .confirm-delete button {
    width: auto;
    height: auto;
    padding: 0.5rem 0.875rem;
  }

  /* With the other action buttons hidden during delete confirmation (see
     recipe-detail.tsx), .recipe-detail-actions holds only the confirm bar —
     drop back to one column so it (and the format picker above it) both
     stretch to the header's full width instead of leaving the picker
     short by the now-unused second column's track and gap. */
  .recipe-detail-header:has(.confirm-delete) {
    grid-template-columns: 1fr;
  }

  .confirm-delete {
    width: 100%;
    justify-content: space-between;
  }

  .confirm-delete-buttons {
    display: flex;
    gap: 0.5rem;
  }
}

/* The header (format picker + actions) spans .recipe-detail's full width
   (see its own rule) so the picker and the action buttons have room to
   share one line. Everything below it narrows to its own format's
   natural width and centers under that full-width header rather than
   pinning to either edge. */
.recipe-detail-content[data-format="cook-mode"],
.recipe-detail-content[data-format="notecard"] {
  /* Explicit width, not just max-width: as a flex item (child of .stack),
     auto cross-axis margins disable the default stretch-to-fill behavior,
     leaving width to fall back to shrink-to-fit content. The notecard's
     font sizes are all `cqw` off its own container
     (.notecard-frame — see its comment), so a shrunk container here
     cascades into a collapsed, tiny card. width:100%-then-clamped keeps
     this a normal, definite-size box instead. */
  width: 100%;
  max-width: 34rem;
  margin: 0 auto;
}

/* Both formats carry their own .stack class (for the gap/column layout),
   which caps them at .stack's base 42rem — override the width here so
   they can use the space .recipe-detail now makes available. .stack's own
   margin: 0 auto is left as-is, which is what centers them under the
   full-width header above. Traditional stays bounded well short of
   .recipe-detail's own max: an ingredients/steps grid that wide would
   stretch line length past comfortable reading. Flowchart gets the full
   width, since the chart has real use for it and already scrolls its own
   box (.flowchart-scroll) rather than reading line by line. */
/* Matches .recipe-detail's own cap exactly (see .recipe-detail-header,
   which always spans that full min(78rem, 94vw)), so the sheet's edges
   line up with the button row above it instead of leaving a gap on an
   ultrawide screen. The two-column ingredients/steps split (see
   .recipe-detail-body) still keeps each column's own line length
   reasonable at this width — it's the page that widens, not the measure
   a single column of prose runs to. */
.format-traditional {
  max-width: min(78rem, 94vw);
}

.format-flowchart {
  max-width: 100%;
}

/* The traditional format's defining move: a physical page rather than a
   bare content block. Surface (not Ground) so it reads as a sheet sitting
   on the page, the same hairline-plus-shadow treatment .notecard-frame
   already established as this system's one sanctioned elevation exception
   (see DESIGN.md's Elevation & Depth). Margins are asymmetric — generous
   top/bottom, narrower sides — because a printed page's margins never
   match a screen's default padding. */
.recipe-page {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-8) var(--space-6);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.08);
}

.recipe-page > * + * {
  margin-top: 1rem;
}

.recipe-page > h1:first-child {
  margin-top: 0;
}

/* Centered at its own shared size (see .recipe-photo-traditional's
   min(100%, 34rem)/3:2 cap), not stretched to the sheet's width. A
   full-bleed plate looked right at the page's old 50rem measure, but
   widening the sheet to close the gap under the header buttons (see
   .format-traditional) meant stretching to full width blew the photo up
   past its designed size — worst for a lower-resolution upload, which
   just goes soft at that scale rather than reading as a deliberately
   large plate. Capping and centering keeps the photo the same size here
   as in cook mode regardless of how wide the page around it gets. */
.recipe-page-photo {
  display: flex;
  justify-content: center;
}

/* Running head: which chapter this page belongs to, set like a printed
   book's own page furniture — small caps over a hairline, not a UI
   label. Only the recipe's first category, deliberately: a true chapter
   *number* would have to replicate the home page's household-wide
   numbering (every tag, sorted, filtered to ones with a visible recipe —
   see home.tsx's buildChapters), which this page has no reason to
   duplicate or re-fetch just to print a numeral. */
.recipe-page-kicker {
  margin-top: 0;
}

.recipe-page-photo + .recipe-page-kicker {
  margin-top: 1.25rem;
}

.recipe-page-chapter {
  margin: 0 0 0.375rem;
  font-size: 0.8125rem;
  font-variant: small-caps;
  letter-spacing: 0.08em;
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

.recipe-page-rule {
  height: 0;
  margin: 0;
  border: 0;
  border-top: 1px solid var(--line);
}

/* The colophon: source credit and filing, moved to the foot of the page
   like a printed book's own back matter rather than UI chrome (a labeled
   "Source:" row, clickable pills) sitting above the fold. */
.recipe-page-colophon {
  border-top: 1px solid var(--line);
  padding-top: 0.75rem;
}

.recipe-page-filed {
  margin: 0;
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

.recipe-page-source {
  margin: 0.375rem 0 0;
  font-size: 0.875rem;
  font-style: italic;
  color: var(--muted);
}

/* A rule-bounded stats band standing in for a masthead dateline, reusing
   the home page's dateline voice (uppercase, tracked, 70% muted — see
   .home-dateline) rather than inventing a second small-caption treatment. */
.recipe-page-stats {
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: 0.5rem 0;
}

.recipe-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

.recipe-stats-empty {
  margin: 0;
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

/* Column gap is carried by padding + a rule on the second column rather
   than `gap`, so the vertical divider can sit exactly on the seam instead
   of floating in an ungapped void. */
.recipe-detail-body {
  display: grid;
  grid-template-columns: 1fr 2fr;
  column-gap: 0;
  row-gap: 2rem;
}

.recipe-detail-body > div:first-child {
  padding-right: 2rem;
}

.recipe-detail-body > div:last-child {
  padding-left: 2rem;
  border-left: 1px solid var(--line);
}

.recipe-detail-body h2 {
  font-size: 1.125rem;
}

.ingredient-group h3 {
  font-size: 0.9375rem;
  font-style: italic;
  font-variant: small-caps;
  color: var(--muted);
  margin-bottom: 0.25rem;
}

/* Hanging indent instead of a bullet: wrapped lines align back under the
   ingredient name rather than under the leading quantity. Oldstyle figures
   read as set type rather than UI text; the vulgar-fraction glyphs
   (formatQuantity) are already real characters, so diagonal-fractions only
   ever touches a plain digit ratio like a stray "1/2" in raw source text. */
.recipe-ingredient-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.recipe-ingredient-list li {
  padding-left: 1.1em;
  text-indent: -1.1em;
  font-variant-numeric: oldstyle-nums diagonal-fractions;
}

.recipe-ingredient-list li + li {
  margin-top: 0.375rem;
}

/* Hanging numerals in the margin, set in the same accent-strong step the
   home index uses for its chapter numbers, rather than a browser-default
   ::marker. */
.recipe-steps {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: recipe-step;
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
}

.recipe-steps li {
  counter-increment: recipe-step;
  display: grid;
  grid-template-columns: 1.75rem 1fr;
  gap: 0.625rem;
  hyphens: auto;
  text-wrap: pretty;
}

.recipe-steps li::before {
  content: counter(recipe-step);
  color: var(--accent-strong);
  text-align: right;
}

@media (max-width: 40rem) {
  .recipe-detail-body > div:first-child {
    padding-right: 0;
  }

  .recipe-detail-body > div:last-child {
    padding-left: 0;
    border-left: none;
  }
}

/* Print: the traditional format is the one page in the app actually meant
   to be printed, so it gets a real print stylesheet rather than just the
   chrome-hiding rules every page shares (see the shared `@media print`
   block). Ink-on-paper instead of the screen palette, the elevation
   dropped (a shadow is a screen-only illusion of a page; on actual paper
   the page doesn't need to look like a page), and the sheet allowed to
   run the full printable width instead of centering with the app's
   screen margins. */
@media print {
  .format-traditional {
    max-width: none;
  }

  .recipe-page {
    background: #fff;
    color: #000;
    border-color: #000;
    box-shadow: none;
    border-radius: 0;
  }

  .recipe-page-rule,
  .recipe-page-stats,
  .recipe-page-colophon,
  .recipe-detail-body > div:last-child {
    border-color: #000;
  }

  .recipe-page-chapter,
  .recipe-stats,
  .recipe-stats-empty,
  .recipe-page-filed,
  .ingredient-group h3,
  .recipe-page-source {
    color: #000;
  }

  .recipe-steps li::before {
    color: #000;
  }

  @page {
    margin: 0.75in;
  }
}

.category-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.category-pill {
  padding: 0.25rem 0.625rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 0.75rem;
  color: var(--muted);
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

/* A ul, so the browser's list indent and bullets have to go: the rows are
   meant to run the full width of the section they sit in. */
.settings-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Fixed tracks, not flex, because the row swaps content as it changes state.
   The action column is exactly two icon buttons wide (2rem each plus the
   0.5rem between them), so nothing shifts sideways when a row goes from
   reading to editing. The count column sets a floor for the same reason:
   "1 recipe" must not pull the icons in relative to "12 recipes". */
.settings-row {
  display: grid;
  grid-template-columns: 1fr minmax(5rem, auto) 4.5rem;
  align-items: center;
  gap: 0.75rem 1rem;
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--divider);
}

.settings-row:last-child {
  border-bottom: none;
}

.settings-row-name {
  min-width: 0;
}

.settings-row-count {
  color: var(--muted);
  font-size: 0.875rem;
  text-align: right;
}

/* The rename input stands in for the name and count together. Height is
   pinned to the icon buttons beside it (2rem) rather than left to the global
   input padding, which is a third taller and grew the row on every edit. */
.settings-row-edit {
  grid-column: 1 / 3;
  min-width: 0;
  height: 2rem;
  padding-top: 0;
  padding-bottom: 0;
}

/* The delete question and the error both belong to the row as a whole. */
.settings-row-confirm,
.settings-row .form-error {
  grid-column: 1 / -1;
}

.settings-row-actions {
  display: flex;
  gap: 0.5rem;
}

/* Two children, not three: keep the control in the action column so it lines
   up with the icon buttons in the sections below. */
.settings-row .switch {
  grid-column: 3;
  justify-self: end;
}

.settings-add {
  display: flex;
  gap: 0.5rem;
}

.settings-add input {
  flex: 1;
  min-width: 0;
}

/* Square icon-only button. The glyph is 16px and the box is 2rem, which keeps
   the border a quiet frame rather than a badge, and stays inside the same
   --radius as every other control. Names live on the button's aria-label. */
.icon-button {
  width: 2rem;
  height: 2rem;
  padding: 0;
  justify-content: center;
  color: var(--muted);
}

/* Deliberately a rectangle, not the usual 999px pill: the Nearly-Sharp Rule
   applies here like everywhere else, so the track and the knob both route
   through --radius. The knob slides rather than the track changing shape. */
.switch {
  width: 2.75rem;
  height: 1.5rem;
  padding: 2px;
  justify-content: flex-start;
  background: transparent;
  transition: background-color 120ms ease, border-color 120ms ease;
}

.switch[aria-checked="true"] {
  border-color: var(--accent-strong);
  background: var(--accent-strong);
}

/* The global button:hover recolors border and text; the knob carries this
   control's state instead, so hover only nudges the border, and the checked
   fill keeps its own border rather than reverting to the base accent. */
.switch:hover {
  border-color: var(--accent);
}

.switch[aria-checked="true"]:hover {
  border-color: var(--accent-strong);
}

.switch-knob {
  width: 1.125rem;
  height: 100%;
  border-radius: var(--radius);
  background: var(--muted);
  transition: transform 120ms ease, background-color 120ms ease;
}

.switch[aria-checked="true"] .switch-knob {
  background: var(--accent-contrast);
  transform: translateX(1.125rem);
}

@media (prefers-reduced-motion: reduce) {
  .switch,
  .switch-knob {
    transition: none;
  }
}

@media (max-width: 40rem) {
  .recipe-detail-body {
    grid-template-columns: 1fr;
  }

  /* Too many fixed-width columns to fit one row on a phone: let a draft row
     wrap, with the free-text fields claiming a full line each rather than
     being squeezed down to nothing. */
  .draft-row {
    flex-wrap: wrap;
  }

  .draft-row .name,
  .draft-row .group,
  .draft-row .step-text {
    flex-basis: 100%;
  }
}

/* Display formats ------------------------------------------------------- */

.format-picker {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  min-width: 0;
  max-width: 100%;
}

.format-picker-options {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  padding: 0.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* Matched to .recipe-detail-actions button/.button-link's own box
     height below: the two sit side by side in .recipe-detail-header and
     are individually shorter (smaller font/padding here, an extra
     wrapping border there), so without a shared height they land at
     very slightly different heights and align-items: center on the
     header splits that difference — center-aligned but visibly a hair
     off, rather than lining up edge to edge. */
  min-height: 2.75rem;
}

/* Mobile swaps the button row for a single native select: a scrolling
   button strip and a wrapped multi-row grid both read badly on a phone,
   and a select collapses to one line while keeping every option (and its
   unavailable-reason text) reachable without any of them taking up
   standing room. */
.format-picker-select {
  display: none;
}

@media (max-width: 40rem) {
  .format-picker-options {
    display: none;
  }

  .format-picker-select {
    display: block;
    width: 100%;
    /* A <select>'s intrinsic width follows its widest option, including
       ones only shown open (like a long unavailable-reason string). Flex
       items refuse to shrink below that content width by default, which
       overrides `width: 100%` above and pushes the action icons onto their
       own line; min-width: 0 removes that floor so 100% actually holds. */
    min-width: 0;
  }
}

.format-option {
  padding: 0.3125rem 0.75rem;
  border-color: transparent;
  font-size: 0.875rem;
}

.format-option[aria-pressed="true"],
.format-option-active {
  border-color: var(--accent);
  color: var(--accent);
}

.format-option:disabled:hover {
  border-color: transparent;
  color: var(--fg);
}

.format-option-star {
  color: var(--accent);
}

.format-option-wrap {
  display: flex;
}

.link-button {
  padding: 0;
  border: 0;
  color: var(--muted);
  font-size: 0.8125rem;
  text-decoration: underline;
}

.link-button:disabled {
  text-decoration: none;
}

.link-button:hover {
  border: 0;
}

/* Notecard: 4x6 card proportions, landscape.
   Everything inside the card is sized in `cqw` or `em` off the frame's width,
   so a narrow screen scales the whole card down uniformly instead of
   reflowing it, which is what keeps the content budget in notecard-fit.ts
   honest at every viewport. The budget is calibrated against these sizes;
   change them together and re-check with a max-budget recipe. */
.notecard-frame {
  container-type: inline-size;
  width: 100%;
  max-width: 34rem;
  margin: 0 auto;

  /* A real index card doesn't relight itself for the room: the notecard
     format stays on its own paper-card palette regardless of the app-wide
     theme, by shadowing the shared tokens for everything inside it. */
  --bg: #f6f1e7;
  --fg: #17140f;
  --muted: #6b6252;
  --line: #8f8370;
  --accent: #1c7266;
  --accent2: #b8402f;
  --ok: #417033;
  --warn: #b8402f;
  --rule: rgba(84, 104, 132, 0.16);
  --margin-rule: rgba(184, 64, 47, 0.32);
  color-scheme: light;
}

.notecard {
  position: relative;
  width: 100%;
  aspect-ratio: 6 / 4;
  display: flex;
  flex-direction: column;
  gap: 0.6em;
  /* Left padding clears the 2em margin rule below, so content sits right of
     it like real handwriting rather than crossing it. */
  padding: 1em 1.2em 1em 2.5em;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background-color: var(--bg);
  /* The red margin rule, like a real index card, spans the full card height
     (header included). The horizontal ruled lines live on `.notecard-body`
     instead of here: putting them on the whole card ran them straight
     through the header's own border-bottom, which looked like two
     competing dividers. */
  background-image: linear-gradient(
    to right,
    transparent 0,
    transparent 2em,
    var(--margin-rule) 2em,
    var(--margin-rule) calc(2em + 1px),
    transparent calc(2em + 1px),
    transparent 100%
  );
  background-repeat: no-repeat;
  color: var(--fg);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.08);
  font-size: 2.45cqw;
  /* In `em`, not unitless: computed once here to a fixed height and
     inherited as-is by every descendant, so a smaller element (a group
     heading, the meta line) gets the same line-box height as body text
     instead of its own shorter one. That's what keeps every line, not
     just same-size ones, on the ruled-line grid in `.notecard-body`. */
  line-height: 1.35em;
  overflow: hidden;
}

/* A library-card due-date stamp standing in for cook count: canted, inked in
   the alert accent, and set in the same mono face as a real ink stamp. */
.notecard-stamp {
  position: absolute;
  top: 0.9em;
  right: 1.1em;
  transform: rotate(-6deg);
  padding: 0.25em 0.5em;
  border: 0.12em solid var(--accent2);
  border-radius: 0.25em;
  color: var(--accent2);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.68em;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  white-space: nowrap;
  mix-blend-mode: multiply;
}

/* The card's own maker's mark, inked in the corner like a stamp pressed
   after the recipe was written — clear of the ruled lines, so opacity only
   has to read as "faded ink," not "stay legible under handwriting." */
.notecard-watermark {
  position: absolute;
  right: 0.9em;
  bottom: 0.9em;
  pointer-events: none;
}

.notecard-watermark svg {
  width: 7em;
  height: 7em;
  color: var(--accent2);
  opacity: 0.3;
  mix-blend-mode: multiply;
  transform: rotate(9deg);
}

.notecard-header {
  border-bottom: 1px solid var(--line);
  padding-bottom: 0;
}

/* The single card has no lower spot to drop the cook-count stamp into (see
   .notecard-stamp-inline for the split card's version), so instead the
   title reserves room for it and wraps earlier rather than running under
   it. Adjacent-sibling selector: only applies when the stamp is actually
   rendered before the header. */
.notecard-stamp + .notecard-header {
  padding-right: 7em;
}

.notecard-header h1 {
  font-size: 1.4em;
  letter-spacing: 0;
}

.notecard-meta {
  margin: 4px 0 0;
  color: var(--accent2);
  font-size: 1.05em;
  font-family: "Courier Prime", ui-monospace, monospace;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.notecard-body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 2fr 3fr;
  gap: 1.2em;
  /* Rule spacing matches the card's line-height (1.35em) so body text sits
     on the lines instead of drifting across them. The repeat tile must be
     exactly 1.35em, not 1.35em + 1px: a repeating-linear-gradient's period
     is its last stop minus its first, so putting the 1px line *after*
     1.35em (rather than inside it) made every tile 1px taller than an
     actual text line. Invisible over a few lines, but the 1px error
     compounds every line: by a dozen wrapped step lines the rule had
     drifted a full line's height off the text and was cutting through it.

     Painted as a single-tile linear-gradient repeated via background-size
     rather than as a repeating-linear-gradient: letting the browser tile
     an infinitely-repeating gradient itself allows sub-pixel drift over a
     tall card, and a couple of tile seams land on top of each other and
     render visibly darker than the rest. A fixed background-size forces
     every tile onto the same pixel grid, so every line renders alike. */
  background-image: linear-gradient(
    to bottom,
    transparent 0,
    transparent calc(1.35em - 1px),
    var(--rule) calc(1.35em - 1px),
    var(--rule) 1.35em
  );
  background-size: 100% 1.35em;
  background-repeat: repeat-y;
}

.notecard-ingredients h2 {
  /* Zero, not a fractional em: any margin here that isn't a multiple of
     the 1.35em rule spacing knocks everything after it off the ruled-line
     grid for the rest of the card. */
  margin: 0;
  font-size: 0.85em;
  color: var(--muted);
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.notecard-ingredients ul,
.notecard-steps {
  margin: 0;
  padding-left: 1.1em;
}

.notecard-ingredients ul {
  list-style: none;
  padding-left: 0;
}

.notecard-steps li + li {
  /* Was a 0.3em gap for breathing room, but that's off-grid, see the
     .notecard-ingredients h2 comment above. The numbers already separate
     steps visually. */
  margin-top: 0;
}

/* Front/back split: a longer recipe than one card holds gets two, stacked
   like a card flipped over rather than side by side. Each card is
   single-column and full-width, matching the more generous split budget in
   notecard-fit.ts. */
.notecard-split {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  align-items: center;
}

.notecard-split .notecard-frame {
  margin: 0;
}

.notecard-body-single {
  display: block;
}

/* The ingredient-only front card has spare width, unlike the steps-only
   back card (prose reads badly split into columns). Two columns roughly
   doubles the vertical line budget of the single column. Avoiding
   break-inside on the whole group would keep an ungrouped recipe's one big
   list from ever splitting at all, so the break rules apply per item
   instead: a line never splits top/bottom across the gap, and a group
   header never strands at the bottom of a column separated from its items.
   This is deliberately its own content-sized box nested inside the flex:1
   `.notecard-body`, not that flex item itself: a *definite*-height multicol
   container doesn't overflow a column downward past that height: it
   silently opens *more* columns instead, which run off the card to the
   right and vanish under overflow:hidden. Content-sized here restores the
   normal "overflow spills past the bottom, gets clipped by the card"
   behavior every other section relies on, while the outer flex:1 box still
   stretches to the card's full height so its ruled-line background reaches
   the bottom even when the ingredients don't fill it. */
.notecard-ingredients-columns {
  columns: 2;
  column-gap: 1.5em;
}

.notecard-ingredients-columns li {
  break-inside: avoid-column;
}

.notecard-ingredients-columns h2 {
  break-after: avoid-column;
}

.notecard-side-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.notecard-side-label {
  margin: 0.5em 0 0;
  color: var(--accent2);
  font-size: 1.05em;
  font-family: "Courier Prime", ui-monospace, monospace;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Inline variant: the split card's front has room for the cook-count stamp
   beside "Ingredients" instead of pinned over the header, where a long,
   wrapping title would otherwise print straight through it. */
.notecard-stamp-inline {
  position: static;
  margin: 0.5em 0 0;
}

.notecard-back .notecard-header h1 {
  font-size: 1.1em;
  color: var(--muted);
}

/* The single/split card and, when there is one, the snapshot above it. */
/* Stretch, never center. The card sizes its own type from a container query
   (`.notecard-frame` is the container, and `.notecard` reads 2.45cqw off it),
   so a column that shrink-to-fits its children leaves the frame with no
   definite inline size: cqw resolves to zero, the type goes to 0px, and the
   card collapses to its own border. The snapshot centers itself below
   instead of the column centering everything. */
.notecard-stack {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* A print tucked into the recipe box with the card: its own mount, canted a
   degree or two off the card's square edge so the two read as separate
   objects rather than one misaligned block. Sized off the card's own
   max-width so it stays clearly the smaller of the pair.

   Like the card, it keeps paper colors in either theme: a photo mount that
   turned charcoal in dark mode would look like app chrome, not a print. */
.notecard-snapshot {
  margin: 0 auto;
  width: 100%;
  max-width: 15rem;
  padding: 0.5rem 0.5rem 1.5rem;
  border: 1px solid #8f8370;
  border-radius: var(--radius);
  background: #fbf8f2;
  /* Same lift as the card itself, so the two objects sit on one surface. */
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.08);
  transform: rotate(-1.5deg);
}

.notecard-snapshot .recipe-photo {
  width: 100%;
  aspect-ratio: 4 / 3;
}

/* Print: the notecard prints as an actual 6x4 card, page chrome removed. */
@media print {
  .no-print,
  .app-header,
  .app-footer {
    display: none !important;
  }

  body {
    background: #fff;
    color: #000;
  }

  .notecard-frame {
    width: 6in;
    max-width: none;
  }

  .notecard {
    /* The cqw font-size follows the 6in frame, so the printed card is the
       same layout at true card size. */
    width: 6in;
    height: 4in;
    aspect-ratio: auto;
    border: 1px solid #000;
    border-radius: 0;
    box-shadow: none;
    background: #fff;
    color: #000;
  }

  @page {
    margin: 0.5in;
  }

  /* Split front/back: each card gets its own printed page rather than
     stacking on one sheet. */
  .notecard-split {
    gap: 0;
  }

  .notecard-split .notecard-frame:not(:last-child) {
    break-after: page;
  }
}

/* Cook mode: checklist ingredients, one step at a time. Kitchen use: hands
   messy, glancing rather than reading closely, so this format alone gets
   touch targets and text sized above the app's baseline scale, per
   PRODUCT.md's cook-mode accessibility commitment. */
.cook-mode {
  max-width: 34rem;
}

/* The photo and the control that dismisses it. The button sits on the
   picture rather than beside it, so putting the photo away costs no vertical
   space of its own, and it stays put when the photo goes: the same control in
   the same corner brings it back, which is what keeps hiding from being a
   one-way door. */
.cook-mode-photo {
  position: relative;
  width: min(100%, 34rem);
}

/* A solid chip, not the bare .icon-button: --muted on transparent is
   unreadable over an arbitrary photograph. Ground and ink instead, so it
   reads against whatever is underneath it. */
.cook-mode-photo-toggle {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  background: var(--bg);
  color: var(--fg);
}

/* With the photo gone there is nothing to sit on, so the button rejoins the
   flow where the photo was. */
.cook-mode-photo-hidden .cook-mode-photo-toggle {
  position: static;
}

.cook-mode-checklist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}

.cook-mode-checklist label,
.cook-mode-current-step {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 2.75rem;
  font-size: 1.0625rem;
}

.cook-mode-checklist input[type="checkbox"],
.cook-mode-current-step input[type="checkbox"] {
  flex: none;
  width: 1.5rem;
  height: 1.5rem;
  /* Herb Green, not the app's usual interactive teal: checking an
     ingredient/step off is a completion signal, the role Herb Green is
     reserved for everywhere else in the system. */
  accent-color: var(--ok);
}

.cook-mode-step {
  padding: 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.cook-mode-current-step {
  font-size: 1.375rem;
  line-height: 1.35;
  margin-top: 0.75rem;
}

.cook-mode-nav {
  display: flex;
  gap: 0.75rem;
  margin-top: 1.25rem;
}

.cook-mode-nav button {
  flex: 1;
  min-height: 3rem;
  font-size: 1.0625rem;
  justify-content: center;
}

/* Flowchart (Cooking for Engineers) format ------------------------------- */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Title and, when there is one, a thumbnail of the dish the chart converges
   on. Thumbnail-sized on purpose: the chart is the content here, and a hero
   image above it would bury the first row of the diagram. */
.flowchart-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.flowchart-heading {
  flex: 1 1 12rem;
}

.flowchart-title h1 {
  margin: 0;
}

.flowchart-heading .muted {
  margin: 0.25rem 0 0;
}

/* Big enough to read as the finished dish rather than a favicon, and shaped
   like the other formats' photo so one recipe looks like one recipe across
   the picker. Stays out of the chart's way: the table below is the subject. */
.flowchart-photo {
  flex: none;
  width: 11rem;
}

.flowchart-photo .recipe-photo {
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  border: 1px solid var(--line);
}

/* A chart gets wide before it gets tall, so it scrolls inside its own box
   rather than pushing the whole page sideways. */
.flowchart-scroll {
  overflow-x: auto;
}

.flowchart {
  border-collapse: collapse;
  min-width: 100%;
}

.flowchart-cell {
  border: 1px solid var(--line);
  padding: 0.5rem 0.625rem;
  vertical-align: middle;
  font-size: 0.9375rem;
  line-height: 1.35;
}

/* Ingredients are the entry points: narrow, quiet, and kept on one line so
   the rows stay short enough that a tall chart is still readable. */
.flowchart-ingredient {
  min-width: 9rem;
  max-width: 14rem;
  color: var(--muted);
  background: color-mix(in srgb, var(--line) 22%, transparent);
}

/* Operations are the substance of the chart: they carry the emphasis. Now
   that labels are a terse action rather than the full step prose, the cap
   can be tight enough to actually read as a Cooking for Engineers cell
   instead of a table of paragraphs; a step still short of that wraps.
   Wider than the tightest that fits one operation, because operations stack
   and every one that wraps costs a line in every cell of the column. */
.flowchart-step {
  min-width: 8rem;
  max-width: 13rem;
}

/* Where a step feeds two later steps, the second one points back at it with
   the target's own short label — a plain "from step 2" stopped meaning
   anything once step numbers left the chart. */
.flowchart-reference {
  color: var(--muted);
  font-style: italic;
  text-align: center;
}

.flowchart-reference .flowchart-label-line:first-child::before {
  content: "↩ ";
}

/* Squares no cell reaches: the blank space an ingredient stretches across.
   Bordered like the rest would cut the chart into a grid, so they aren't. */
.flowchart-filler {
  border: 0;
}

.flowchart-label {
  display: block;
}

/* A step usually bundles several operations, and each gets its own line.
   Running them together as one phrase buried the later ones, which are
   exactly where the ingredients feeding this cell get used. */
.flowchart-label-line {
  display: block;
}

.flowchart-label-line + .flowchart-label-line {
  margin-top: 0.1875rem;
}

/* Params — time, temperature, heat — get their own quieter line under the
   action, the same information a printed CfE cell puts in small type. */
.flowchart-detail {
  display: block;
  margin-top: 0.125rem;
  color: var(--muted);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}

/* The cell itself is a button so the short label can expand to the full
   step prose without leaving the chart. Reset to read as plain cell content
   until focused or hovered. */
.flowchart-step-toggle {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  border-radius: var(--radius);
}

.flowchart-step-toggle:hover .flowchart-label {
  text-decoration: underline;
  text-decoration-color: var(--muted);
}

/* The expanded full-prose panel, shown inline under whichever cell was
   clicked. Needs its own min-width since the collapsed columns are sized
   for the short label, not a sentence. */
.flowchart-full {
  min-width: 12rem;
  margin: 0.5rem 0 0;
  padding-top: 0.5rem;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.4;
}

.flowchart-full-number {
  display: block;
  margin-bottom: 0.125rem;
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

/* The full step text as a fallback list, for print and for anyone who'd
   rather read straight down a list than click through the chart. */
.flowchart-steps-full h2 {
  margin: 0 0 0.5rem;
  font-size: 1rem;
}

.flowchart-steps {
  margin: 0;
  padding-left: 1.25rem;
}

.flowchart-steps li {
  margin-bottom: 0.375rem;
  line-height: 1.45;
}

.flowchart-steps li[data-active="true"] {
  color: var(--fg);
  font-weight: 600;
}

@media (max-width: 40rem) {
  .flowchart-cell {
    padding: 0.375rem 0.5rem;
    font-size: 0.875rem;
  }

  .flowchart-ingredient {
    min-width: 7rem;
  }

  .flowchart-step {
    min-width: 9rem;
  }
}

@media print {
  .flowchart-step-toggle {
    cursor: default;
  }

  .flowchart-full {
    display: none;
  }
}

/* Graph editor ----------------------------------------------------------- */

.graph-step-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
}

.graph-step {
  padding: 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.graph-step h2 {
  font-size: 1rem;
  margin: 0 0 0.75rem;
}

.graph-step-text {
  font-weight: 400;
  color: var(--muted);
}

/* Photo import: OCR source text ------------------------------------------ */

.ocr-source {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.75rem 0.875rem;
}

.ocr-source summary {
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--muted);
}

.ocr-source summary:hover {
  color: var(--accent);
}

.ocr-source-text {
  margin: 0.75rem 0 0;
  padding: 0.75rem;
  max-height: 22rem;
  overflow: auto;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--fg) 4%, transparent);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Flags what the engine was least sure of, so review is "check these three
   words" rather than a full re-read against the original. */
.ocr-source-text mark {
  background: color-mix(in srgb, var(--warn) 28%, transparent);
  color: inherit;
  border-radius: var(--radius);
  padding: 0 0.125rem;
}

/* Recipe photo: focal point ----------------------------------------------- */

/* Two images side by side: the one you click, and the one crop it changes.
   A control whose effect is invisible until you save and switch formats is a
   control nobody trusts, so the consequence sits right next to the cause. */
.focal-picker-controls {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.focal-picker-target {
  position: relative;
  display: block;
  padding: 0;
  width: 20rem;
  max-width: 100%;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: none;
  overflow: hidden;
  cursor: crosshair;
}

.focal-picker-image {
  display: block;
  width: 100%;
}

/* A ring rather than a filled dot: the point of the control is to see what's
   underneath it, and a solid marker hides the very thing being aimed at. */
.focal-picker-crosshair {
  position: absolute;
  width: 1.25rem;
  height: 1.25rem;
  margin: -0.625rem;
  border-radius: 50%;
  border: 2px solid var(--accent);
  box-shadow: 0 0 0 2px var(--bg);
  pointer-events: none;
}

.focal-picker-preview {
  flex: none;
  margin: 0;
  width: 9rem;
  text-align: center;
}

.focal-picker-preview-image {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--line);
}

/* Dateline/meta voice, as elsewhere. */
.focal-picker-preview figcaption {
  margin-top: var(--space-1);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

.focal-picker-help {
  margin-top: var(--space-2);
}

/* Photo import: crop/rotate (Phase 10c) ----------------------------------- */

.crop-rotate-toolbar,
.crop-rotate-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.crop-rotate-actions {
  justify-content: flex-end;
}

.crop-rotate-stage {
  position: relative;
  /* Not `overflow: hidden`: the default full-frame crop puts its corner
     handles exactly on the stage boundary, and a clipped container would
     cut the negative-margin handle dots in half right where they're most
     likely to be grabbed. The image itself is always sized to exactly fill
     the (rotation-adjusted) stage, so nothing genuinely needs clipping. */
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  touch-action: none;
  user-select: none;
}

.crop-rotate-image {
  position: absolute;
  left: 50%;
  top: 50%;
  transform-origin: center center;
  pointer-events: none;
  max-width: none;
}

.crop-rotate-mask {
  position: absolute;
  border: 2px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: 0 0 0 999px color-mix(in srgb, var(--bg) 55%, transparent);
  cursor: move;
}

.crop-rotate-handle {
  position: absolute;
  width: 1rem;
  height: 1rem;
  margin: -0.5rem;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--bg);
  cursor: pointer;
}

.crop-rotate-handle-nw {
  left: 0;
  top: 0;
  cursor: nwse-resize;
}

.crop-rotate-handle-ne {
  left: 100%;
  top: 0;
  cursor: nesw-resize;
}

.crop-rotate-handle-se {
  left: 100%;
  top: 100%;
  cursor: nwse-resize;
}

.crop-rotate-handle-sw {
  left: 0;
  top: 100%;
  cursor: nesw-resize;
}

.photo-queue {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
  gap: 0.75rem;
}

.photo-queue-item {
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.photo-queue-item img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
}

.photo-queue-item button {
  position: absolute;
  top: 0.25rem;
  right: 0.25rem;
}

.photo-queue-item.pending {
  outline: 2px solid var(--warn);
}

/* Cookbooks (Phase 11) ---------------------------------------------------- */

.cookbook-shelf {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--space-4);
}

.cookbook-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  color: inherit;
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--space-2);
}

.cookbook-card:hover .cookbook-card-title {
  color: var(--accent);
}

.cookbook-card-cover {
  display: block;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--divider);
}

.cookbook-card-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.cookbook-card-cover-placeholder {
  display: block;
  width: 100%;
  height: 100%;
  background: repeating-linear-gradient(
    135deg,
    var(--divider),
    var(--divider) 0.5rem,
    var(--surface) 0.5rem,
    var(--surface) 1rem
  );
}

.cookbook-card-title {
  font-family: var(--font-heading);
  font-size: 1.0625rem;
}

.cookbook-card-description {
  font-size: 0.8125rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.cookbook-card-count {
  margin-top: auto;
  padding-top: var(--space-1);
  font-size: 0.75rem;
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

.cookbook-breadcrumb {
  margin: 0;
  font-size: 0.875rem;
}

/* Prev/next recipe within the cookbook currently being read through, see
   recipe-detail.tsx. Fixed to the viewport so they stay put while
   scrolling, but positioned off the outside edge of .recipe-detail itself
   rather than the viewport's raw edge: --half-width mirrors
   .recipe-detail's own max-width formula (min(78rem, 94vw), halved), so the
   arrows track the section's actual edge at any width instead of a
   hardcoded inset. There's only real room for that outside .recipe-detail
   on a wide viewport, since below its 78rem cap the section already runs to
   94vw; .cookbook-breadcrumb-nav is the fallback everywhere narrower than
   that. */
.recipe-detail-nav-arrow {
  --half-width: min(39rem, 47vw);
  display: none;
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 5;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
}

.recipe-detail-nav-arrow:hover,
.recipe-detail-nav-arrow:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
}

.recipe-detail-nav-prev {
  left: calc(50vw - var(--half-width) - var(--space-3) - 2.75rem);
}

.recipe-detail-nav-next {
  left: calc(50vw + var(--half-width) + var(--space-3));
}

.cookbook-breadcrumb-nav {
  display: flex;
  gap: var(--space-3);
  margin: 0;
  font-size: 0.875rem;
}

/* Matches the point .recipe-detail's own max-width (min(78rem, 94vw)) hits
   its 78rem cap and stops growing with the viewport, the width at which
   there is finally a real gutter beside it to float an arrow in. */
@media (min-width: 90rem) {
  .recipe-detail-nav-arrow {
    display: flex;
  }

  .cookbook-breadcrumb-nav {
    display: none;
  }
}

.cookbook-masthead-cover {
  display: block;
  width: 100%;
  max-width: 34rem;
  margin: 0 auto var(--space-4);
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  overflow: hidden;
}

.cookbook-masthead-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.cookbook-masthead-description {
  margin: 0 auto var(--space-2);
  max-width: 34rem;
  text-align: center;
  color: var(--muted);
}

.cookbook-entry-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.cookbook-entry-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.cookbook-entry-row .home-index-row {
  flex: 1;
  margin: 0;
}

.cookbook-section-editor {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.cookbook-add-section {
  display: flex;
  gap: var(--space-2);
}

.cookbook-add-section input {
  flex: 1;
  max-width: 20rem;
}

.cookbook-section-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.cookbook-section-name-input {
  flex: 1;
  max-width: 20rem;
  font-family: var(--font-heading);
  font-size: 1.0625rem;
  border: none;
  border-bottom: 1px solid transparent;
  padding: 0 0 0.125rem;
  background: transparent;
}

.cookbook-section-name-input:hover,
.cookbook-section-name-input:focus {
  border-bottom-color: var(--line);
}

.cookbook-section-empty {
  padding: var(--space-2) 0;
}

.cookbook-move-to-select {
  width: 10rem;
  font-size: 0.8125rem;
}

.cookbook-recipe-control {
  position: relative;
}

.cookbook-recipe-control-panel {
  position: absolute;
  z-index: 1;
  top: 100%;
  right: 0;
  margin-top: var(--space-1);
  min-width: 16rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--space-2);
}

/* ---------------------------------------------------------------------
   Login: the same markup and classes as the recipe notecard format
   (apps/web/src/formats/notecard.tsx) — .notecard-frame, .notecard,
   .notecard-header, .notecard-meta, .notecard-body, .notecard-watermark
   — reused as-is, not re-implemented, so the card's shape, cqw type
   scaling, ruled lines, margin rule, and stamp texture all come from the
   same rules a recipe's card already uses. .login-card and
   .login-card-body below are the only overrides: a sign-in form isn't
   pre-measured content like a recipe, so it needs to grow past the fixed
   6x4 shape once the email field opens, and it's a single-column list of
   buttons rather than a two-column ingredients/steps grid. */
.login-error {
  max-width: 26rem;
  margin: 0 auto;
}

/* A dog-eared corner, like a card pulled from the box enough times to
   curl — the one flourish here with no equivalent on the recipe card. */
.login-card-fold {
  position: absolute;
  top: 0;
  right: 0;
  width: 1.75em;
  height: 1.75em;
  overflow: hidden;
  pointer-events: none;
}

.login-card-fold::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--bg);
  clip-path: polygon(100% 0, 0 0, 100% 100%);
  border-bottom: 1px solid var(--line);
  border-left: 1px solid var(--line);
  box-shadow: -0.15em 0.15em 0.2em rgb(0 0 0 / 0.08);
}

/* The mark sits inline with the title, like a letterhead seal, rather
   than the recipe card's own corner watermark. It's positioned out of
   flow and set to the same full size the real notecard's own watermark
   uses (7em here scales to the same ~96px "full" tier BrandMark is drawn
   for), rotated askew like a pressed stamp — including letting it bleed
   past the header's own border-bottom into the ruled body below, which a
   flat, in-flow logo couldn't do without pushing the header taller. */
/* The header grows tall enough that its own border-bottom lands where the
   ruled body's first line starts, rather than hugging the text: padding
   above and below the heading, not just below it. The stamp itself stays
   pinned near the header's top edge (fixed offset, not centered on the
   header's height), so growing this padding moves the border down without
   dragging the stamp down with it. */
.login-card-header {
  position: relative;
  padding-left: 7.6em;
  padding-top: 1.45em;
  padding-bottom: 1.6em;
}

.login-card-mark {
  position: absolute;
  left: 0;
  top: -0.3em;
  width: 7em;
  height: 7em;
  color: var(--accent2);
  transform: rotate(11deg);
  pointer-events: none;
}

.login-card-heading {
  display: flex;
  flex-direction: column;
  gap: 0.25em;
}

/* .notecard fixes line-height at 1.35em off the card's own base font-size
   so every descendant sits on the same ruled-line grid (see .notecard's
   comment) — but that height is too short for this heading's own larger
   type, so the "g" in "Sign in" was overlapping the tagline below it. The
   heading opts out with its own relative line-height, sized off its own
   font-size instead of the inherited fixed one. */
.login-card-header h1 {
  font-size: 2em;
  line-height: 1.15;
}

/* Courier Prime, not the shared --font-mono token: that variable actually
   resolves to Newsreader (see its own definition comment), kept mono in
   name only under the two-voice type system. This is a deliberate one-off
   third face, the same typewriter-stamp font BrandMark already sets by
   hand for its own ring lettering, so the tagline reads like it was
   typed on the same stamp as the mark beside it. */
.login-card-tagline {
  margin: 0;
  font-family: "Courier Prime", ui-monospace, monospace;
  font-size: 1.1em;
  color: var(--accent2);
}

/* A single column of buttons, not the recipe card's ingredients/steps
   grid. The email form keeps the same fixed, centered measure as the
   buttons, so the whole group reads as one column rather than several
   different widths. The ruled-line background-image itself is
   .notecard-body's own (see its comment on the fixed-pixel-grid fix),
   inherited unchanged.

   Opening the email form is taller than the two-button rest state, but
   the card itself never resizes for it (a real index card doesn't grow):
   this box scrolls internally instead, within .notecard's own fixed 6x4
   shape and overflow: hidden. */
.login-card-body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  margin-top: 0.6em;
  overflow-y: auto;
}

/* Centered in the space below the header, then nudged up off dead center
   by margin-bottom rather than a straight top-anchor: two of the ruled
   lines' own tile height (1.35em, see .notecard-body's comment), so the
   group reads as sitting a line or two above the card's true middle. */
.login-card-body > .stack {
  align-items: center;
  margin-bottom: 2.7em;
}

/* Wide enough that "Sign in with Google" and "Sign in with Email" never
   wrap to a second line, with room to spare so the labels don't crowd the
   edges. An opaque fill, not the button system's usual transparent one:
   without it the ruled lines behind a button cross straight through its
   label, instead of the label sitting cleanly in the blank space between
   two lines the way handwriting would. */
.login-card-body > .stack > button {
  width: 100%;
  max-width: 16em;
  justify-content: center;
  white-space: nowrap;
  background-color: var(--bg);
}

.login-card-body form {
  width: 100%;
  max-width: 16em;
}

.login-card-body form button {
  justify-content: center;
  white-space: nowrap;
  background-color: var(--bg);
}

/* The app-wide button/input/field-label sizing is fixed in rem, which is
   correct everywhere else but wrong here: everything else on the card
   shrinks with its own cqw type on a narrow phone, so a fixed-rem control
   stays full size while the card around it shrinks, no longer fits the
   card's own shrunk height budget, and forces .login-card-body to scroll.
   Switching these to the equivalent em values lets the controls shrink
   in step with the rest of the card instead. */
.login-card-body button {
  padding: 0.5em 0.875em;
}

.login-card-body input {
  padding: 0.5em 0.625em;
}

.login-card-body .field {
  gap: 0.375em;
}

.login-card-body .field span {
  font-size: 0.75em;
}
