/*
 * The one shared stylesheet. Every colour, space, size, radius and shadow used
 * anywhere in the app is a custom property defined here, so screen one and
 * screen seven cannot drift apart.
 *
 * The values are the ones decided in 004, against the real fixture stash rather
 * than against lorem ipsum — direction A, "paper and ink", in
 * `specs/004/design-brief.md` and drawn out on the design canvas in
 * `design/004/`. The accent moved from brown to deep petrol because half the
 * stash is beige, mustard and cognac, and a brown accent on warm paper is
 * precisely the feeling she called old-fashioned.
 */

:root {
  /* Colour */
  --colour-page: #faf8f5;
  --colour-surface: #ffffff;
  --colour-slot: #f1ece5;
  --colour-border: #e9e3db;
  --colour-border-soft: #f1ece5;
  /* The boundary of a control she types into or opens. 3.1:1 on the paper and
     3.3:1 on white, which is WCAG 1.4.11's floor for the edge that tells a
     field from the page around it — the hairline above is 1.2:1 and stays for
     containers, where nothing depends on seeing the edge. 029. */
  --colour-border-control: #958c85;
  --colour-text: #1b1917;
  --colour-text-quiet: #6f6862;
  --colour-text-faint: #b0a7a0;
  /* 2.2:1 on the paper, so faint is a *mark* and never text she reads — the
     summary's separator dot, a placeholder's ghost, the empty photo slot's
     glyph, a disabled button's ground. Text that used to be faint is quiet
     since 029; tests/contrast_test.js names the marks that may still use it. */
  --colour-accent: #125c5f;
  --colour-accent-text: #ffffff;
  --colour-accent-soft: #e4efee;
  /* 158: the project page's status band, one hue per state. *I gang* is the
     accent's; *Planlagt* the reservation's amber and *Trevlet op* the error's
     red, both below; *Færdigt* is a green and *På pause* a grey, each a text
     colour at 4.5:1 on its own surface, with a border for the hairline. */
  --colour-accent-border: #c5dcda;
  --colour-finished: #2f6b3a;
  --colour-finished-surface: #e8f1e6;
  --colour-finished-border: #bfd8bb;
  --colour-hold: #6f6862;
  --colour-hold-surface: #efebe6;
  --colour-hold-border: #d9d1c8;
  --colour-reserved: #7a4a12;
  /* 138: the fireworks on a finished project — decoration, never text: the
     accent's teal, the reservation's amber, a warm red and a soft gold. */
  --colour-firework-1: #1f8a8e;
  --colour-firework-2: #c8862a;
  --colour-firework-3: #c4473d;
  --colour-firework-4: #e3c25a;
  --colour-reserved-surface: #f7eeda;
  --colour-reserved-border: #e3cda1;
  --colour-error: #8f2f28;
  --colour-error-surface: #fbeeec;
  --colour-error-text: #5c3a35;
  --colour-error-border: #e3b9b3;
  /* The three inks this file lays over something, read as an ascending scale.

     Ink at 32%, behind the header's menu: *a panel is open over the page you
     were reading*. The one of the three laid over the app's own surfaces. */
  --colour-scrim: rgb(27 25 23 / 32%);
  /* Ink at 62%, under *Fjern billedet* on `add.html`'s picture tiles. White
     text on a photograph needs more ink than a menu does, and a photograph is
     the only ground in this app that is not one of the colours above. Chosen
     at 008 against a real picture and first drawn at 035: until then it was a
     second declaration of `--colour-scrim`, above the line that won. */
  --colour-photo-scrim: rgb(27 25 23 / 62%);
  /* Ink at 82%, for the one place the page goes away for a moment: 034's
     photograph, opened as large as the screen allows. A token of its own rather
     than a reuse of the scrim above — a menu scrim says *a panel is open over
     the page you were reading*, where this says *the page is gone and this
     picture is the whole screen*. Different jobs at different weights. */
  --colour-photo-backdrop: rgb(27 25 23 / 82%);
  /* 203: Statistik's three marks — ind, forbrug + ud, and the month's net.
     Run through the dataviz validator against --colour-page: protan ΔE
     10.1 between the first two, each at least 3:1 on the page. */
  --colour-chart-in: #00907a;
  --colour-chart-out: #dc5f2c;
  --colour-chart-net: #4a50b0;

  /* Spacing — a single scale, so gaps line up across screens */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  /* Type */
  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* 197: headings, her choice from 196's trial. */
  --font-display: "Lora", Georgia, serif;
  --text-micro: 0.6875rem;
  --text-small: 0.8125rem;
  --text-meta: 0.9375rem;
  --text-body: 1rem;
  --text-large: 1.0625rem;
  --text-lot: 1.1875rem;
  --text-title: 1.25rem;
  --text-display: 1.625rem;
  /* 158: the project's name in its band — the one size above display. */
  --text-hero: 2rem;
  --line-tight: 1.25;
  --line-body: 1.5;

  /* Radius and shadow */
  --radius-small: 6px;
  --radius-medium: 10px;
  --radius-large: 14px;
  --radius-pill: 999px;
  --shadow-low: 0 1px 2px rgb(27 25 23 / 6%);
  --shadow-raised: 0 8px 24px rgb(27 25 23 / 14%);

  /* Layout — the desktop layout is a constrained column, never a stretched
     phone screen */
  --measure-page: 60rem;
  --tap-target: 44px;
  /* 187: the tab bar's height, without the home indicator's inset. */
  --tab-bar-height: 3.5rem;
  /* 185, his: a filter bar's controls, the iOS dropdown's own height, so the
     bar does not take the top of the screen. Over 2.2's 24 px target. */
  --control-compact: 1.75rem;
  --header-height: 3.5rem;
  --action-height: 3.25rem;
  --photo-slot: 2.75rem;
  /* 164: the picture on a list card, a size up from the row's slot. */
  --photo-card: 3.5rem;
  /* 033: the picture inside the colour's photograph panel. A 44 px square
     cannot answer *is this one better*, and the two pictures sit side by side
     where the width allows it — so this is the largest square two of which fit
     across a 320 px column with the gutters, and it grows to the column on a
     phone where they stack. */
  --photo-choice: 8.5rem;
  --capture-tile: 6rem;
  --capture-remove: 2.5rem;
  --breakpoint-wide: 48rem;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: var(--space-5) var(--space-4) var(--space-7);
  background: var(--colour-page);
  color: var(--colour-text);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: var(--line-body);
}

main {
  max-width: var(--measure-page);
  margin: 0 auto;
}

h1 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-display);
  line-height: var(--line-tight);
}

h2 {
  margin: 0 0 var(--space-3);
  font-size: var(--text-title);
  line-height: var(--line-tight);
}

section {
  margin-bottom: var(--space-6);
}

/* Focus is visible on everything that takes it, in the accent, against the
   paper. Keyboard-only use is a requirement, not a courtesy. */

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

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

.button {
  min-height: var(--tap-target);
  padding: var(--space-2) var(--space-5);
  border: none;
  border-radius: var(--radius-medium);
  background: var(--colour-accent);
  color: var(--colour-accent-text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

/* The four states */

.state {
  padding: var(--space-4);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-large);
  background: var(--colour-surface);
  box-shadow: var(--shadow-low);
}

.state__label,
.state__message {
  display: block;
  margin: 0 0 var(--space-3);
  color: var(--colour-text-quiet);
}

.state__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-large);
  color: var(--colour-error);
}

.state--error {
  border-color: var(--colour-error-border);
  background: var(--colour-error-surface);
  box-shadow: none;
}

.skeleton {
  height: var(--space-4);
  margin-bottom: var(--space-2);
  border-radius: var(--radius-small);
  background: var(--colour-border);
}

.skeleton:last-child {
  width: 60%;
  margin-bottom: 0;
}

/* Counts and stash lines */

.counts,
.lines {
  margin: 0;
  padding: 0;
  list-style: none;
}

.count,
.lines li {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--colour-border);
}

.count {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
}

.count__value {
  font-variant-numeric: tabular-nums;
}

/*
 * The app frame — every screen that wears the shared header.
 *
 * The page itself has no gutters: the header spans the width and the list
 * hairlines run edge to edge, so each block carries its own padding instead.
 */

.app {
  padding: 0;
}

.app .screen {
  max-width: var(--measure-page);
  margin: 0 auto;
  padding: 0 0 var(--space-7);
}

.app .state {
  margin: var(--space-4);
}

.app .state--empty {
  padding: var(--space-6) var(--space-4);
  text-align: center;
}

.app .state--empty .state__message {
  margin: 0;
  color: var(--colour-text);
}

/* The way out, under the answer. It is instructional rather than the finding
   itself, so it is quieter — and nothing here borrows the error surface: not
   matching a search is an answer, and often good news. */

.state__hint {
  margin: var(--space-2) 0 0;
  font-size: var(--text-small);
  color: var(--colour-text-quiet);
}

/*
 * The start-up failure sentence, and the no-JavaScript fallback — the same
 * paragraph doing both jobs.
 *
 * It is in the markup from first paint so it survives JavaScript being off, and
 * it is hidden the moment the inline mark in `<head>` says JavaScript is
 * running. The module reveals it again if start-up actually fails. Without this
 * it is on screen for the whole of a slow load, telling her the app failed
 * while the app is busy starting — which is the same lie as hiding a failure
 * that did happen.
 */

.startup-failed {
  margin: var(--space-5) var(--space-4);
  color: var(--colour-text-quiet);
}

.js-booting .startup-failed {
  display: none;
}

/* The frame and the shape of the list, from first paint, on any connection.
   Both are static markup carrying no words, and both are replaced the instant
   `t()` exists — so the screen is recognisably itself while it waits, instead of
   white. Neither survives JavaScript being off: a skeleton that never resolves
   would be a screen pretending to load. */

.header--waiting {
  min-height: var(--header-height);
}

.boot-skeleton {
  display: none;
}

.js-booting .boot-skeleton {
  display: block;
}

/* The sentence carries what went wrong; the heading above it does not. It is
   set for the pink surface rather than left in the neutral grey the loading
   label uses. */

.app .state--error .state__message {
  color: var(--colour-error-text);
}

/* The shared header, and its menu */

.header {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--colour-page);
  border-bottom: 1px solid var(--colour-border);
}

.header__bar {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  max-width: var(--measure-page);
  min-height: var(--header-height);
  margin: 0 auto;
  padding: 0 var(--space-4);
  background: var(--colour-page);
}

.header__brand {
  display: flex;
  align-items: center;
  min-height: var(--tap-target);
  font-size: var(--text-large);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--colour-text);
  text-decoration: none;
}

.header__menu-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-target);
  height: var(--tap-target);
  margin-right: calc(var(--space-2) * -1);
  padding: 0;
  border: none;
  border-radius: var(--radius-medium);
  background: transparent;
  color: var(--colour-text);
  cursor: pointer;
}

.header__menu-button[aria-expanded="true"] {
  background: var(--colour-accent-soft);
  color: var(--colour-accent);
}

.header__menu-button .icon--open,
.header__menu-button[aria-expanded="true"] .icon--closed {
  display: none;
}

.header__menu-button[aria-expanded="true"] .icon--open {
  display: block;
}

.header__scrim {
  position: fixed;
  inset: 0;
  z-index: 1;
  background: var(--colour-scrim);
}

.header__menu {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: max(
    var(--space-3),
    calc((100% - var(--measure-page)) / 2 + var(--space-3))
  );
  z-index: 3;
  width: min(20rem, calc(100% - var(--space-6)));
  overflow: hidden;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-large);
  background: var(--colour-surface);
  box-shadow: var(--shadow-raised);
}

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

.header__items li + li .header__item {
  border-top: 1px solid var(--colour-border-soft);
}

.header__item {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--tap-target);
  padding: var(--space-2) var(--space-4);
  border: none;
  background: transparent;
  color: var(--colour-text);
  font: inherit;
  text-align: left;
  text-decoration: none;
}

a.header__item:hover {
  background: var(--colour-accent-soft);
}

/* Inset, because a menu item fills the panel and the panel clips its own
   overflow to keep the rounded corners. An outline sitting outside the item
   would be trimmed off on both sides, leaving a keyboard user with a focus ring
   in pieces. */

.header__item:focus-visible {
  outline-offset: -2px;
}

/* Sign-out, and the sentence it prints when it did not work.
 *
 * The sentence lives beneath the button and inside the panel, so it is where
 * she is already looking. `.banner` carries the app's gutter for the framed
 * screens; here the panel is the gutter, so the margin goes and a little air
 * above it takes its place. */

.signout__message:empty {
  display: none;
}

.signout__banner {
  margin: 0 var(--space-3) var(--space-3);
}

/* The one action, and the summary under it */

.action {
  padding: var(--space-3) var(--space-4) var(--space-2);
}

.action__capture {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--action-height);
  border-radius: var(--radius-medium);
  background: var(--colour-accent);
  color: var(--colour-accent-text);
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
}

.summary {
  padding: var(--space-1) var(--space-4) var(--space-3);
}

/* Absent while loading, rather than an empty band of padding where three
   numbers are about to appear. */

.summary:empty {
  display: none;
}

.summary__items {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-small);
  color: var(--colour-text-quiet);
}

.summary__item {
  display: flex;
  gap: var(--space-1);
}

.summary__item + .summary__item::before {
  content: "·" / "";
  color: var(--colour-text-faint);
}

.summary__value {
  color: var(--colour-text);
  font-variant-numeric: tabular-nums;
}

/* 189: the yarn list's three equal tiles, the number large above its word,
   where 004 had the line above — which the support view still draws. */

.summary__tiles {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* 194: an icon above the figure, the accent's soft ground and its edge on
   the left, so the three read as the page's summary rather than three more
   rows. */

.summary__tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: var(--space-3);
  border: 1px solid var(--colour-accent-border);
  border-left: 4px solid var(--colour-accent);
  border-radius: var(--radius-large);
  background: var(--colour-accent-soft);
}

.summary__tile-icon {
  margin-bottom: var(--space-1);
  color: var(--colour-accent);
}

.summary__tile-value {
  color: var(--colour-text);
  font-size: var(--text-title);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.summary__tile-label {
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

.summary__note {
  margin: var(--space-1) 0 0;
  font-size: var(--text-small);
  color: var(--colour-text-quiet);
}

/*
 * The filter bar.
 *
 * Not sticky, and the header is: two sticky bars on a phone eat the screen the
 * list needs, and the escape hatch — the app name — is in the sticky one, which
 * is the part that must never require scrolling.
 *
 * Native controls throughout. A `<select>` on a phone opens the platform
 * picker, which is big-thumbed, scrollable, accessible and free; a hand-built
 * dropdown is a focus, keyboard and screen-reader problem this project has no
 * framework to hide. Nothing here transitions or animates: every filter is
 * instant, and anything that implies waiting would be a lie about that.
 */

.filter {
  padding: var(--space-2) var(--space-4);
}

/* Absent while loading, and in every state that has no rows to narrow. */

.filter:empty {
  display: none;
}

.filter-bar {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Wrapping, never shrinking and never scrolling sideways: at 320 px the
   controls drop onto their own lines and keep their tap targets. */

.filter-bar__controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.filter-bar__search,
.filter-bar__control {
  margin: 0;
}

.filter-bar__control {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

.filter-bar__input,
.filter-bar__select {
  width: 100%;
  min-height: var(--control-compact);
  padding: 0 var(--space-3);
  border: 1px solid var(--colour-border-control);
  border-radius: var(--radius-medium);
  background: var(--colour-surface);
  color: var(--colour-text);
  font: inherit;
}

.filter-bar__input::placeholder {
  color: var(--colour-text-quiet);
}

/* 185: iOS draws a select at its own height whatever the stylesheet says, so
   the native drawing goes — 086's reason for the date field — and the
   chevron is drawn here. Tapping still opens iOS's own picker. */

.filter-bar__select {
  -webkit-appearance: none;
  appearance: none;
  padding-right: calc(var(--space-3) + 1rem);
}

.filter-bar__control::after {
  content: "";
  position: absolute;
  top: 50%;
  right: var(--space-3);
  width: 0.4rem;
  height: 0.4rem;
  border-right: 2px solid var(--colour-text-quiet);
  border-bottom: 2px solid var(--colour-text-quiet);
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
}

/* 116: a yes/no beside the selects, sized to its words rather than stretched
   as they are, so on a phone it takes its own line beneath them. */

.filter-bar__toggle {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-compact);
  margin: 0;
}

/* The list, and the row */

.stash-list {
  margin: 0;
  padding: 0 var(--space-4);
  list-style: none;
}

/* 164: every list is cards — her verdict B from the mockup, his "for all
   lists for consistency". One rule gives each list's row 158's surface,
   border, radius and shadow; the hairlines between rows are gone and a gap
   stands between cards instead, as a margin rather than a flex gap so a
   repeated lot on the stash list can close it. No hover: a phone has none. */

.stash-row,
.pattern-row,
.project-row,
.name-row,
.invitee-row,
.ravelry-row,
.colour {
  background: var(--colour-surface);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-large);
  box-shadow: var(--shadow-low);
}

.stash-row + .stash-row,
.pattern-row + .pattern-row,
.project-row + .project-row,
.name-row + .name-row,
.invitee-row + .invitee-row,
.ravelry-row + .ravelry-row,
.colour + .colour {
  margin-top: var(--space-3);
}

/* A colour's lots share one card: the second lot joins the card above —
   no gap, no top corners, a hairline between the two — and the card above
   gives up its bottom corners and its bottom edge to it. */

.stash-row--repeat {
  margin-top: 0;
  border-top: 1px solid var(--colour-border-soft);
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.stash-row:has(+ .stash-row--repeat) {
  border-bottom: none;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/* 047: the row is the box, not the link.

   A `<button>` cannot live inside an `<a>`, so the picture came out of the
   anchor and the `<li>` took over the flex and the gap the anchor used to
   carry. **The geometry on screen is unchanged**, and deliberately so: whichever
   of the two boxes is the row's own child keeps the row's left padding, the
   anchor keeps its right, and both reach the row's full height — so the picture,
   the text and the whitespace land where they landed before. What did change is
   named in the spec: hover no longer covers the picture, and the link's tap area
   loses the 44 px column. */

.stash-row {
  display: flex;
  gap: var(--space-3);
}

/* `content-box` because the slot's width is the picture and the padding is the
   row's, not the picture's — and the button, which has no width of its own,
   takes the same padding so the square does not move when a colour gains a
   photograph. */

.stash-row > .stash-row__slot,
.stash-row > .stash-row__open {
  flex: none;
  box-sizing: content-box;
  padding: var(--space-2) 0 var(--space-2) var(--space-3);
}

/* Since 047 the square is a control, and only where there is a picture to open.
   No ground, no border, no padding of its own: the only visible difference
   before she presses anything is a focus ring where a keyboard reaches it. A
   mark saying it can be opened was rejected — forty marks on a hundred rows is
   the interface competing with the yarn. */

.stash-row__open {
  display: block;
  border: none;
  border-radius: var(--radius-medium);
  background: none;
  font: inherit;
  color: inherit;
}

.stash-row__link {
  flex: 1;
  min-width: 0;
  display: flex;
  padding: var(--space-2) var(--space-3) var(--space-2) 0;
  color: inherit;
  text-decoration: none;
}

.stash-row__link:focus-visible {
  outline-offset: -2px;
}

.stash-row__slot {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  width: var(--photo-card);
}

/* The colour's photograph since 023, in the slot 004 drew for it, and the icon
   below is what a colour that has not been photographed still shows.

   `cover` rather than `contain`: a band photographed on a table is mostly
   table, and a square crop of the middle is the colour — which is the whole
   reason the photograph is on the row. It is the same file `read-label` is sent
   at 1024 px, drawn at 44, so the phone is doing the downscaling rather than a
   second stored size. */

.photo-slot__image {
  width: var(--photo-slot);
  height: var(--photo-slot);
  border-radius: var(--radius-medium);
  object-fit: cover;
  background: var(--colour-slot);
}

/* The colour's photograph belongs to the colour, so a repeated colour gets a
   tie up to the row above rather than the same square again. */

.stash-row__photo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--photo-card);
  height: var(--photo-card);
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  color: var(--colour-text-faint);
}

/* 164: the picture on a stash or project card is a size up from the slot. */

.stash-row .photo-slot__image,
.project-row .photo-slot__image {
  width: var(--photo-card);
  height: var(--photo-card);
}

.stash-row--repeat .stash-row__photo {
  display: none;
}

/* 049: the tie stretches to the row rather than setting its height.

   It was a fixed 44 px, which was the photograph's height and therefore the
   *row's* — the body was always shorter. Once the repeated identity went, a
   fixed tie would have kept the row at 68 px and bought whitespace where the
   request was for less noise, so it takes whatever height the one remaining
   line leaves. The row falls to about 46 px, which is above the 44 px a thumb
   needs, and the list gains the hierarchy it did not have: the row that
   introduces a colour is tall and carries the picture, its other lots are short
   lines beneath it. The tie is now the only thing saying they belong together. */

.stash-row--repeat .stash-row__slot {
  align-items: stretch;
}

.stash-row--repeat .stash-row__slot::after {
  content: "";
  width: 1px;
  height: auto;
  background: var(--colour-border);
}

/* The one remaining line sits in the middle of what height the row has, rather
   than hanging from the top edge. It is set on the link, because the body is
   the link's flex item; at `--width-wide` the link is a grid that already
   centres and this rule is simply agreed with. */

.stash-row--repeat .stash-row__link {
  align-items: center;
}

.stash-row__body {
  flex: 1;
  min-width: 0;
}

/* 194: the brand a small quiet line above the yarn and the colour on a
   phone; run in on the wide grid's one line, below. */

.stash-row__brand {
  display: block;
  margin-bottom: 2px;
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  font-weight: 400;
}

.stash-row__identity {
  display: block;
  font-size: var(--text-small);
  line-height: var(--line-tight);
  color: var(--colour-text-quiet);
  overflow-wrap: break-word;
}

/*
 * The first row of a colour carries its name, and carries it in bold.
 *
 * Her verdict on the built list, overturning 004: scrolling a hundred lots is
 * looking for a colour, not for a number, so the row that introduces one has to
 * be findable at speed. It is the row that already has the photograph slot, so
 * the weight and the thumbnail mark the same thing — and the repeats below it
 * stay faint, which is what keeps the group reading as a group.
 *
 * It steps aside for a finished row rather than being overridden below: this
 * selector outweighs the used-up rules, so a lot she has finished would come
 * back louder than the ones she still owns.
 */

.stash-row:not(.stash-row--repeat):not(.stash-row--finished) .stash-row__identity {
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--colour-text);
}

.stash-row--repeat .stash-row__identity {
  color: var(--colour-text-quiet);
}

.stash-row__main {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0 var(--space-3);
}

/* Plain text, and the same size as the quantity beside it, by her decision. 004
   set the lot as the loudest thing on the row; she overturned that on the real
   list, because a number she reads once she has already found the colour does
   not need to be the thing she scans for. The two values on this line are now
   one line rather than a headline and a footnote. Tabular figures stay — that
   is columns lining up, not emphasis. */

.stash-row__lot {
  font-size: var(--text-meta);
  font-variant-numeric: tabular-nums;
}

.stash-row__quantity {
  font-size: var(--text-meta);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.stash-row__tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

.stash-row__badge {
  padding: 0 var(--space-2);
  border: 1px solid var(--colour-reserved-border);
  border-radius: var(--radius-pill);
  background: var(--colour-reserved-surface);
  color: var(--colour-reserved);
  font-size: var(--text-small);
  font-weight: 600;
}

.stash-row__note {
  font-size: var(--text-small);
  color: var(--colour-reserved);
}

/*
 * The three exceptions, marked three ways, because they are not the same kind
 * of thing.
 *
 * A state she acts on gets a rule in the margin, where the eye picks up a
 * rhythm down a long list without any single row shouting. Two rows in five
 * carry some exception, so anything louder than this turns the list into an
 * error log — which is the feeling she rejected before she rejected Ravelry.
 */

.stash-row--reserved {
  box-shadow: inset 3px 0 0 var(--colour-reserved), var(--shadow-low);
}

/* Not measured yet, which is a gap rather than an alarm — and the commonest
   thing on the screen while she is entering five hundred skeins.

   **The italic went at 050's M2**, and the reason is what that row changed. It
   was styling a sentence, *vægt ikke registreret*, which really was unfinished;
   with the sentence gone it was styling `2 ngl.`, and that count is recorded and
   exact. Italicising it says the number is provisional about the one value on
   the row that is not. What is left is the quiet colour, which marks the cell
   without making a claim about what is in it — and the absence of a weight
   beside the count is the signal that actually carries this. */

.stash-row--unweighed .stash-row__quantity {
  color: var(--colour-text-quiet);
}

/* The label printed no name. Marked as a placeholder so it cannot be read as a
   brand actually called "uden mærke"; corrected on the yarn screen in 026. */

.stash-row--unnamed .stash-row__identity {
  font-style: italic;
}

/* Used up is kept, not deleted: the whole row recedes and the word stands in
   the quantity. No red, no strike-through, no warning sign — it is finished,
   not wrong. */

.stash-row--finished .stash-row__identity,
.stash-row--finished .stash-row__lot,
.stash-row--finished .stash-row__quantity {
  color: var(--colour-text-quiet);
}

.stash-row--finished .stash-row__photo {
  background: var(--colour-page);
  color: var(--colour-border);
}

/* --- 006: add.html, the by-hand form ------------------------------------- */

/* A task screen names itself. The stash list is the home screen and the app
   name in the header names that one; this is the only h1 in the app. */

.page-title {
  margin: 0;
  padding: var(--space-4) var(--space-4) var(--space-2);
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.lede {
  margin: 0 0 var(--space-4);
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
  line-height: var(--line-body);
}

/* Persistent, never a toast: a message about her data must not vanish while
   she is looking at yarn rather than at the phone. */

.banner {
  margin: 0 var(--space-4) var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--colour-error-border);
  border-radius: var(--radius-medium);
  background: var(--colour-error-surface);
  color: var(--colour-error-text);
  font-size: var(--text-meta);
  line-height: var(--line-body);
}

/* A real `<form>`, for the two things iOS gives one and gives nothing else: the
   `< >` bar above the keyboard, which is the only next-field control that works
   above the numeric keypad, and a return key that reports itself even with
   predictive text on. See `add.html`. */

/* One gutter, on the screen rather than on each block inside it, exactly as
   `.lot` does for 007. Required rather than tidy-minded since the cards went:
   the fields have no padding of their own any more, so without this every label
   and box would sit flush against the edge of the phone. */

.add {
  padding-inline: var(--space-4);
}

/* 080: sections sit as close as fields. There is no card to separate them -
   008's verdict - and the fields' own labels say which is which, so a larger
   gap here was height she scrolled past on every ball. */

.add-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
}

/* The yarn's details are empty until a yarn is chosen or begun, and an empty
   host would still take a gap. */

.add-form__details:empty {
  display: none;
}

/* A fieldset per level of the model, so a screen reader says which of the four
   a control belongs to — Mærke and Garn both hold a field called a name.

   **No card, and no border.** Both forms in this app were drawn as bordered,
   shadowed cards with upper-case grey legends above them, and she overturned it
   three times before it landed: the legend was a caption for a group while doing
   a label's job, so it read as both and as neither, and at that size and case it
   read as faint besides. The `fieldset` and `legend` stay — they are what a
   screen reader uses to keep *Navn på mærket* and *Garnets navn* apart — and
   nothing draws them as furniture. */

.section {
  margin: 0;
  padding: 0;
  border: none;
  background: none;
}

/* Still waiting on the section above it: since 169 the whole card is dimmed,
   under the 169 block at the foot; the legend is hidden on every card. */

/* Set exactly like a field's own label — same size, same case, same colour, the
   same gap above the control that *Navn på mærket* has above its box. It is
   drawn only where it labels the picker beneath it or stands in for a section
   not yet her turn; everywhere else the markup hides it, because a `legend` names
   a group and the fields below already carry their own labels.

   Deliberately sets no `display` and no `width`: a `<legend>` is already a block
   by default, and claiming either here would outrank `.visually-hidden`. */

.section__legend {
  padding: 0;
  margin: 0 0 var(--space-1);
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
}

.section__body:empty {
  display: none;
}

.section__picker .field {
  margin-bottom: 0;
}

.section__body {
  margin-top: var(--space-3);
}

/* Only beneath a picker: with none above it, the top margin stacked on the
   form's gap and made a section look twice as far away as a field. 080. */

.section__picker:empty + .section__body {
  margin-top: 0;
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0 0 var(--space-3);
}

.field:last-child {
  margin-bottom: 0;
}

/* Visible, never placeholder-only: a placeholder vanishes the moment she types,
   which is exactly when "what is this field?" becomes a live question — and she
   has to be able to read her answers back before saving. */

.field__label {
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

.field__input,
.field__select {
  width: 100%;
  min-height: var(--tap-target);
  padding: 0 var(--space-3);
  border: 1px solid var(--colour-border-control);
  border-radius: var(--radius-medium);
  background: var(--colour-page);
  color: var(--colour-text);
  font: inherit;
}

/* 086's M5, on her phone: iOS draws a date field at a width of its own and
   ignores the column it is in, so it ran under the next field and, on a line
   to itself, would sit shorter than the fields above it. Dropping the native
   appearance is what lets `width: 100%` hold. */

.field__input[type="date"] {
  min-width: 0;
  -webkit-appearance: none;
  appearance: none;
  text-align: left;
}

.field__input:focus-visible,
.field__select:focus-visible {
  outline: 2px solid var(--colour-accent);
  outline-offset: 2px;
}

/* The number and its unit on one line: "50" alone is not a weight, and a unit
   that wraps below the box at 320 px reads as a stray letter. */

.field--measure {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "label label" "input unit";
  align-items: center;
  column-gap: var(--space-2);
}

.field--measure .field__label {
  grid-area: label;
}

.field--measure .field__input {
  grid-area: input;
}

.field__unit {
  grid-area: unit;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* An error is a sentence beside the field it is about, and colour is never the
   whole message. */

.field__error {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--colour-error);
  font-size: var(--text-small);
  /* Bold since 183's walk, his: small and red alone read as dim. */
  font-weight: 700;
  line-height: var(--line-body);
}

/* A warning, and deliberately not --colour-error: the impossible weight is
   saved anyway, and a refusal-coloured sentence beside a value that will be
   kept is the screen saying something it does not mean. */

.field__note {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  line-height: var(--line-body);
}

/* 079: short values side by side, so the form she scrolls on every ball is
   shorter. Each field takes four of the row's lines through subgrid - label,
   box, and two for an error and a warning - so a label that wraps pushes only
   its own text up, every box sits on one line, and a warning appearing under
   Vægt tilbage as she types grows the row downward without moving the box she
   is typing in or its neighbours. Row gaps are zero and the spacing is margins,
   because a gap between two empty note lines would be height for nothing. */

.field-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  grid-template-rows: repeat(4, auto);
  column-gap: var(--space-3);
  row-gap: 0;
  margin: 0 0 var(--space-3);
}

.field-row:last-child {
  margin-bottom: 0;
}

/* A colour's name is the long one and its code the short one. */

.field-row--name-code {
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
}

.field-row > .field {
  display: grid;
  grid-row: span 4;
  grid-template-rows: subgrid;
  row-gap: 0;
  margin: 0;
}

.field-row > .field > .field__label {
  align-self: end;
  margin-bottom: var(--space-1);
}

.field-row .field__error,
.field-row .field__note {
  margin-top: var(--space-1);
}

/* Read-only, because correcting a fibre corrects it for every colour of that
   yarn — that is 026's screen, not the one she uses forty times a week. */

.facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-4);
  margin: 0 0 var(--space-3);
}

.facts__term {
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

.facts__value {
  margin: 0;
  font-size: var(--text-meta);
}

.facts__value--absent {
  color: var(--colour-text-quiet);
}

/* 079: on add.html weight and length share a line - term, value, term,
   value - and the fibre, the long one, keeps a line of its own. */

.facts--paired {
  grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
}

.facts--paired .facts__value:first-of-type {
  grid-column: span 3;
}

.facts__link {
  /* Two standalone links stacked 4 px apart at 13 px were 19.5 px tall, under
     WCAG 2.5.8's 24 px floor. Inline-flex so the box grows to the floor and
     the text stays where it was. 029. */
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--colour-accent);
  font-size: var(--text-small);
}

.button {
  min-height: var(--tap-target);
  padding: 0 var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-medium);
  font: inherit;
  cursor: pointer;
}

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

.button--primary {
  width: 100%;
  min-height: var(--action-height);
  margin-top: var(--space-5);
  background: var(--colour-accent);
  color: var(--colour-accent-text);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* 085: Gem, visible at all times on add.html. Pinned to the bottom with the
   form scrolling beneath it, on the page ground with a rule above it, as the
   header has one below. Full width on a phone; on a desktop the button keeps
   to the page column. The home-indicator inset is added below it so a
   standalone tile does not put Gem under the bar iOS draws. */

.save-bar {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1;
  padding: var(--space-3) var(--space-4)
    calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  background: var(--colour-page);
  border-top: 1px solid var(--colour-border);
}

.save-bar .button--primary {
  display: block;
  max-width: var(--measure-page);
  margin: 0 auto;
}

/* Room for the bar, so the last field and the form's panel scroll clear of it
   rather than stopping underneath it. */

.app .screen--save-bar {
  padding-bottom: calc(
    var(--action-height) + var(--space-3) * 2 + var(--space-7)
  );
}

.button--primary:disabled {
  background: var(--colour-text-faint);
  cursor: default;
}

.button--quiet {
  border-color: var(--colour-border);
  background: var(--colour-surface);
  color: var(--colour-text);
  font-size: var(--text-meta);
}

/* The only confirmation a save gets, so it persists until the next one
   replaces it. */

.saved {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  margin: var(--space-4) 0 0;
}

.saved:empty {
  margin: 0;
}

.saved__label {
  color: var(--colour-accent);
  font-size: var(--text-small);
  font-weight: 600;
}

.saved__line {
  font-size: var(--text-meta);
}

.saved__link {
  color: var(--colour-accent);
  font-size: var(--text-small);
}

/* --- 007: lot.html, one lot and what she can do to it -------------------- */

/* One gutter, on the screen rather than on each block inside it.
   `.app .screen` has none of its own below the breakpoint, and 004 gives each
   block its own padding because the list's hairlines run edge to edge. Nothing
   here does: **Gem** is full width, its confirmation is a panel, and the saved
   line is a sentence — all three are children of this element rather than of the
   form, so a per-block inset would have left them touching both edges while the
   cards above them were inset, and put the save's confirmation on a different
   left edge from the two below it. */

.lot {
  padding-inline: var(--space-4);
}

/* 159: the lot page is 158's — a band tinted by the lot's state, with the
   colour's name as the hero and its square beside it, the photograph at the
   right; the cards beneath in `.lot`'s gutter. The band's elements share the
   project band's rules, listed there, so the two pages are one treatment. */

.lot-band--in_stock {
  border-bottom-color: var(--colour-accent-border);
  background: var(--colour-accent-soft);
  color: var(--colour-accent);
}

.lot-band--reserved {
  border-bottom-color: var(--colour-reserved-border);
  background: var(--colour-reserved-surface);
  color: var(--colour-reserved);
}

.lot-band--used_up {
  border-bottom-color: var(--colour-hold-border);
  background: var(--colour-hold-surface);
  color: var(--colour-hold);
}

/* 161: the pattern page is the same band and cards, tinted by the one state
   a pattern has — reviewed, or not yet — on 158's two hues; Åbn at the right
   where Rediger stands; Gem beneath the band and under the last card (118);
   the cards in `.pattern-view`'s gutter, inside the one form. */

.pattern-band--reviewed {
  border-bottom-color: var(--colour-accent-border);
  background: var(--colour-accent-soft);
  color: var(--colour-accent);
}

.pattern-band--unreviewed {
  border-bottom-color: var(--colour-reserved-border);
  background: var(--colour-reserved-surface);
  color: var(--colour-reserved);
}

.pattern-view {
  padding-inline: var(--space-4);
}

.pattern-view__beneath {
  display: flex;
  justify-content: flex-end;
  margin: 0;
  padding: var(--space-4) var(--space-4) 0;
}

.pattern-view__beneath .button--primary {
  width: auto;
  margin-top: 0;
}

/* The form's gap is the cards' gap, and the last field in a card keeps no
   margin under it — 159's rule, read again here. */

.pattern-view .add-form {
  gap: var(--space-4);
}

.pattern-view .add-form > .button--primary {
  margin-top: 0;
}

.pattern-view .project-card > :last-child {
  margin-bottom: 0;
}

.pattern-card--pattern .pattern-confirm-host:not(:empty),
.project-edit .project-confirm-host:not(:empty) {
  margin-bottom: var(--space-3);
}

/* The square at the left of the name, at the slot's size, top-aligned so a
   two-line name hangs beside it. */

.lot-band__name {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

.lot-band__name .swatch-space {
  flex: none;
  width: var(--photo-slot);
  height: var(--photo-slot);
  margin-top: var(--space-1);
}

.lot-band__name .swatch {
  border-radius: var(--radius-medium);
}

.lot-identity__slot {
  flex: none;
}

/* Since 034 the square is a control, and only where there is a picture to open.
   No ground, no border, no padding: the only visible difference before she
   presses anything is a focus ring where a keyboard reaches it. */

.lot-identity__open {
  flex: none;
  display: block;
  padding: 0;
  border: none;
  border-radius: var(--radius-medium);
  background: none;
  font: inherit;
  color: inherit;
}

/* A thumb has nothing to say about cursors; a mouse is the one pointer that can
   be told what a press will do before it happens. */

@media (pointer: fine) {
  .lot-identity__open,
  .stash-row__open {
    cursor: zoom-in;
  }
}

/* What a colour that has not been photographed shows — 004's slot, at 004's
   size, so the two screens agree about what a colour looks like before it has
   been photographed. The photograph itself is `.photo-slot__image`, which both
   screens share. */

.lot-identity__photo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--photo-slot);
  height: var(--photo-slot);
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  color: var(--colour-text-faint);
}

/* 034: the picture, as large as the screen allows, on the top layer.

   The dialog's own box covers the viewport rather than shrinking to fit the
   photograph, and that is load-bearing rather than tidy — it is what makes a
   press on the dark ground have the dialog itself as its target, which is how
   `photoViewer.js` tells the ground from the picture without measuring
   anything. The colour is painted here and on `::backdrop` both: the second is
   what covers a strip the first could leave on a rounding difference.

   No animation. The viewer is there or it is not, so there is nothing to
   declare for reduced motion and nothing to interrupt. */

.photo-viewer {
  position: fixed;
  inset: 0;
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: none;
  background: var(--colour-photo-backdrop);
}

.photo-viewer::backdrop {
  background: var(--colour-photo-backdrop);
}

/* Guarded on `[open]`, because the rule that hides a closed dialog is the
   browser's and an author's `display` outranks it. */

.photo-viewer[open] {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Maxima and **no width or height of its own**, which is what makes *never
   upscale* structural rather than a rule somebody has to remember: an image
   with only maxima renders at its natural size until a maximum bites, and 023
   stores a longest edge of 1024 px. On a phone the gutter decides; on a wide
   monitor the picture's own size does.

   `contain` and not the slot's `cover`: the square crops to the middle because
   that is where the colour is, but here she is looking at the whole picture on
   purpose, and cropping would hide the half of the band the lot is printed
   on. */

.photo-viewer__image {
  max-width: calc(100vw - var(--space-4) * 2);
  max-height: calc(100dvh - var(--space-4) * 2);
  object-fit: contain;
}

/* Opaque, and that is a contrast decision rather than a style one: this is the
   only thing in the app drawn over a photograph, and a photograph is the one
   ground whose colour nothing knows. Opaque makes the pair `colour-text` on
   `colour-surface`, which tests/contrast_test.js already holds at 4.5:1. */

.photo-viewer__close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-target);
  height: var(--tap-target);
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--colour-surface);
  color: var(--colour-text);
  box-shadow: var(--shadow-low);
}

/* 157: a search list in a dialog, not in the page. On a phone the dialog is
   the whole screen: the field at the top, the status line under it, the
   matches filling the rest and scrolling inside the dialog, and a bar at
   the bottom with Annullér and, on a fine pointer, Vælg. The page beneath
   never changes height while she searches, which is the whole point.

   The form fills the dialog's box and carries its padding, so on a computer
   only the dimmed ground has the dialog itself as a click's target. */

.search-dialog {
  position: fixed;
  inset: 0;
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: none;
  background: var(--colour-page);
  color: var(--colour-text);
}

.search-dialog::backdrop {
  background: var(--colour-scrim);
}

/* 157, his at M2: in the page the choice is a field in the form's own shape
   — a label above, a box that reads the invitation or the choice, a cross
   at its end — so the page is one pattern: label, box, label, box. Two
   sibling buttons drawn as one box; the cross is its own 44 px target over
   the box's right end, and the box's padding leaves room for it. */

.choice-field {
  position: relative;
  display: block;
}

.choice-field__open {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: var(--tap-target);
  padding: 0 var(--space-3);
  border: 1px solid var(--colour-border-control);
  border-radius: var(--radius-medium);
  background: var(--colour-page);
  color: var(--colour-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.choice-field__open--empty {
  color: var(--colour-text-quiet);
}

.choice-field__words {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.choice-field__chevron {
  flex: none;
  color: var(--colour-text-quiet);
}

/* Over the chevron's place, so the box reads the same with or without it. */

.choice-field__clear {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-target);
  height: 100%;
  padding: 0;
  border: none;
  border-radius: var(--radius-medium);
  background: transparent;
  color: var(--colour-text);
  cursor: pointer;
}

.choice-field__clear:not([hidden]) + .choice-field__chevron,
.choice-field:has(.choice-field__clear:not([hidden])) .choice-field__chevron {
  visibility: hidden;
}

.choice-field__open:focus-visible,
.choice-field__clear:focus-visible {
  outline: 2px solid var(--colour-accent);
  outline-offset: 2px;
}

/* The filter bar's look: the bar's surface, in the bar's own slot. */

.choice-field--compact .choice-field__open {
  min-height: var(--control-compact);
  background: var(--colour-surface);
}

/* Guarded on `[open]`, the viewer's reason. */

.search-dialog[open] {
  display: flex;
}

.search-dialog__form {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
  padding: var(--space-4) var(--space-4) 0;
}

.search-dialog__field {
  margin: 0;
}

/* Annullér beside the field, where the keyboard cannot cover it. */

.search-dialog__row {
  display: flex;
  flex-direction: row-reverse;
  gap: var(--space-2);
}

.search-dialog__row > .search-clear {
  flex: 1 1 auto;
  min-width: 0;
}

/* 165: the cross at the end of every search field — the choice field's
   cross, inside the box at its right edge, hidden while the field is empty
   so the box never changes shape. His, at 164's walk: "like the designer,
   it is not in the search field". The field is padded away from it so a
   long word does not run under the cross. */

.search-clear {
  position: relative;
  display: block;
}

.search-clear__input {
  padding-right: var(--tap-target);
}

.search-clear__button {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-target);
  height: 100%;
  padding: 0;
  border: none;
  border-radius: var(--radius-medium);
  background: transparent;
  color: var(--colour-text);
  cursor: pointer;
}

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

.search-dialog__cancel {
  flex: none;
  padding-inline: var(--space-3);
}

.search-dialog__status {
  margin: var(--space-2) 0 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.search-dialog__status:empty {
  display: none;
}

.search-dialog__list {
  flex: 1 1 auto;
  min-height: 0;
  margin: var(--space-2) 0 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

.search-dialog__list:not(:empty) {
  border-top: 1px solid var(--colour-border);
}

/* Vælg, on a fine pointer only: the bar is empty on a phone and not drawn. */

.search-dialog__bar {
  display: flex;
  flex: none;
  justify-content: flex-end;
  margin: 0;
  padding: var(--space-3) 0;
  border-top: 1px solid var(--colour-border);
}

.search-dialog__bar:has(> [hidden]) {
  display: none;
}

/* A real `<form>`, for the two things iOS gives one and gives nothing else —
   see `lib/ui/lotForm.js`. One column at every width: a two-column form asks
   her to read in a Z, and this one is answered downward. Since 159 it holds
   two cards, so its gap is the cards' gap. */

.lot-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
}

/* The yarn's facts on their card, under the heading row. */

.lot-card--about .facts {
  margin: var(--space-3) 0 0;
}

/* The cards' gap is the only space between them — his, at M6: "a lot of
   space between the cards". Four margins stacked on it: an empty saved line
   still took a gap's slot, Gem carried its own top margin, a fieldset's
   body kept its top margin under a heading that already has one, and the
   last field in a card kept its bottom margin. */

.lot .saved:empty {
  display: none;
}

.lot > .button--primary {
  margin-top: 0;
}

.lot .project-card .section__body {
  margin-top: 0;
}

.lot .project-card .section__body > :last-child {
  margin-bottom: 0;
}

/* The finished-and-gone area: the two things typing again cannot undo, last on
   the screen and last in the tab order, so she reaches everything she came for
   before she reaches anything irreversible. */

/* 159, his: *Slet partiet* is the Mængde heading's action — a button in the
   card action's clothes, in the error colour because it is the one thing on
   the page typing again cannot undo. Its confirmation opens under the
   heading, in the host the verb leaves while it stands. */

.lot-delete,
.yarn-delete,
.project-delete,
.pattern-delete {
  padding: 0;
  border: none;
  background: none;
  color: var(--colour-error);
  font: inherit;
  font-size: var(--text-meta);
  font-weight: 600;
  cursor: pointer;
}

.lot-delete:hover,
.yarn-delete:hover,
.project-delete:hover,
.pattern-delete:hover {
  text-decoration: underline;
}

.lot-delete:focus-visible,
.yarn-delete:focus-visible,
.project-delete:focus-visible,
.pattern-delete:focus-visible {
  outline: 2px solid var(--colour-accent);
  outline-offset: 2px;
}

.lot-card--quantity .lot-confirm-host:not(:empty) {
  margin-top: var(--space-3);
}

.lot-confirm-host:empty,
.yarn-delete-host:empty,
.project-confirm-host:empty,
.pattern-confirm-host:empty {
  display: none;
}

/* In place, two buttons, no modal. 004 rejected a focus trap for a three-item
   menu and this is two buttons and a sentence; native `confirm()` cannot be
   translated or styled, which would make the most serious sentence in the app
   the only one not in Danish. */

.confirm {
  padding: var(--space-4);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-medium);
  background: var(--colour-surface);
  box-shadow: var(--shadow-low);
}

.confirm__sentence {
  margin: 0 0 var(--space-2);
  font-size: var(--text-meta);
  line-height: var(--line-body);
}

/* What she is about to destroy, in the words 004's row uses. */

.confirm__line {
  margin: 0 0 var(--space-3);
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  line-height: var(--line-body);
  overflow-wrap: break-word;
}

.confirm__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.confirm__buttons .button {
  flex: 1 1 8rem;
}

/* The accent, for a confirmation that destroys nothing: marking a lot used up is
   a state she acts on, and a save is a save. */

.button--accent {
  background: var(--colour-accent);
  color: var(--colour-accent-text);
  font-weight: 600;
}

/* Reinforcement, never the message: the button is named with the verb, and the
   sentence above it says what happens. */

.button--danger {
  border-color: var(--colour-error-border);
  background: var(--colour-error-surface);
  color: var(--colour-error);
  font-weight: 600;
}

.button--block {
  width: 100%;
}

.button:disabled {
  cursor: default;
  opacity: 0.6;
}

/* The way out of the not-found panel, which is the only place on this screen
   where a visible way back to the list is drawn. */

.state__link {
  color: var(--colour-accent);
  font-size: var(--text-meta);
}

/* `hidden` has to win. The lot number line and the used-up note are shown and
   hidden by the attribute, and any author `display` on their own class would
   quietly outrank the browser's rule for it. */

[hidden] {
  display: none !important;
}

/* The one breakpoint: a real desktop layout, not a phone column stretched
   across a monitor. The row becomes columns and the numbers line up down the
   page. */

/* --- 008: the capture block, the match panel and the disagreement -------- */

/* The fifth section of the form, wearing the same card as the other four so a
   screen reader and an eye both place it the same way. */

.capture {
  margin-bottom: var(--space-5);
}

.capture__input {
  display: none;
}

/* Picture, picture, add — one row read left to right, with no control standing
   outside it. Her decision. Three tiles and their gaps fit at 320 px with room
   to spare: they shrink before they wrap, and they never go below a tap
   target. */

.capture__strip {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.capture__tile {
  position: relative;
  flex: 0 1 var(--capture-tile);
  min-width: var(--tap-target);
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  overflow: hidden;
}

.capture__thumb {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A bar across the foot of the picture rather than a dot in its corner. It is
   the one control in the app narrower than `--tap-target` in one dimension, and
   it is wider in the other: the area is larger than a 44 px square, the thumb
   lands on it without covering the picture she is checking, and a corner dot
   this size would have been the worse trade in both directions. */

.capture__remove {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: var(--capture-remove);
  border: none;
  background: var(--colour-photo-scrim);
  color: var(--colour-accent-text);
  font: inherit;
  font-size: var(--text-large);
  line-height: 1;
  cursor: pointer;
}

.capture__remove::before {
  content: "×";
}

.capture__remove:focus-visible {
  outline: 2px solid var(--colour-accent-text);
  outline-offset: -3px;
}

/* An empty photo slot with a dashed edge, so the row reads as *picture,
   picture, add* rather than as *picture, picture, something else*. The glyph is
   drawn here and the name is translated in the markup: a bare `＋` is a label
   she can see and not one a screen reader can use. */

.capture__tile--add {
  background: none;
}

.capture__add {
  width: 100%;
  height: 100%;
  border: 1px dashed var(--colour-border);
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  color: var(--colour-text-quiet);
  font: inherit;
  font-size: var(--text-display);
  line-height: 1;
  cursor: pointer;
}

.capture__add::before {
  content: "＋";
}

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

/* The read button is full width in the same shape as **Gem**: it is the primary
   action of this block and what her thumb goes to after the last picture.
   Annullér is on its own line beneath it, never beside it — putting the control
   that undoes the read where the read was is how a slow network becomes a
   cancelled read she did not ask for. */

.capture__controls {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.capture__controls:empty {
  display: none;
}

.capture__controls .button--primary {
  margin-top: 0;
}

.capture__cancel {
  align-self: center;
}

/* 083: the camera in the page. At most 45 % of the height, her report on the
   spike: a camera filling the screen hid whether a picture had been taken. The
   thumbnails sit directly beneath it and the shutter counts, so the view, the
   pictures and the button are on screen together. The ground is the photo
   backdrop, so a view still starting is dark rather than a white hole. */

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

/* 221: a fixed height rather than a cap. Capped, the box followed whatever
   frame WebKit reported, and under iOS 27 a landscape frame and a band
   beneath it pushed the shutter off her screen. The picture is drawn from the
   frame, never from this box, so the crop is the view's alone. */
.camera__view {
  width: 100%;
  height: 45vh;
  object-fit: cover;
  border-radius: var(--radius-medium);
  background: var(--colour-photo-backdrop);
  transition: opacity 0.15s;
}

.camera__view--flash {
  opacity: 0.2;
}

.camera__starting {
  margin: 0;
}

/* 231: the shutter and the read in one row, equal halves; a label that does
   not fit its half wraps rather than shrinking. The read is hidden until
   there is a picture, and the shutter then takes the whole row. */
.camera__row {
  display: flex;
  gap: var(--space-3);
}

.camera__row > .button {
  flex: 1 1 0;
  min-width: 0;
  min-height: var(--action-height);
  margin-top: 0;
  white-space: normal;
}

/* 231: nothing to save while the camera is open, so Gem and the room kept
   for it go, and Annullér stops standing behind the bar. */
.app .screen--camera .save-bar {
  display: none;
}

.app .screen--camera.screen--save-bar {
  padding-bottom: 0;
}

.camera__cancel {
  align-self: center;
}

.capture__note {
  margin: var(--space-3) 0 0;
}

/* 176: the one checkbox beneath the strip, and the sentence for a cut-out
   that failed. A label with the box inside it, so the words are the target. */

.capture__keep,
.colour-photo__keep {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  margin-top: var(--space-3);
}

.capture__keep-box,
.colour-photo__keep-box {
  width: var(--space-4);
  height: var(--space-4);
  margin: 0;
}

.capture__cutout-failed,
.colour-photo__cutout-failed {
  margin: var(--space-3) 0 0;
  color: var(--colour-text-quiet);
}

/* 176's run-once. */

.cutouts__intro,
.cutouts__count,
.cutouts__progress {
  margin: 0 0 var(--space-3);
}

.cutouts__failures {
  margin: var(--space-3) 0 0;
  padding-left: var(--space-4);
}

/* Five to eight seconds is a state with a design, not a flicker, and it happens
   roughly a hundred times during the onboarding. It is decoration beside the
   polite region that carries the words. */

.spinner {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  margin-left: var(--space-2);
  vertical-align: -0.15em;
  border: 2px solid currentcolor;
  border-top-color: transparent;
  border-radius: var(--radius-pill);
  animation: spin 0.8s linear infinite;
}

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

/*
 * `index.html` — the door.
 *
 * The only screen with no header, and the one place the drawing stands at its
 * 176 px ceiling with the name at display size and one sentence under it. It
 * was shaped at 016 to match the first-run screen 010 built, so that the first
 * two things a new person saw read as one app; 053 deleted that screen, and
 * this is the shape that survived it.
 */

.signin-page {
  padding: 0;
}

.signin {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 100vh;
  max-width: 34rem;
  padding: var(--space-6) var(--space-4) var(--space-7);
}

/* The screen carries its own gutter, so the banner gives its up rather than
   doubling it. */

.signin .banner {
  margin-inline: 0;
}

.signin .startup-failed {
  margin-inline: 0;
}

.signin__identity {
  text-align: center;
}

/* 512 x 512 is a permanent ceiling on the source file, so 176 px is the largest
   this or any screen may draw it — an upscale invents detail in the face and
   the hands, which is most of the drawing. The box is reserved here so the page
   does not jump when the file lands. */

.signin__art {
  display: block;
  width: 144px;
  height: 144px;
  margin: 0 auto;
}

.signin__name {
  margin: var(--space-2) 0 0;
  font-size: var(--text-display);
  line-height: var(--line-tight);
  letter-spacing: -0.02em;
}

.signin__lede {
  margin: var(--space-3) 0 0;
  font-size: var(--text-meta);
  color: var(--colour-text-quiet);
  text-wrap: pretty;
}

/* One button, full width, and the only accent on the screen. */

.signin__action {
  display: grid;
  margin-top: var(--space-6);
}

.signin__action .action__capture {
  border: none;
  cursor: pointer;
}

.signin__action .action__capture:disabled {
  cursor: default;
}

/* The shape the identity stands in while the dictionary is still on its way:
   no words, and gone rather than hidden once the real thing arrives. */

.signin__bar {
  width: 100%;
  height: 0.75rem;
  margin: var(--space-3) auto 0;
  border-radius: var(--radius-small);
  background: var(--colour-slot);
}

.signin__bar--name {
  width: 55%;
  height: 1.5rem;
}

.signin__bar--part {
  width: 70%;
}

/* The policy link under the button, and the deployed commit under everything.
   Both are quiet on purpose: the door has one thing to do, and neither of these
   is it. */

.signin__policy {
  margin: var(--space-4) 0 0;
  text-align: center;
  font-size: var(--text-small);
}

.signin__policy a {
  color: var(--colour-text-quiet);
}

.build-stamp {
  margin: var(--space-6) 0 0;
  text-align: center;
  font-size: var(--text-small);
  color: var(--colour-text-quiet);
}

/*
 * The two privacy pages, and the only documents in this app.
 *
 * They are prose rather than an interface - no header, no states, no cards -
 * so the whole treatment is a reading measure and the type scale the rest of
 * the app already uses. 34rem rather than `--measure-page`: 60rem of running
 * text is a line nobody's eye can return from.
 */

/* 236: the two public pages' one line about where the app comes from. */
.public-footer {
  padding: var(--space-6) var(--space-4);
  text-align: center;
  font-size: var(--text-small);
  color: var(--colour-text-quiet);
}

.public-footer a {
  color: var(--colour-accent);
}

.policy {
  max-width: 34rem;
  padding-bottom: var(--space-6);
}

.policy__nav {
  display: flex;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
  font-size: var(--text-small);
}

.policy h1 {
  margin-bottom: var(--space-5);
}

.policy h2 {
  margin-top: var(--space-6);
  font-size: var(--text-large);
}

.policy p {
  margin: 0 0 var(--space-4);
  text-wrap: pretty;
}

.policy__updated {
  margin-top: var(--space-6);
  font-size: var(--text-small);
  color: var(--colour-text-quiet);
}

/*
 * 137: the help page — the privacy pages' reading measure inside the app's
 * frame. The screen insets itself, as `.projects` does, and the steps are
 * ordinary numbered lists.
 */

.app .help {
  display: block;
  padding-inline: var(--space-4);
}

.help h3 {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--text-body);
}

.help ol {
  margin: 0 0 var(--space-4);
  padding-inline-start: var(--space-5);
}

.help li {
  margin-bottom: var(--space-2);
}

.help__toc {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0 0 var(--space-4);
  padding: 0;
  list-style: none;
}

@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation-duration: 3s;
  }
}

/* The sentence saying the fields below came from a picture, in the slot the
   match panel would have used.

   Inset by the card's own padding rather than sitting flush: it is a sentence
   about the fields underneath it, so it has to start where those fields start.
   Aligned to the card edge instead, it reads as a stray line belonging to
   nothing. */

.read-outcome {
  margin: 0 0 var(--space-5);
}

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

/* The most important panel in the app: the whole argument in one rectangle. It
   has to be unmistakably the answer to what she just did without becoming the
   loudest thing on a screen that also holds a four-section form — so it takes
   the accent's own quiet ground rather than a heavier weight or a louder hue.
   In the flow, above the form, never a dialog: the always-available empty form
   must stay reachable, and a sheet on a phone can be dismissed by accident. */

.match {
  margin: 0 0 var(--space-5);
  padding: var(--space-4);
  border: 1px solid var(--colour-accent);
  border-radius: var(--radius-large);
  background: var(--colour-accent-soft);
}

.match__title {
  margin: 0 0 var(--space-2);
  color: var(--colour-accent);
  font-size: var(--text-title);
  line-height: var(--line-tight);
}

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

/* 004's row, one size up. On the list it is one of a hundred and is scanned;
   here it is one of one and is read, carefully, because she is checking whether
   this is really the ball in her hand. */

.match__line {
  margin: 0;
  font-size: var(--text-large);
  line-height: var(--line-body);
}

/* A used-up match reads differently rather than looking different: 004 settled
   that a finished lot is drawn quietly, and this sentence is the whole visible
   difference between the two panels. */

.match__note {
  margin: var(--space-2) 0 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
  line-height: var(--line-body);
}

.match .button--primary {
  margin-top: var(--space-4);
}

/* Findable when the app is wrong and quiet when it is right. Too quiet is a
   trap; too loud makes every correct match look like a question. */

.button--link {
  border: none;
  background: none;
  color: var(--colour-accent);
  font-size: var(--text-meta);
  text-decoration: underline;
}

.match__reject,
.match__skip {
  display: block;
  width: 100%;
  margin-top: var(--space-1);
  text-align: center;
}

/* Her record wins and nothing is overwritten, so this is a note and not a
   refusal: no error colour, no `aria-invalid`, nothing blocked. It sits on the
   page ground inside the yarn's card, above the link that is the way to change
   it for every colour of that yarn at once. */

.disagrees {
  margin: 0 0 var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-medium);
  background: var(--colour-page);
}

.disagrees__sentence {
  margin: 0 0 var(--space-2);
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  line-height: var(--line-body);
}

.disagrees__fact {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin: 0 0 var(--space-1);
  font-size: var(--text-small);
}

.disagrees__fact:last-child {
  margin-bottom: 0;
}

.disagrees__label {
  color: var(--colour-text-quiet);
}

/* --- 009: brands.html, her list of names --------------------------------- */

/* The gutter is on the blocks rather than on the screen, exactly as 004's list
   does it: the rows' hairlines run edge to edge on a phone and the form above
   them does not. */

.name-add {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding: 0 var(--space-4) var(--space-4);
}

.name-add__submit {
  align-self: flex-start;
}

/* One line, directly beneath the add form, and nothing at all until a write has
   happened. */

.name-status {
  margin: 0 var(--space-4) var(--space-3);
}

.name-status:empty {
  margin: 0;
}

/* The dot between *Gemt* and the brand's name, drawn in CSS so a screen reader
   reads two values rather than a punctuation mark — the same reason `.summary`
   draws its separators this way.

   026's colour status region is the same fact in the same shape, so it shares
   this rule rather than carrying a copy of it: both are a label, a separator
   nobody reads aloud, and the name of the row that was just written. */

.name-status .saved__line::before,
.colour-status .saved__line::before,
.yarn-saved .saved__line::before {
  content: "·" / "";
  margin-right: var(--space-1);
  color: var(--colour-text-faint);
}

/* A banner naming what it is about carries the name on a second line rather
   than inside the sentence: Danish and English would not agree about where it
   goes. */

.banner__sentence,
.banner__line {
  margin: 0;
}

.banner__line {
  margin-top: var(--space-1);
  font-weight: 600;
}

/* 219: the door, beside the sentence saying she is signed out. */
.banner__link {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
}

/* 164: the gutter moved from the row to the list, since the row is a card. */
.name-list {
  margin: 0;
  padding: 0 var(--space-4);
  list-style: none;
}

/* Not a `<table>`: two of the three things in a row are controls, and at 320 px
   the row wraps to two lines — the name on the first, the count and the two
   verbs on the second — which is a layout a table cannot have. */

.name-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3);
}

/* Nothing groups these rows: every one is a brand and the name is the whole of
   its identity, so 004's rule puts the emphasis here and everything else on the
   row is quieter than it. A long name wraps rather than truncating — a
   truncated name on the screen for fixing names is a joke at her expense. */

.name-row__name {
  flex: 1 1 100%;
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--colour-text);
  overflow-wrap: break-word;
}

.name-row__count {
  flex: 1 1 auto;
  font-size: var(--text-meta);
  color: var(--colour-text-quiet);
}

/* 133: a designer's row carries up to three verbs, so they wrap beneath a
   long name rather than pushing it. */

.name-row__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.name-replace__sentence {
  margin: var(--space-3) 0 0;
}

.name-replace__sentence:empty {
  margin: 0;
}

/* Words, never icons: a pencil and a bin would be the two most consequential
   controls on the screen drawn as pictures nobody can be sure of. Quiet text
   controls that gain their border on hover and focus rather than wearing one
   down the whole list. */

.button-text {
  min-height: var(--tap-target);
  padding: 0 var(--space-3);
  border: 1px solid transparent;
  border-radius: var(--radius-medium);
  background: transparent;
  color: var(--colour-accent);
  font: inherit;
  font-size: var(--text-meta);
  cursor: pointer;
}

.button-text:hover:not(:disabled) {
  border-color: var(--colour-border);
  background: var(--colour-surface);
}

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

.button-text:disabled {
  color: var(--colour-text-faint);
  cursor: default;
}

/* An open row is the whole row and nothing else on the screen repaints: on a
   phone a rebuilt text field loses the caret. */

.name-row--editing,
.name-row--confirming {
  display: block;
}

.name-rename {
  margin: 0;
}

.name-rename__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.name-rename__buttons .button {
  flex: 1 1 8rem;
}

/* The link lists on the dev pages - `reads.html`'s eleven stubbed reads and
   `states.html`'s forced failures. One block rather than two because they are
   the same list doing the same job, and both go with the mock in 024. */

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

.devlinks__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--colour-border-soft);
}

.devlinks__link {
  color: var(--colour-accent);
}

.devlinks__parameter {
  color: var(--colour-text-faint);
  font-size: var(--text-small);
}

/* --- 026: yarn.html, one yarn and everything under it -------------------- */

/*
 * The one visual question this screen asks, and the answer it got.
 *
 * Three levels on a 390 px phone — a yarn, its colours, their lots — must read
 * as *a list of colours she owns* and not as an outline, a file tree or an admin
 * panel. That last is the failure mode named in the design brief, and it is the
 * feeling she rejected before she rejected Ravelry.
 *
 * **Nothing is a card and nothing is nested in a box.** A colour is separated
 * from the next one by a hairline running edge to edge and by whitespace, which
 * is exactly how 004's list separates its rows — so the two screens use one
 * idiom rather than two. Inside a colour, the lot rows carry no rules at all:
 * they are held together by being indented under a heading that is louder than
 * they are, which is 004's grouping rule one level up. A second border inside
 * the first is what turns indentation into an outline.
 *
 * The gutter is on each block rather than on the screen, again as 004 does it,
 * because the colours' hairlines have to reach both edges of the phone while
 * the form above them does not.
 */

.yarn {
  padding-inline: var(--space-4);
}

/* Not the name of a task — this screen is *about* a yarn. Since 169 the name
   is the band's `<h1>`, the project band's rules, with the brand beneath. */

/* One column at every width, per 007: a two-column form asks her to read in a
   Z, and this one is answered downward. */

.yarn-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin: 0;
}

/* The brand when her brand list could not be read: the name the yarn already
   carries, as text. Set like a field's value rather than like a disabled
   control — there is no control, and one that looked answerable would be the
   lie 005 refused for the filter bar. */

.field__readonly {
  min-height: var(--tap-target);
  display: flex;
  align-items: center;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* 007's saved line, and nothing beside the word: the six values she changed are
   in the fields above it and in the `<h1>`, both repainted from the row that
   came back. */

.yarn-saved {
  margin: var(--space-4) 0 0;
}

/*
 * 032 — the offer to recompute the lots a corrected ball weight left behind.
 *
 * *Slet garnet*'s panel in *Slet garnet*'s shape, mounted directly beneath the
 * form after *Gemt* rather than at the foot of the screen: it is a question
 * about the save she just made, and on a phone the rows it is about are below
 * the fold. The same gutter as `.yarn`, so the panel's edge lines up with the
 * fields above it.
 */

.yarn-reweigh {
  margin: var(--space-4) var(--space-4) 0;
}

.yarn-reweigh:empty {
  margin: 0;
}

/*
 * The seam between the form and the list — the design brief's Open 4.
 *
 * Two genuinely different objects on one screen: a form she fills in, and a list
 * she reads. *Farver* is the only visible section heading in the app, and it
 * earns that by naming a block which has no label of its own; the extra space
 * above it and the rule at the top of the first colour are what stop the
 * colours reading as a seventh field.
 */

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

.colour-status {
  margin: var(--space-2) var(--space-4) 0;
}

.colour-status:empty {
  margin: 0;
}

.colour-list {
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

/* The hairline and the whitespace that separate one colour from the next, and
   the whole of what separates them. Edge to edge on a phone, exactly as 004's
   rows are. */

.colour {
  padding: var(--space-4) var(--space-3);
}

/*
 * The heading and its two verbs, on one line where there is room and two lines
 * where there is not — the design brief's Open 2 and Open 9. Wrapping rather
 * than shrinking, so at 320 px the name takes the first line and the buttons
 * the second, and every button keeps its tap target either way.
 *
 * Quiet text buttons that gain their border on hover and focus, which is 009's
 * answer to the same ratio problem: four colours means eight buttons, and eight
 * bordered buttons is a toolbar.
 */

.colour__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1) var(--space-3);
  padding: 0 var(--space-4);
}

/* The three open states stand in the place of the slot, the heading *and* its
   buttons, so each takes the whole width of the head rather than a share of one
   line. */

.colour__head > .colour-edit,
.colour__head > .confirm,
.colour__head > .colour-photo {
  flex: 1 1 100%;
}

/* 033's square, left of the heading, at 004's size and with 004's icon — the
   third place in the app it is drawn and the same 44 px in all three, so a
   colour looks the same here as it does in the stash list and on the lot. */

.colour__slot {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  width: var(--photo-slot);
}

.colour__photo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--photo-slot);
  height: var(--photo-slot);
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  color: var(--colour-text-faint);
}

.colour__name {
  flex: 1 1 11rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
  margin: 0;
  font-size: var(--text-body);
  font-weight: 600;
  line-height: var(--line-tight);
}

/* The colour is what groups the rows, so the colour carries the emphasis —
   `CLAUDE.md`'s lesson from 004's real list, one level up. A long name wraps
   rather than truncating: a truncated name on the screen for fixing names is a
   joke at her expense. */

.colour__label {
  color: var(--colour-text);
  overflow-wrap: anywhere;
}

/* Quiet beside the name, and drawn only when it is not already the name. The
   separator is CSS so a screen reader reads two values rather than a
   punctuation mark. */

.colour__code {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

.colour__code::before {
  content: "·" / "";
  margin-right: var(--space-1);
  color: var(--colour-text-faint);
}

/* The negative end margin gives the last button's own padding back to the
   gutter, so the words line up with the edge the rest of the block keeps. */

.colour__actions {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-inline-end: calc(var(--space-3) * -1);
}

/*
 * The lots of one colour: indented under it, quieter than it, and separated by
 * nothing. Not a `<table>` — at 320 px each row keeps its number and quantity
 * on one line and the reservation wraps below them, which a table cannot do.
 */

.lot-list {
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}

.lot-row__link {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0 var(--space-3);
  padding: var(--space-2) var(--space-4) var(--space-2) var(--space-6);
  color: inherit;
  text-decoration: none;
}

.lot-row__link:hover {
  background: var(--colour-surface);
}

.lot-row__link:focus-visible {
  outline-offset: -2px;
}

/* Plain text at the same size as the quantity beside it, by her decision on
   004's real list. `--text-lot` does not appear on this screen: 007 is its one
   reader, where she has arrived at a single lot and there are no rows to group.
   Tabular figures stay — that is columns lining up, not emphasis. */

.lot-row__number {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
  font-variant-numeric: tabular-nums;
}

.lot-row__quantity {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.lot-row__tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  flex-basis: 100%;
  margin-top: var(--space-1);
}

/* 004's badge and note, reused unchanged: a reservation is a fact about whether
   the yarn is spoken for, and this is a list of yarn. */

.lot-row__badge {
  padding: 0 var(--space-2);
  border: 1px solid var(--colour-reserved-border);
  border-radius: var(--radius-pill);
  background: var(--colour-reserved-surface);
  color: var(--colour-reserved);
  font-size: var(--text-small);
  font-weight: 600;
}

.lot-row__note {
  color: var(--colour-reserved);
  font-size: var(--text-small);
}

/* One of 004's three exceptions, marked the way 004 marks it: a state she acts
   on gets a rule in the margin. Anything louder than this turns the block into
   an error log.

   **A measurement nobody has taken yet is no longer marked here, since 050's
   M2.** It was italic, and the whole quantity on this screen is already quiet,
   so the italic was the only mark it had — it was styling the sentence *vægt
   ikke registreret*, and once that sentence went it was italicising a ball
   count that is recorded and exact. On the stash list the quiet colour survives
   the italic; here there was nothing beneath it, so the absence of a weight
   beside the count is what says it, on a screen showing one colour's few lots
   rather than a hundred rows being scanned. */

.lot-row--reserved {
  box-shadow: inset 3px 0 0 var(--colour-reserved);
}

/*
 * The count of what she cannot see — the design brief's Open 5.
 *
 * Quieter than a lot row, below the rows and above the *Tilføj parti* link. Not
 * a badge, not `--colour-error`, and not a row itself: this is a state she acts
 * on elsewhere rather than an exception. It exists so the number in the delete
 * confirmation is never a surprise.
 */

.colour__used-up {
  margin: var(--space-2) 0 0;
  padding: 0 var(--space-4) 0 var(--space-6);
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

/*
 * The one control of the three she uses often — the design brief's Open 3.
 *
 * A link and not a button, because it goes somewhere, and inside the colour's
 * indentation so it reads as belonging to the colour above it rather than
 * floating between two of them. Its own line, with a tap target, because it sits
 * where a thumb arrives after reading the rows.
 */

.colour__add {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  margin: 0 var(--space-4) 0 var(--space-6);
  color: var(--colour-accent);
  font-size: var(--text-small);
}

/* An open colour is its head and nothing else on the screen: on a phone a
   rebuilt text field loses the caret, and the lot rows below do not move. */

.colour-edit {
  margin: 0;
}

.colour-edit__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.colour-edit__buttons .button {
  flex: 1 1 8rem;
}

/* 033: the colour's photograph panel, the third thing a colour's head can turn
   into. The file input behind it is never seen and is never a tab stop — the
   visible verbs are what click it — so it is taken out of the layout entirely
   rather than hidden in a way that leaves a gap. */

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

.colour-photo {
  margin: 0;
}

/* The sentence before the pictures rather than after them, because it is the
   thing she has to have read before she looks at the choice. Drawn as the
   app's other warnings are: an error ground and error text, at 4.5:1 on the
   tokens `tests/contrast_test.js` already measures. */

.colour-photo__warning {
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--colour-error-border);
  border-radius: var(--radius-medium);
  background: var(--colour-error-surface);
  color: var(--colour-error-text);
  font-size: var(--text-small);
  line-height: var(--line-body);
}

/* Side by side where there is room and stacked where there is not, so the
   comparison is the panel rather than something she has to remember. */

.colour-photo__pictures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.colour-photo__figure {
  flex: 1 1 var(--photo-choice);
  max-width: 100%;
  margin: 0;
}

.colour-photo__picture {
  display: block;
  width: 100%;
  height: var(--photo-choice);
  border-radius: var(--radius-medium);
  object-fit: cover;
  background: var(--colour-slot);
}

/* Named, because *which of these two am I looking at* is the only question the
   panel exists to answer. */

.colour-photo__caption {
  display: block;
  margin-top: var(--space-1);
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

.colour-photo__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.colour-photo__buttons .button {
  flex: 1 1 8rem;
}

/* A count inside a confirmation, on a line of its own so it can take its own
   plural. Louder than the identity above it, because it is the fact she is
   least likely to already know — the design brief's Open 11. */

.confirm__count {
  margin: 0 0 var(--space-3);
  font-size: var(--text-meta);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/*
 * *Slet garnet*, at the foot of a long scroll — the design brief's Open 6.
 *
 * Last on the screen and last in the tab order, so she reaches everything she
 * came for before she reaches the one thing that cannot be undone, and so the
 * rows it will take are the last thing she reads before she taps it. 007's
 * treatment for `.lot-actions`: quiet, above a rule, full width — findable
 * without being what she sees when she arrives.
 */

/* Since 169 the verb is the Om garnet heading's action, in `.lot-delete`'s
   rules as `.yarn-delete`, and its confirmation opens under the heading in
   `.yarn-delete-host`; the block at the foot went with the flat paper. */

/* 025 — the administrator's screen. Nothing here is new furniture: the add
   form, the status line, the banner and the confirmation are 009's, reused
   rather than restyled, and what follows is only the two lists this screen has
   that no other screen has. */

.invitee-add {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding: 0 var(--space-4) var(--space-4);
}

.invitee-add__submit {
  align-self: flex-start;
}

/* One line, directly beneath the add form, and nothing at all until a write has
   happened. The dot between the label and the address is drawn in CSS so a
   screen reader reads two values rather than a punctuation mark — the same
   reason `.name-status` and `.summary` draw their separators this way. */

.admin-status {
  margin: 0 var(--space-4) var(--space-3);
}

.admin-status:empty {
  margin: 0;
}

.admin-status .saved__line::before {
  content: "·" / "";
  margin-right: var(--space-1);
  color: var(--colour-text-faint);
}

/* Both headings are the same size and weight, because the list and the history
   are two answers to one question rather than a thing and its footnote. */

.invitees__heading {
  margin: var(--space-5) 0 var(--space-2);
  padding: 0 var(--space-4);
  font-size: var(--text-meta);
  font-weight: 600;
  color: var(--colour-text-quiet);
}

.invitee-list,
.event-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* 164: the gutter moved from the row to the list, since the row is a card. */
.invitee-list {
  padding: 0 var(--space-4);
}

/* Not a `<table>`: the third thing in a row is a control, and at 320 px the row
   wraps to two lines — the address on the first, the note and the verb on the
   second. The address is what identifies the row and nothing groups these rows,
   so 004's rule puts the emphasis there and the rest of the row is quieter. */

.invitee-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3);
}

/* `anywhere` rather than `break-word`: an address is one long token with no
   spaces to break at, and a truncated address on the screen for deciding who
   may sign in is a joke at his expense. */

.invitee-row__email {
  flex: 1 1 100%;
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--colour-text);
  overflow-wrap: anywhere;
}

.invitee-row__note {
  flex: 1 1 auto;
  font-size: var(--text-meta);
  color: var(--colour-text-quiet);
}

.invitee-row__actions {
  display: flex;
  gap: var(--space-2);
}

/* An open confirmation is the whole row and nothing else on the screen
   repaints: on a phone a rebuilt control loses the focus ring, and the field
   above this list is where he was standing. */

.invitee-row--confirming {
  display: block;
}

/* The sentence saying nobody else is invited yet, which stands in for an empty
   state this list cannot reach: allowed_email always holds the administrator
   and his row cannot be deleted. It is not an error and must not read as one —
   `--colour-error` belongs nowhere near it. */

.invitees__nobody-else {
  margin: var(--space-3) 0 0;
  padding: 0 var(--space-4);
  font-size: var(--text-meta);
  color: var(--colour-text-quiet);
}

/* The history is a record and not a list he acts on, so every part of it is
   quieter than an invitee row — including the address, which here is one of
   three facts about a past event rather than the identity of a row. */

.event-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
  padding: var(--space-1) var(--space-4);
  font-size: var(--text-small);
  color: var(--colour-text-quiet);
}

.event-row__email {
  overflow-wrap: anywhere;
}

.event-row__action {
  color: var(--colour-text);
}

.event-row__at {
  flex: 1 1 auto;
  text-align: right;
  font-size: var(--text-micro);
  color: var(--colour-text-quiet);
}

/* --- 030: support.html, somebody else's stash ---------------------------- */

/* The one link into the support screen, on the one page that already has no
   menu entry. It sits above the list rather than beneath it because the list
   grows and a link below a growing list is a link that moves. */

.admin-support {
  margin: var(--space-4) 0 0;
  padding: 0 var(--space-4);
  font-size: var(--text-meta);
}

/* 182: her forwarding address under the pattern section. */

.status__mail {
  margin-top: var(--space-4);
}

.status__mail-line {
  margin: 0 0 var(--space-2);
  overflow-wrap: anywhere;
}

.status__mail-where {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* A glyph button, 44 px to tap though it draws 18. */
.status__mail-copy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-target);
  min-height: var(--tap-target);
  padding: 0;
  border: 1px solid var(--colour-border-control);
  border-radius: var(--radius-medium);
  background: var(--colour-surface);
  color: var(--colour-text);
  cursor: pointer;
}

.status__mail-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0 0 var(--space-2);
}

.status__mail-ask {
  margin: var(--space-2) 0;
}

/* 182: the two ways a PDF arrives, as one pair. */

.pattern-add__sources {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 100%;
}

.pattern-add__sources .button {
  width: 100%;
}

/* 182: the PDFs waiting in her mail, on Tilføj opskrift. */

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

.pattern-add__mail-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--colour-border);
}

.pattern-add__mail-meta {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.pattern-add__mail-error {
  color: var(--colour-error);
}

/* 183: Fjern is the lesser action, so it keeps its own width. */
.pattern-add__mail-remove {
  align-self: flex-start;
}

/* 180: the invoice basis. A plain table, for the laptop it is mostly read
   on; it scrolls sideways inside its own box on a phone rather than taking
   the page with it. */

.invoice__month {
  margin: 0 0 var(--space-4);
}

.invoice {
  overflow-x: auto;
}

.invoice__table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.invoice__table th,
.invoice__table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--colour-border);
  text-align: left;
}

.invoice__table .invoice__n {
  text-align: right;
  white-space: nowrap;
}

.invoice__sum th,
.invoice__total th,
.invoice__total td {
  font-weight: 600;
}

.invoice__model th,
.invoice__model td {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.invoice__model th {
  padding-left: var(--space-5);
}

.invoice__note {
  margin: var(--space-3) 0 0;
  color: var(--colour-text-quiet);
}

/* 203: Statistik, as amended at its walk. One period at a time: the month or
   year choice and its dropdown, three figures, and the year's bars. */

/* The page's own gutter: `.app .screen` has none, and the chart's labels
   would otherwise touch the edge of the phone. */
.stats {
  padding-inline: var(--space-4);
}

.stats-period__pick {
  flex: 0 0 auto;
}

.stats-period__pick .filter-bar__select {
  width: auto;
}

.stats-period {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-4);
}

.stats-period__modes {
  display: flex;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.stats-period__mode {
  cursor: pointer;
}

.stats-period__mode--on {
  border-color: var(--colour-accent);
  background: var(--colour-accent);
  color: var(--colour-accent-text);
}

.stats-period__mode:has(:focus-visible) {
  outline: 2px solid var(--colour-accent);
  outline-offset: 2px;
}

.stats-figures {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
}

.stats-figure {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-large);
  font-variant-numeric: tabular-nums;
}

.stats-figure__name,
.stats-figure__metres {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.stats-figure__grams {
  font-size: var(--text-title);
  font-weight: 600;
  line-height: var(--line-tight);
}

.stats__section {
  margin-top: var(--space-6);
}

.stats__heading {
  margin: 0 0 var(--space-3);
  font-size: var(--text-title);
}

.stats-chart {
  display: block;
  width: 100%;
  height: auto;
  max-width: 40rem;
}

.stats-chart__grid {
  stroke: var(--colour-border);
  stroke-width: 1;
}

.stats-chart__zero {
  stroke: var(--colour-text-quiet);
  stroke-width: 1;
}

.stats-chart__tick,
.stats-chart__initial {
  fill: var(--colour-text-quiet);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}

.stats-chart__in {
  fill: var(--colour-chart-in);
}

.stats-chart__out {
  fill: var(--colour-chart-out);
}

.stats-chart__month--faded .stats-chart__in,
.stats-chart__month--faded .stats-chart__out {
  opacity: 0.3;
}

/* A column beyond the axis, cut in the page's colour. */
.stats-chart__cut {
  fill: none;
  stroke: var(--colour-page);
  stroke-width: 2;
}

.stats-chart__hit {
  fill: transparent;
  cursor: pointer;
}

.stats-chart__net {
  fill: none;
  stroke: var(--colour-chart-net);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.stats-chart__net-point {
  fill: var(--colour-chart-net);
  stroke: var(--colour-page);
  stroke-width: 2;
}

/* Values wear text ink; the mark beside each carries its identity. */
.stats-chart__value {
  fill: var(--colour-text);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  paint-order: stroke;
  stroke: var(--colour-page);
  stroke-width: 3;
}

.stats-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.stats-legend__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.stats-legend__in,
.stats-legend__out {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 2px;
}

.stats-legend__in {
  background: var(--colour-chart-in);
}

.stats-legend__out {
  background: var(--colour-chart-out);
}

.stats-legend__net {
  width: 1rem;
  height: 2px;
  background: var(--colour-chart-net);
}

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

.stats__kinds li {
  display: flex;
  justify-content: space-between;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--colour-border);
  font-variant-numeric: tabular-nums;
}

.stats__note {
  margin: var(--space-3) 0 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* The strip that says whose stash this is, and it is deliberately the error
   banner's treatment rather than a quieter one of its own. The requirement is
   that this page cannot be mistaken for his own, and the loudest thing this
   stylesheet already owns is the right amount of loud for looking at another
   person's data. It is drawn once and never changes, so it is a heading and
   carries no live region — see the markup. */

.support-whose {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.support-whose__label {
  margin: var(--space-2) 0 0;
  font-size: var(--text-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* The only `h1` on this screen, and it is the address. It inherits the
   banner's colour rather than `--colour-text`: it is part of the warning and
   not a page title that happens to sit inside one. `anywhere`, because an
   address is one long token with no spaces to break at and a truncated one on
   the screen for reading somebody's stash is the wrong thing to abbreviate. */

.support-whose__email {
  margin: 0;
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

.support-whose__back {
  margin-top: var(--space-2);
  align-self: flex-start;
  font-size: var(--text-meta);
}

/* The same link outside the strip, which is what the error state for an id
   that is not a person gets: there is no address to head the page with, and
   the list is the only useful thing left to offer. */

.support-back {
  margin: var(--space-4) 0 0;
  padding: 0 var(--space-4);
  font-size: var(--text-meta);
}

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

/* The whole row is one link, as a stash row is: the address identifies it and
   the count and the date are what he reads once he has found the person, so
   004's rule puts the emphasis on the first and quietens the rest. */

.person-row {
  border-bottom: 1px solid var(--colour-border-soft);
}

.person-row__link {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-4);
  color: inherit;
  text-decoration: none;
}

.person-row__email {
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--colour-text);
  overflow-wrap: anywhere;
}

.person-row__meta {
  font-size: var(--text-meta);
  color: var(--colour-text-quiet);
}

/* A row that is not a control must not look like one. `stash-row__link` is an
   `<a>` on her own stash and a `<span>` here, so every hover and focus
   affordance the anchor carries has to be switched off rather than inherited
   by a thing nothing can do anything with. */

span.stash-row__link {
  cursor: default;
}

@media (min-width: 48rem) {
  .app .screen {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    column-gap: var(--space-6);
    padding: 0 var(--space-5) var(--space-7);
  }

  /* 006's form is one column at every width. A two-column form asks her to
     read in a Z rather than down, and this one's whole job is to be answered in
     order. */

  .app .screen--form {
    display: block;
    padding: 0 var(--space-5) var(--space-7);
  }

  .page-title {
    padding: var(--space-5) 0 var(--space-3);
  }

  .banner,
  .add-form,
  .lede {
    margin-inline: 0;
  }

  .add {
    padding-inline: 0;
  }

  /* 007's form is one column too. The screen's own padding takes over here, so
     the block holding the phone's gutter gives it up and the identity block
     above the form keeps only its vertical rhythm. */

  .lot {
    padding-inline: 0;
  }

  /* 009's screen is one column too, and at this width the row fits on one
     line. The screen's own padding takes over, so the blocks holding the
     phone's gutter give it up. */

  .name-add,
  .name-row {
    padding-inline: 0;
  }

  .name-status {
    margin-inline: 0;
  }

  /*
   * 026 stays one column too — the design brief's Open 8, answered with its own
   * default.
   *
   * This is the first screen in the app with two separable blocks on it, so it
   * is the first place a second column is even arguable: the form on the left,
   * the colours on the right. It stays one column because the two blocks are
   * read in order rather than compared — she corrects the fibre, then looks at
   * the colours — and because a form beside a list is a screen whose two halves
   * scroll independently on a laptop and not on a phone, which is two layouts
   * to keep agreeing rather than one.
   *
   * The screen's own padding takes over, so every block holding the phone's
   * gutter gives it up. The lot rows keep their indent, because that indent is
   * what makes them belong to the colour above them at any width.
   */

  .yarn {
    padding-inline: 0;
  }

  .yarn-colours,
  .colour-status {
    margin-inline: 0;
  }

  .colour__head {
    padding-inline: 0;
  }

  .lot-row__link {
    padding-inline: var(--space-5) var(--space-2);
  }

  .colour__used-up {
    padding-inline: var(--space-5) 0;
  }

  .colour__add {
    margin-inline: var(--space-5) 0;
  }

  .name-row__name {
    flex: 1 1 auto;
  }

  .name-row__count {
    flex: 0 0 auto;
  }

  /* Explicit rows, because the bar sits between the action and the summary in
     the DOM — the reading and tab order — while spanning the width they share
     a line across. */

  .action {
    grid-column: 1;
    grid-row: 1;
    padding: var(--space-5) 0 var(--space-4);
  }

  .action__capture {
    padding: 0 var(--space-5);
  }

  .summary {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    padding: var(--space-5) 0 var(--space-4);
    text-align: right;
  }

  .summary__items {
    justify-content: flex-end;
  }

  /* 113. The strip that names the person on support.html, beside its
     counts line; since 234 that line is the only summary block there. */

  #whose {
    grid-column: 1;
    grid-row: 1;
  }

  .filter {
    grid-column: 1 / -1;
    grid-row: 2;
    padding: 0 0 var(--space-4);
  }

  /* One line while it fits, with the box given the room left: it is the
     shop lookup and the most-used control on the screen. 185: a dropdown
     keeps its words, so when they do not fit beside the box they take a
     line of their own rather than being cut. */

  .filter-bar {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
  }

  .filter-bar__search {
    flex: 1 1 16rem;
  }

  .filter-bar__designer {
    flex: 0 1 12rem;
    min-width: 9rem;
  }

  .filter-bar__controls {
    flex: 0 1 auto;
    gap: var(--space-3);
  }

  .content {
    grid-column: 1 / -1;
    grid-row: 3;
  }

  .app .state {
    margin: var(--space-4) 0;
  }

  /* 047: the picture column belongs to the row and the link keeps the other
     four. The anchor is **not** `display: contents` — that would take away its
     box, and with it the hover ground and the focus ring. The text columns
     still line up down the list, because every link has the same width. */

  .stash-row {
    align-items: center;
    gap: var(--space-5);
  }

  .stash-row > .stash-row__slot,
  .stash-row > .stash-row__open {
    padding: var(--space-2) 0 var(--space-2) var(--space-2);
  }

  .stash-row__link {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 7rem 9rem 12rem;
    align-items: center;
    gap: var(--space-5);
    padding: var(--space-2) var(--space-2) var(--space-2) 0;
    border-radius: var(--radius-medium);
  }

  .stash-row__body,
  .stash-row__main {
    display: contents;
  }

  .stash-row__identity {
    font-size: var(--text-meta);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .stash-row__brand {
    display: inline;
    margin: 0;
    font-size: inherit;
  }

  .stash-row__brand::after {
    content: " · ";
  }

  .stash-row__tags {
    margin-top: 0;
    flex-wrap: nowrap;
    min-width: 0;
  }

  .stash-row__note {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The door at desktop widths: the drawing takes the full 176 px here because
     there is room for it — and 176 px is the ceiling, not a step on the way to
     a larger one. */

  .signin__art {
    width: 176px;
    height: 176px;
  }

  .signin {
    padding: var(--space-7) var(--space-5);
  }

  .signin__lede {
    font-size: var(--text-body);
  }

  /* 157: on a computer the search dialog is centred, 32 rem wide at most,
     80 % of the window tall at most, the list scrolling inside it. */

  .search-dialog {
    width: 32rem;
    height: fit-content;
    max-height: 80vh;
    margin: auto;
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-medium);
    box-shadow: var(--shadow-low);
  }
}

/* --- 062: pattern-add.html, one pattern from her phone ------------------- */

.pattern-add {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Which file is being read: on a phone the picker has closed, and nothing else
   on the screen says so. Quiet, because it is a reminder and not a heading. */

.pattern-add__file {
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  overflow-wrap: anywhere;
}

.pattern-add__status {
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
  line-height: var(--line-body);
}

.pattern-add__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0;
}

/* The two long fields grow downwards, which is why they come last. */

textarea.field__input {
  padding: var(--space-2) var(--space-3);
  line-height: var(--line-body);
  resize: vertical;
}

.pattern-add__choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
}

.pattern-add__choice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
}

/* Åbn is a link, so a new tab opens from her tap (062): drawn as the button it
   replaced, and quiet until the link is signed. */

a.button {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

a.button[aria-disabled="true"] {
  cursor: default;
  opacity: 0.6;
}

/* --- 063: patterns.html, finding a pattern ------------------------------- */

/* One gutter, on the screen rather than on each block inside it — `lot.html`'s
   reasoning: `.app .screen` has none below the breakpoint, and nothing here
   runs edge to edge the way the stash list's hairlines do. Walked on her phone
   at 063's M7 with the text touching both edges, which is what the missing
   gutter looks like. One column at every width, like the forms: the wide
   screen's two-column grid would put the list beside its own search. */

.app .patterns,
.app .status,
.app .ravelry,
.app .pattern-new {
  display: block;
  padding-inline: var(--space-4);
}

.patterns .page-title,
.patterns .state,
.status .page-title,
.status .state,
.status .summary,
.ravelry .page-title,
.ravelry .state,
.pattern-new .page-title,
.pattern-new .state,
.pattern-new .banner {
  padding-inline: 0;
  margin-inline: 0;
}

@media (min-width: 48rem) {
  .app .patterns,
  .app .status,
  .app .ravelry,
  .app .pattern-new {
    padding-inline: var(--space-5);
  }
}

.patterns__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
}

/* 185: the head holds the title's spacing, so the title and the button are
   centred on each other with the gap beneath both, never the title's
   uneven padding centred against a button touching the bar below. */

.patterns__head,
.projects__head {
  padding-block: var(--space-4) var(--space-3);
}

.patterns__head .page-title,
.projects__head .page-title {
  padding-block: 0;
}

@media (min-width: 48rem) {
  .patterns__head,
  .projects__head {
    padding-top: var(--space-5);
  }
}

/* 105: one quiet line or nothing, so an empty region takes no space. */

.ravelry-news__line {
  margin: var(--space-2) 0 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.ravelry-news__link {
  color: var(--colour-accent);
}

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

.patterns__summary,
.patterns__shown {
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

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

/* Emphasis on what she travels by: the title leads, the facts are quiet, and
   the description is small but full contrast, because it is what 063 asks her
   to judge. */

.pattern-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3);
}

/* 171: the thumbnail slot and the title share a line — the title takes the
   width, the slot holds the stash row's photograph size at the start. */

.pattern-row__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* 195: the title and the facts line stacked beside the thumbnail. */

.pattern-row__text {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.pattern-row__title {
  display: block;
  padding: var(--space-1) 0 0;
  color: var(--colour-text);
  font-size: var(--text-body);
  font-weight: 600;
  text-decoration: none;
}

.pattern-row__meta {
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.pattern-row__description {
  margin: 0;
  color: var(--colour-text);
  font-size: var(--text-meta);
  line-height: var(--line-body);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* 093: two lines until the row is opened, which is what fits more than two
   patterns on her phone. Open, the whole description and the gauge. */

.pattern-row--open .pattern-row__description {
  display: block;
  -webkit-line-clamp: none;
  line-clamp: none;
  overflow: visible;
}

.pattern-row__gauge {
  margin: 0;
  color: var(--colour-text);
  font-size: var(--text-meta);
  line-height: var(--line-body);
  white-space: pre-line;
}

.pattern-row__gauge-label {
  color: var(--colour-text-quiet);
}

.pattern-row__more {
  align-self: flex-start;
  min-height: 24px;
  padding: var(--space-1) 0;
  border: none;
  background: none;
  color: var(--colour-accent);
  font: inherit;
  font-size: var(--text-meta);
  text-decoration: underline;
  cursor: pointer;
}

/* 062: Åbn and Gem in a row of buttons on pattern-add.html, sized to their
   text, with no room claimed above them. */

.pattern-add__actions .button--primary {
  width: auto;
  margin-top: 0;
}

/* --- 089: status.html, who is signed in and what is in each module -------- */

/* The gutter is the pattern screens' rule above, which names this screen too.
   `.summary` is borrowed from `stash.html` for its type and its separators,
   and its own inset and its wide-screen grid placement are undone here: this
   page is one column at every width. */

.status__account {
  margin: 0 0 var(--space-5);
  overflow-wrap: anywhere;
}

.status__section {
  margin-bottom: var(--space-5);
}

.status .summary {
  padding-block: 0 var(--space-2);
  text-align: left;
}

.status__link {
  display: inline-block;
  padding-block: var(--space-2);
}

/* Her Ravelry library, 104: one row per library entry — the title, the
   designer, date and language beneath it quietly, then the state and the one
   thing she can do about it. The rows are the pattern list's rhythm. */

.ravelry__updates {
  margin: 0 0 var(--space-3);
}

.ravelry__updates[hidden] {
  display: none;
}

/* 109: what she has hidden and the way to hide the rest, on one line that
   wraps; the confirmation for skipping everything opens in the button's
   place. */

.ravelry__skipbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin: 0 0 var(--space-3);
}

.ravelry__skipbar[hidden],
.ravelry__toggle[hidden],
.ravelry__all-skipped[hidden],
.ravelry-row[hidden] {
  display: none;
}

.ravelry__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
}

.ravelry__bulk .button {
  width: auto;
  margin-top: 0;
  padding-inline: var(--space-4);
}

.ravelry__bulk .confirm {
  flex-basis: 100%;
}

.ravelry__skipped {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--text-meta);
}

.ravelry__skipped:empty {
  display: none;
}

.ravelry__all-skipped {
  margin: var(--space-4) 0;
  color: var(--colour-text-quiet);
}

.ravelry__list {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
}

/* One line of choice per entry: what it is on the left, what she can do on
   the right. The app's primary button is a full-width form action, and twenty
   of them read as twenty forms (his, at M9), so here it keeps to its text. */
.ravelry-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--space-3);
  align-items: center;
  padding: var(--space-3);
}

.ravelry-row__title,
.ravelry-row__meta {
  grid-column: 1;
}

.ravelry-row__title {
  margin: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.ravelry-row__meta {
  margin: var(--space-1) 0 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.ravelry-row__action {
  grid-column: 2;
  grid-row: 1 / span 2;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
}

.ravelry-row__action .button {
  width: auto;
  min-height: var(--tap-target);
  margin-top: 0;
  padding-inline: var(--space-4);
}

.ravelry-row__state {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.ravelry-row__note {
  grid-column: 1 / -1;
  margin: var(--space-1) 0 0;
  font-size: var(--text-meta);
}

.ravelry-row__note:empty {
  display: none;
}

/* Forbrug — 122. Three sections in one column: the search — since 157 a
   button opening the dialog, whose rows carry a radio per lot — the entry
   for the chosen lot, and the list of what is about to post. The radio is
   drawn, not hidden as the filter bar's are: a chosen lot is the native
   control's own mark and never a colour alone. */

.usage-search {
  padding: 0 var(--space-4);
}

.usage-list__status {
  margin: var(--space-2) 0 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* The count sits outside the list's section, so it survives the section
   hiding after a post — and so it owes its own gutter. His, on the phone at
   M4: it ran to the edge. */

.usage-list__status {
  margin-top: var(--space-3);
  padding-inline: var(--space-4);
}

.usage-list {
  margin-bottom: 0;
}

.usage-lot {
  border-bottom: 1px solid var(--colour-border);
}

.usage-lot__label {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-target);
  padding: var(--space-2) 0;
  cursor: pointer;
}

.usage-lot__label:hover {
  background: var(--colour-surface);
}


.usage-lot__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.usage-lot__meta,
.usage-lot__note {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
  font-variant-numeric: tabular-nums;
}

.usage-entry,
.usage-list {
  padding-block: var(--space-4) 0;
  padding-inline: var(--space-4);
}

.usage-chosen {
  margin: 0 0 var(--space-3);
  font-weight: 600;
}

.usage-chosen__remaining {
  font-variant-numeric: tabular-nums;
}

.usage-unweighed {
  margin: 0 0 var(--space-3);
}

.usage-kinds {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0 0 var(--space-4);
  padding: 0;
  border: none;
}

.usage-kinds .section__legend {
  flex-basis: 100%;
}

.usage-kind {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  cursor: pointer;
}

.usage-kind__radio {
  width: var(--space-5);
  height: var(--space-5);
  margin: 0;
  accent-color: var(--colour-accent);
}

/* The grams, their unit and **Resten** on one line beneath the label. */

.usage-grams {
  grid-template-columns: 1fr auto auto;
  grid-template-areas: "label label label" "input unit rest";
}

.usage-grams__rest {
  grid-area: rest;
}

.usage-list__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-body);
  font-weight: 600;
}

.usage-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.usage-line__text {
  min-width: 0;
}

.usage-line__remove {
  flex: none;
}

.usage-list .button--block {
  margin-top: var(--space-4);
}

/* 123: a locked lot's figures are facts under one sentence, plain text rather
   than an alert — true on load and true after. */

.lot-locked {
  margin: 0 0 var(--space-3);
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
  line-height: var(--line-body);
}

/* 123: the history, since 159 a card between the saved line and the delete,
   each entry a lot row: the kind and the day as the main line, the details
   beneath, the grams at the right. The row's own line is not a link, so it
   carries the link's layout under its own name. */

.lot-row__line {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-target);
  padding: var(--space-2) 0;
}

/* 127: a line's verbs beneath its text, and the form or confirmation a line
   turns into while she answers. */

.lot-history__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding-bottom: var(--space-2);
}

.lot-history__item--open {
  padding-block: var(--space-2);
}

.lot-history__return .field {
  margin-top: var(--space-2);
}

/* The frog offer: a line per lot, its checkbox above its grams. */

.frog-offer__intro {
  margin: 0 0 var(--space-3);
  font-size: var(--text-meta);
  color: var(--colour-text-quiet);
}

.frog-offer__line {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--colour-border-soft);
  font-size: var(--text-meta);
}

.frog-offer__field {
  margin-top: var(--space-1);
}

.frog-offer__go {
  margin-top: var(--space-4);
}

/* 140: the settle offer — the frog offer's lines, a lot's name above its
   field, and two buttons. */

.settle-offer__lot {
  margin: 0;
}

/* 152: a lot with no weight has no field, only the note leading to it. */
.settle-offer__unweighed {
  margin: var(--space-1) 0 0;
}

.settle-offer__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.settle-offer__buttons .button {
  width: auto;
  margin-top: 0;
}

/* A button drawn as *Registrér forbrug*'s link above it. */
.settle-offer__open {
  padding: 0;
  border: none;
  background: none;
  font-family: inherit;
  text-decoration: underline;
  cursor: pointer;
}

.lot-history__empty,
.lot-history__unweighed {
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
  line-height: var(--line-body);
}

.lot-history__register[hidden],
.lot-history__unweighed[hidden] {
  display: none;
}

/* 159, his at M6: Registrér forbrug is the Forbrug heading's action, the
   one tap she comes for, a link in the card action's clothes like every
   other heading action — his, the same day. */

.lot-history__unweighed {
  margin-bottom: var(--space-3);
}

/* --- 124: the project module's first two screens ------------------------ */

/* The list, `patterns.html`'s shape: the screen insets itself, the heading
   and *Opret projekt* share a row, and each row is one link. */

.app .projects,
.app .swatches {
  display: block;
  padding-inline: var(--space-4);
}

.projects .page-title,
.projects .state,
.projects .banner,
.swatches .page-title,
.swatches .state {
  padding-inline: 0;
  margin-inline: 0;
}

.projects__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
}

.projects__head .button[hidden] {
  display: none;
}

.project-list {
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

.project-row {
  padding: var(--space-3);
}

.project-row__link {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-height: var(--tap-target);
  padding: 0;
  color: var(--colour-text);
  text-decoration: none;
}

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

.project-row__name {
  font-size: var(--text-body);
  font-weight: 600;
}

.project-row__meta {
  line-height: var(--line-body);
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* 162: the status as a dot and its word in the status's own hue — the
   band's map, so the list says at a glance what the page says in colour.
   The one amber pill every state wore until then is gone. A word the list
   lacks has no modifier and stays in the line's quiet ink, without a dot. */

.project-row__status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-weight: 600;
}

.project-row__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
}

.project-row__status--planned {
  color: var(--colour-reserved);
}

.project-row__status--in_progress {
  color: var(--colour-accent);
}

.project-row__status--finished {
  color: var(--colour-finished);
}

.project-row__status--on_hold {
  color: var(--colour-hold);
}

.project-row__status--frogged {
  color: var(--colour-error);
}

/* The form, one gutter on the block as `lot.html`'s (`tests/gutter_test.js`
   names it). */

.project-edit {
  padding-inline: var(--space-4);
}

/* 167: the form is 158's page — a band from the saved status with the top Gem
   at its right, and the seven sections as cards inside the one form, in
   `.project-edit`'s gutter with the cards' gap. The flat paper's heading row,
   bottom row and section spacing went with it. */

.project-edit > .project-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.project-form__section {
  margin: 0;
  /* A fieldset is min-content wide by default, and a picker's one-line
     text made the Opskrift card wider than the screen — his, at M2. */
  min-width: 0;
}

/* The card's heading row already has a margin beneath it, so the body under
   it keeps none — 159's rule, read again here. */

.project-edit .project-card .section__body {
  margin-top: 0;
}

/* Gem under the last card claims no room above it: the gap is the cards'. */

.project-edit > .button--primary {
  margin-top: 0;
}

/* The top Gem in the band, sized to its word, where Rediger stands. */

.project-band__edit.button--primary {
  width: auto;
  margin-top: 0;
}

/* Status: five radios in a row that wraps, Forbrug's kind shape. */

.project-status {
  margin: 0;
  padding: 0;
  border: none;
}

.project-status__choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-1);
}

.project-status__choice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  cursor: pointer;
}

.project-status__radio {
  width: var(--space-5);
  height: var(--space-5);
  margin: 0;
  accent-color: var(--colour-accent);
}

/* The pattern picker: the chosen pattern as one line with Fjern at its end,
   and beneath it the button that opens the search dialog (157). The rows
   are drawn inside the dialog's list. */

.pattern-picker__item {
  border-bottom: 1px solid var(--colour-border);
}

.pattern-picker__label {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-target);
  padding: var(--space-2) 0;
  cursor: pointer;
}

.pattern-picker__label:hover {
  background: var(--colour-surface);
}

/* 190, his at the walk: *"do the list need to have a radio button at the
   start?"* The radio stays — on a fine pointer an arrow key moves through
   them and Vælg confirms, 148's split — but drawn nowhere: the row is the
   target, and the current choice carries a check at its end instead. */

.pattern-picker__radio,
.usage-lot__radio {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}

.pattern-picker__label,
.usage-lot__label {
  position: relative;
}

.pattern-picker__label::after,
.usage-lot__label::after {
  content: "";
  flex: none;
  width: 0.45rem;
  height: 0.85rem;
  margin: 0 var(--space-2) 0 auto;
  border-right: 2px solid var(--colour-accent);
  border-bottom: 2px solid var(--colour-accent);
  transform: rotate(45deg) translate(-2px, -2px);
  visibility: hidden;
}

.pattern-picker__label:has(.pattern-picker__radio:checked)::after,
.usage-lot__label:has(.usage-lot__radio:checked)::after {
  visibility: visible;
}

.pattern-picker__label:has(.pattern-picker__radio:checked),
.usage-lot__label:has(.usage-lot__radio:checked) {
  color: var(--colour-accent);
  font-weight: 600;
}

.pattern-picker__label:has(.pattern-picker__radio:focus-visible),
.usage-lot__label:has(.usage-lot__radio:focus-visible) {
  outline: 2px solid var(--colour-accent);
  outline-offset: -2px;
}

.pattern-picker__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.pattern-picker__designer {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* Privat: the box and its word on one line, the hint beneath. */

.project-form__check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  cursor: pointer;
}

.project-form__checkbox {
  width: var(--space-5);
  height: var(--space-5);
  margin: 0;
  accent-color: var(--colour-accent);
}

@media (min-width: 48rem) {
  /* The screen's own padding takes over, so the block holding the phone's
     gutter gives it up — `.lot`'s rule. */
  .project-edit {
    padding-inline: 0;
  }
}

/* --- 126: project.html, and the pictures on the form ---------------------- */

/* The view's block, one gutter on the screen as `lot.html`'s
   (`tests/gutter_test.js` names it). */

.project-view {
  padding-inline: var(--space-4);
}

.project-view__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  padding-inline: var(--space-4);
}

.project-view__head .page-title {
  padding-inline: 0;
}

.project-view__head .button {
  width: auto;
  margin-top: 0;
}

.project-view__saved {
  padding-inline: var(--space-4);
}

.project-view__saved .saved {
  margin-top: var(--space-2);
}

.project-view__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding-block: var(--space-4);
}

.project-view__facts {
  margin: 0;
}

.project-view__pattern {
  display: flex;
  flex-direction: column;
}

.project-view__designer {
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

.project-view__yarn .section__legend,
.project-view__note .section__legend {
  display: block;
  margin: 0 0 var(--space-2);
  font-size: var(--text-small);
  font-weight: 600;
}

.project-view__yarn-none {
  margin: 0 0 var(--space-2);
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* --- 158: the project page as a band and cards ---------------------------- */

/* The band: full column width under the header, tinted by status with a
   hairline beneath in the same hue. The grey of *På pause* is the base, so a
   status the list lacks is grey rather than unstyled; each state overrides
   the three colours in turn. Written out per state rather than through a
   local custom property, because every token is declared once
   (tests/tokens_test.js). */

.project-band,
.lot-band,
.pattern-band {
  padding: var(--space-4);
  border-bottom: 1px solid var(--colour-hold-border);
  background: var(--colour-hold-surface);
  color: var(--colour-hold);
}

.project-band--planned {
  border-bottom-color: var(--colour-reserved-border);
  background: var(--colour-reserved-surface);
  color: var(--colour-reserved);
}

.project-band--in_progress {
  border-bottom-color: var(--colour-accent-border);
  background: var(--colour-accent-soft);
  color: var(--colour-accent);
}

.project-band--finished {
  border-bottom-color: var(--colour-finished-border);
  background: var(--colour-finished-surface);
  color: var(--colour-finished);
}

.project-band--frogged {
  border-bottom-color: var(--colour-error-border);
  background: var(--colour-error-surface);
  color: var(--colour-error);
}

/* The kicker, the status word and the dot inherit the band's colour; the
   name and the meta line are the page's own inks. Since 192 the one kicker
   left is the project form's label, no longer a link: the way back is the
   header's. */

.project-band__kicker {
  display: inline-block;
  margin: 0 0 var(--space-1);
  color: inherit;
  font-size: var(--text-micro);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.project-band__row,
.lot-band__row,
.pattern-band__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

.project-band__text,
.lot-band__text,
.pattern-band__text {
  min-width: 0;
}

.project-band__title,
.lot-band__title,
.pattern-band__title {
  margin: 0;
  color: var(--colour-text);
  font-size: var(--text-hero);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: var(--line-tight);
  overflow-wrap: break-word;
  text-wrap: balance;
}

.project-band__status,
.lot-band__status,
.pattern-band__status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  font-size: var(--text-meta);
  font-weight: 600;
}

.project-band__dot,
.lot-band__dot,
.pattern-band__dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: currentColor;
}

.project-band__meta,
.lot-band__meta,
.pattern-band__meta {
  margin: var(--space-1) 0 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

/* Rediger: an outline button, the control's edge on a white ground, so the
   edge is told from the fill at 3:1 whatever the band's tint. */
.button--outline {
  border-color: var(--colour-border-control);
  background: var(--colour-surface);
  color: var(--colour-accent);
  font-weight: 600;
  text-decoration: none;
}

.project-band__edit {
  flex: none;
  display: inline-flex;
  align-items: center;
  margin-top: var(--space-1);
}

/* The cards: one gap between them, each a white surface with the soft
   border, the large radius and the low shadow, one gutter of padding. */

.project-view--cards {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding-block: var(--space-4);
}

.project-card {
  /* A card is a <section>, and the page's rule gives one a 2 rem bottom
     margin; here the container's gap is the whole space between cards,
     the same 1 rem as between the band and the first — his, at 159's M6. */
  margin: 0;
  /* 169: a fieldset is min-content wide by default, and a card that is one
     grew past the screen on a long picker line at 167's M2. */
  min-width: 0;
  padding: var(--space-4);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-large);
  background: var(--colour-surface);
  box-shadow: var(--shadow-low);
}

.project-card--pictures {
  padding: var(--space-2);
}

.project-card__heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin: 0 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--colour-border-soft);
}

.project-card__title {
  margin: 0;
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: var(--line-tight);
}

.project-card__sum {
  margin-left: var(--space-1);
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

.project-card__action {
  flex: none;
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--colour-accent);
  font-size: var(--text-meta);
  font-weight: 600;
  text-decoration: none;
}

.project-card__action:hover {
  text-decoration: underline;
}

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

.project-card--facts .facts {
  margin: 0;
  gap: var(--space-2) var(--space-4);
}

.project-card--facts .facts__value {
  font-size: var(--text-body);
}

/* 151: *Sat til side* and *Brugt*, drawn only when both lists have rows, as
   small spaced capitals so the card still reads as one *Garn*. */
.project-card__group {
  margin: 0 0 var(--space-2);
  color: var(--colour-text-quiet);
  font-size: var(--text-micro);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.lot-rows + .project-card__group {
  margin-top: var(--space-4);
}

/* A lot row: the colour's square, the colour as the main line with brand ·
   yarn beneath, and the grams at the right in tabular figures. */

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

.lot-row + .lot-row {
  border-top: 1px solid var(--colour-border-soft);
}

.lot-row__link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-target);
  padding: var(--space-2) 0;
  color: var(--colour-text);
  text-decoration: none;
}

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

/* The slot's full size here, not the stash row's half: the square is the
   row's picture. */
.lot-row__link .swatch-space {
  width: var(--photo-slot);
  height: var(--photo-slot);
}

.lot-row__link .swatch {
  border-radius: var(--radius-medium);
}

.lot-row__body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.lot-row__colour {
  font-size: var(--text-body);
  font-weight: 600;
  line-height: var(--line-tight);
  overflow-wrap: break-word;
}

.lot-row__yarn {
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  line-height: var(--line-tight);
}

.lot-row__figure {
  flex: none;
  font-size: var(--text-body);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.lot-row__unit {
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  font-weight: 400;
}

/* The foot: Registrér forbrug and, closed, Ret forbruget on one row above a
   hairline; the settle offer's wrapper is transparent to the row so its one
   link sits beside the other, and either offer open takes the whole row. */
.project-card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--colour-border-soft);
}

/* 200: the dialog for yarn from outside the stash — 157's full-screen
   shape, the three fields at the top and the buttons right beneath them,
   where a phone's keyboard cannot cover them. */

.other-yarn-dialog__form {
  gap: var(--space-3);
  overflow-y: auto;
}

.other-yarn-dialog__title {
  margin: 0;
  font-size: var(--text-title);
}

.other-yarn-dialog__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin: 0;
}

.other-yarn-dialog__confirm {
  flex: 1 1 100%;
}

/* 200: two things in the yarn card are buttons that look like their
   neighbours — the line from outside the stash, a lot row that opens a
   dialog rather than a page, and the foot's **＋ Garn uden for lageret**
   beside a link. Only what a button brings of its own is taken away. */

.lot-row__link--button,
.facts__link--button {
  border: 0;
  background: none;
  font-family: inherit;
  cursor: pointer;
}

.lot-row__link--button {
  width: 100%;
  font-size: inherit;
  text-align: start;
}

.facts__link--button {
  padding: 0;
  text-decoration: underline;
}

/* 200, his at the walk: the yarn card's two everyday actions as buttons
   once a project is under way; an <a> drawn as one lines its words up as a
   <button> does. */

.project-card__button {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

.project-card__foot .settle-offer {
  display: contents;
}

.project-card__foot .settle-offer__section,
.project-card__foot .frog-offer {
  flex: 1 1 100%;
}

.project-yarn {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--colour-border-soft);
}

.project-yarn__item {
  border-bottom: 1px solid var(--colour-border-soft);
}

.project-yarn__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--tap-target);
  padding: var(--space-2) 0;
  color: var(--colour-text);
  font-size: var(--text-meta);
  text-decoration: none;
}

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

.project-yarn__grams {
  flex: none;
  color: var(--colour-text-quiet);
}

.project-view__note-text {
  margin: var(--space-1) 0 0;
  font-size: var(--text-meta);
  line-height: var(--line-body);
  white-space: pre-line;
  overflow-wrap: break-word;
}

/* The gallery: the main picture square-cropped to the column, the
   thumbnails as 64 px squares beneath in a row that scrolls sideways — 158
   took 126's ‹ and › off its ends. */

.project-gallery {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.project-gallery__open {
  display: block;
  width: 100%;
  max-width: 480px;
  padding: 0;
  border: none;
  border-radius: var(--radius-medium);
  background: none;
  font: inherit;
  color: inherit;
}

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

.project-gallery__main {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  color: var(--colour-text-faint);
  overflow: hidden;
}

.project-gallery__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.project-gallery__image[hidden] {
  display: none;
}

/* The strip: 2 px of padding so the shown thumbnail's outline is not
   clipped by the scroll box. */
.project-gallery__thumbs {
  display: flex;
  gap: var(--space-2);
  margin: 0;
  padding: 2px;
  list-style: none;
  overflow-x: auto;
}

.project-gallery__thumb-item {
  flex: none;
}

.project-gallery__thumb {
  display: block;
  width: 64px;
  height: 64px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  overflow: hidden;
  cursor: pointer;
}

.project-gallery__thumb[aria-current="true"] {
  border-color: var(--colour-accent);
}

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

.project-gallery__thumb-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.project-gallery__thumb-image[hidden] {
  display: none;
}

/* Billeder on the form: a strip of 64 px thumbnails, each with its
   controls beneath, the ＋ after the last, the count line, the sentence. */

.project-pictures__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* 167, his at M2: a picture is a row — the thumbnail at the left, its verbs
   on one line beside it, the bin for Slet — rather than a tile with the
   verbs stacked beneath, which left most of the card empty. */

.project-pictures__strip {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.project-pictures__tile {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.project-pictures__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-target);
}

.project-pictures__frame {
  display: block;
  width: 64px;
  height: 64px;
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  overflow: hidden;
}

.project-pictures__thumb {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.project-pictures__thumb[hidden] {
  display: none;
}

.project-pictures__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--space-4);
  min-width: 0;
}

.project-pictures__controls[hidden] {
  display: none;
}

.project-pictures__control {
  min-height: var(--tap-target);
  padding: 0;
  border: none;
  background: none;
  color: var(--colour-accent);
  font: inherit;
  font-size: var(--text-small);
  text-align: left;
  cursor: pointer;
}

.project-pictures__control[hidden] {
  display: none;
}

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

/* The ＋ in the heading, in the card action's clothes, a size up so a thumb
   finds it. */
.project-pictures__add {
  margin-left: var(--space-3);
  padding: 0 var(--space-2);
  border: none;
  background: none;
  font: inherit;
  font-size: var(--text-title);
  line-height: 1;
  cursor: pointer;
}

.project-pictures__add::before {
  content: "＋";
}

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

.project-pictures__confirm {
  padding: var(--space-2) 0;
}

.project-pictures__confirm-sentence {
  margin: 0 0 var(--space-2);
  font-size: var(--text-small);
}

.project-pictures__confirm-buttons {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.project-pictures__confirm-buttons .button {
  width: auto;
  margin-top: 0;
}

.project-pictures__status:empty {
  display: none;
}

/* 167, his: the count sentence stands in the card's heading; this line is
   for what is happening and takes no room while nothing is. */

.project-pictures__status:empty {
  display: none;
}

.project-pictures__status {
  margin: var(--space-3) 0 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.project-pictures__note {
  margin: var(--space-1) 0 0;
  color: var(--colour-error);
  font-size: var(--text-meta);
}

.project-pictures__note[hidden] {
  display: none;
}

.project-pictures__save-first {
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* The list's slot, the stash row's at 44 px. */

.project-row__link {
  flex-direction: row;
  align-items: center;
  gap: var(--space-3);
}

.project-row__slot {
  display: flex;
  flex: none;
  width: var(--photo-card);
}

.project-row__photo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--photo-card);
  height: var(--photo-card);
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  color: var(--colour-text-faint);
}

.project-row__body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

@media (min-width: 48rem) {
  /* The screen's own padding takes over, `.project-edit`'s rule. */
  .project-view,
  .project-view__head,
  .project-view__saved {
    padding-inline: 0;
  }

  /* Pictures left, the facts right — the stash's two-column grid. */
  .project-view {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-5);
    align-items: start;
  }

  .project-view__body {
    padding-top: var(--space-4);
  }

  /* 158: the project page stays one column at every width; the two-column
     grid above is `shared.html`'s now. */
  .project-view--cards {
    display: flex;
  }
}

/* 131: Forbrug opened from a project — the way back, the title, and the
   project's name beneath it, in the search's gutter. */

.usage-project {
  padding: var(--space-4) var(--space-4) 0;
}

.usage-project .page-title {
  padding: 0;
}

.usage-project__back {
  display: inline-block;
  margin: 0 0 var(--space-2);
  font-size: var(--text-meta);
}

.usage-project__name {
  margin: var(--space-1) 0 var(--space-4);
  color: var(--colour-text-quiet);
}

/* 128: the public link on project.html, beneath the heading row. */

.project-share {
  padding-inline: var(--space-4);
}

.project-share__row {
  margin-top: var(--space-2);
}

.project-share__row .button {
  width: auto;
  margin-top: 0;
}

.project-share__address {
  overflow-wrap: anywhere;
  user-select: all;
  font-size: var(--text-meta);
}

/* 138: the fireworks on a finished project. Fixed over the viewport and
   taking no taps, so she can scroll and tap through it; removed when done. */

.fireworks {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 100;
}

/* 138, his: the line that comes with the fireworks is flashy too — larger,
   bold, a pop as it arrives, and the app's three dark colours sliding through
   it for the length of the fireworks. Dark ones only: the text still has to be
   read on the page's ground. */

.saved__label--finished {
  display: inline-block;
  font-size: var(--text-title);
  font-weight: 700;
  background: linear-gradient(
    90deg,
    var(--colour-accent),
    var(--colour-reserved),
    var(--colour-error),
    var(--colour-accent)
  );
  background-size: 300% 100%;
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  animation:
    finished-pop 1500ms cubic-bezier(0.2, 1.6, 0.4, 1) both,
    finished-shimmer 3s linear both;
}

@keyframes finished-pop {
  from {
    transform: scale(0.6);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes finished-shimmer {
  from {
    background-position: 0% 50%;
  }
  to {
    background-position: 100% 50%;
  }
}

/* --- 141: a colour's square ------------------------------------------------ */

/* The yarn's colours as a read saw them. The values are data, not design, so
   they arrive as --swatch-1…3 on the element; two or three are stripes, most
   of the ball first. The border keeps a pale yarn visible on the page. */

.swatch {
  display: block;
  width: 100%;
  height: 100%;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-small);
  background: var(--swatch-1);
}

/* The column a list row keeps for it: half the photograph's slot, centred in
   the row, and kept when a colour has none so the text lines up. */

.swatch-space {
  flex: none;
  align-self: center;
  width: calc(var(--photo-slot) / 2);
  height: calc(var(--photo-slot) / 2);
}

.swatch--2 {
  background: linear-gradient(
    to right,
    var(--swatch-1) 50%,
    var(--swatch-2) 50%
  );
}

.swatch--3 {
  background: linear-gradient(
    to right,
    var(--swatch-1) 33.34%,
    var(--swatch-2) 33.34% 66.67%,
    var(--swatch-3) 66.67%
  );
}

/* On add.html: the square, what it is, and the way to drop it. */

.colour-seen {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  font-size: var(--text-meta);
  color: var(--colour-text-quiet);
}

.colour-seen .swatch {
  width: calc(var(--photo-slot) / 2);
  height: calc(var(--photo-slot) / 2);
}

.colour-seen__remove {
  padding: 0;
  cursor: pointer;
}

/* --- 149: the menu's rows; 187 took its module switch for the tab bar ---- */

/* A module with no items of its own draws no empty band. */
.header__items:empty {
  display: none;
}

/* Her account last, smaller and quieter than the module's own items. */
.header__account {
  border-top: 1px solid var(--colour-border-soft);
}

.header__account .header__item {
  min-height: calc(var(--tap-target) - var(--space-2));
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* --- 169: the other forms as cards ---------------------------------------- */

/* Tilføj garn: a section not yet her turn is the card with its heading alone,
   dimmed as a whole — the legend alone used to say *this comes next*. The gap
   between cards stays 080's, as close as fields. */

.section--waiting {
  opacity: 0.55;
}

.add-form .project-card .section__body:empty,
.add-form .project-card .section__picker:empty {
  display: none;
}

.add-form .project-card .section__body {
  margin-top: 0;
}

.add-form .project-card .section__picker:not(:empty) + .section__body:not(:empty) {
  margin-top: var(--space-3);
}

/* The yarn page: the band full width, the card in `.yarn`'s gutter, and
   *Farver* as a heading row above 164's colour cards. */

.yarn-page > .yarn {
  padding-block: var(--space-4);
}

.yarn-card .section__body {
  margin-top: 0;
}

.yarn-colours {
  margin: var(--space-6) var(--space-4) 0;
}

/* Forbrug: three cards, each carrying its own gutter as a margin, since the
   screen has none; the status line beneath keeps its own. */

.usage-search,
.usage-entry,
.usage-list {
  margin-inline: var(--space-4);
  padding: var(--space-4);
}

.usage-entry,
.usage-list {
  margin-top: var(--space-4);
}

.usage-search .choice-field,
.usage-search .field {
  margin-bottom: 0;
}

/* Tilføj opskrift: Filen over 161's two cards, in the page's gutter. */

.pattern-add__cards {
  padding-block: var(--space-2) 0;
}

.pattern-add__file-card .pattern-add__file,
.pattern-add__file-card .pattern-add__status {
  margin: 0;
}

.pattern-add__file-card .project-card__heading .button--primary {
  width: auto;
  margin-top: 0;
}

.pattern-add__cards .add-form {
  gap: var(--space-4);
}

.pattern-add__cards > .add-form > .button--primary {
  margin-top: 0;
}

/* 169, his at M2: the way back to the project above Tilføj garn, in Forbrug's
   shape — a quiet line in the gutter rather than a bare link in the corner. */

.add-back:not(:empty) {
  padding: var(--space-4) var(--space-4) 0;
}

.add-back:not(:empty) + .page-title {
  padding-top: var(--space-2);
}

/* 172: a file that would not open, said on its row after a tap, in the
   facts line's trailing span where *Åbner…* stands. */

.pattern-row__failed {
  color: var(--colour-error);
}

/* --- 171: the thumbnail on a pattern row ---------------------------------- */

/* The slot at the left of the title line, the project row's square: page one
   when the row has a thumbnail, the picture icon when it has none. It is the
   row's opener, the title having become the way to the edit page. */

.pattern-row__head {
  align-items: flex-start;
}

.pattern-row__slot {
  display: flex;
  flex: none;
  width: var(--photo-card);
  height: var(--photo-card);
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  color: var(--colour-text-faint);
  overflow: hidden;
  align-items: center;
  justify-content: center;
}

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

.pattern-row__slot .photo-slot__image {
  width: var(--photo-card);
  height: var(--photo-card);
}

/* The band of the pattern page: the thumbnail where Åbn stands, a size up. */

.pattern-band__thumbnail {
  display: flex;
  flex: none;
  width: 4.5rem;
  height: 4.5rem;
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  overflow: hidden;
  margin-top: var(--space-1);
}

.pattern-band__thumbnail .photo-slot__image {
  width: 4.5rem;
  height: 4.5rem;
}

/* --- 186: Hjem ------------------------------------------------------------ */

/* One column at every width, as the pattern and project lists: the wide
   screen's grid would put the cards beside the actions. */

.app .home {
  display: block;
  padding-inline: var(--space-4);
}

.home .page-title,
.home .state {
  padding-inline: 0;
  margin-inline: 0;
}

/* The cards scroll sideways on a phone, each a little under the width so
   the next one shows at the edge and says there are more. */

.home-cards--row {
  display: grid;
  grid-auto-columns: 85%;
  grid-auto-flow: column;
  gap: var(--space-3);
  margin: 0 calc(-1 * var(--space-4));
  padding: 0 var(--space-4) var(--space-2);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--space-4);
  list-style: none;
}

.home-card {
  scroll-snap-align: start;
}

.home-card__link {
  position: relative;
  display: block;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-large);
  background: var(--colour-accent);
  color: var(--colour-accent-text);
  text-decoration: none;
  overflow: hidden;
}

/* His, at 186's walk: the words over the picture, on a scrim that darkens
   its foot, and the accent where there is no picture yet. */

.home-card__picture {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--colour-accent-soft);
}

.home-card__picture .photo-slot__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.home-card__words {
  position: absolute;
  inset: auto 0 0;
  display: flex;
  flex-direction: column;
  padding: var(--space-6) var(--space-3) var(--space-3);
  background: linear-gradient(to top, var(--colour-photo-scrim), transparent);
}

.home-card__name {
  font-size: var(--text-title);
  font-weight: 600;
  line-height: var(--line-tight);
}

.home-card__started {
  font-size: var(--text-meta);
}

/* 230: the project page's one line for a background plan read. */
.project-view__job {
  margin: var(--space-2) 0 0;
  padding-inline: var(--space-4);
}

/* 230: a finished plan read she has not seen — a dot and its words. */
.home-card__new {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-meta);
  font-weight: 600;
}

.home-card__new::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background: var(--colour-accent);
}

.home__counts,
.home__waiting {
  margin: var(--space-3) 0 0;
  font-size: var(--text-meta);
}

.home__counts a,
.home__waiting a {
  color: var(--colour-accent);
}

/* Four equal buttons, two by two, each an icon above its words. */

.home__actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

.home__actions[hidden] {
  display: none;
}

.home__action {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: 6rem;
  padding: var(--space-3);
  border: 1px solid var(--colour-accent-border);
  border-radius: var(--radius-large);
  background: var(--colour-accent-soft);
  color: var(--colour-accent);
  font-weight: 600;
  text-align: center;
  text-decoration: none;
}

.home__action-note {
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  font-weight: 400;
}

/* 203: this month's statistics, one card, the whole of it the link. */

.home-stats {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding: var(--space-3);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-large);
  color: inherit;
  text-decoration: none;
}

.home-stats[hidden] {
  display: none;
}

.home-stats__words {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

/* 203, at the walk: six months of consumption, beside the words. */
.home-stats__chart {
  flex: none;
}

.home-stats__spark {
  display: block;
  width: 6rem;
  height: 3rem;
}

.home-stats__bar {
  fill: var(--colour-accent-border);
}

.home-stats__bar--now {
  fill: var(--colour-accent);
}

.home-stats__heading,
.home-stats__inout {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

.home-stats__figure {
  font-size: var(--text-title);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

@media (min-width: 48rem) {
  .app .home {
    padding-inline: var(--space-5);
  }

  /* A grid at the wide layout, nothing scrolling sideways. */

  .home-cards--row {
    grid-auto-flow: row;
    grid-template-columns: repeat(3, 1fr);
    margin: 0;
    padding: 0;
    overflow-x: visible;
  }

  .home__actions {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* --- 187: the tab bar ------------------------------------------------------ */

/* Fixed at the foot of the four top-level screens, on the page ground with a
   rule above it as 085's Gem bar, and the home indicator's inset beneath. */

.tab-bar {
  position: fixed;
  inset: auto 0 0;
  z-index: 10;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  border-top: 1px solid var(--colour-border);
  background: var(--colour-page);
}

.tab-bar[hidden] {
  display: none;
}

.tab-bar__tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: var(--tab-bar-height);
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
  text-decoration: none;
}

.tab-bar__tab[aria-current="page"] {
  color: var(--colour-accent);
  font-weight: 600;
}

/* Nothing ends under the bar. */

.app:has(.tab-bar) .screen {
  padding-bottom: calc(
    var(--tab-bar-height) + var(--space-5) + env(safe-area-inset-bottom, 0px)
  );
}

@media (min-width: 48rem) {
  .tab-bar {
    padding-inline: max(0px, calc((100% - var(--measure-page)) / 2));
  }
}

/* --- 188: Opsummering ------------------------------------------------------ */

/* A finished project's card, one line a fact; the accent behind it, quietly,
   because it is the reward at the end of the project. */

.project-card--summary {
  border-color: var(--colour-accent-border);
  background: var(--colour-accent-soft);
}

.project-summary {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.project-summary__prompt {
  display: inline-block;
  margin-top: var(--space-3);
  color: var(--colour-accent);
  font-weight: 600;
}

/* --- 190: the brand lookup in the yarn list's filter ---------------------- */

/* The bar's slot for 157's choice field, beside the availability select. */

.filter-bar__brand {
  flex: 1 1 auto;
  min-width: 0;
}

/* --- 191: the project list as tiles, grouped by status -------------------- */

/* Hjem's card in a grid, two across on a phone and three when wide; the
   whole list scrolls down as one. */

.home-cards--grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.home-cards--grid .home-card__link {
  aspect-ratio: 4 / 5;
}

.home-cards--grid .home-card__name {
  font-size: var(--text-large);
}

.project-groups {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.project-group__title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-large);
  font-weight: 600;
}

.project-group__none {
  margin: 0;
  color: var(--colour-text-quiet);
}

/* The year chips on *Færdige*: links in a row that scrolls sideways when
   there are many, the chosen one filled. */

.chips {
  display: flex;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  overflow-x: auto;
}

.chip {
  flex: none;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--colour-border-control);
  border-radius: var(--radius-pill);
  color: var(--colour-text);
  font-size: var(--text-meta);
  text-decoration: none;
}

.chip[aria-current="true"] {
  border-color: var(--colour-accent);
  background: var(--colour-accent);
  color: var(--colour-accent-text);
}

@media (min-width: 48rem) {
  .home-cards--grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* --- 192: the way back to the module's list ------------------------------- */

/* One quiet line beneath the bar, on every screen but Hjem and the three
   lists: a chevron and the module's name. */

.header__crumb {
  padding: 0 var(--space-4) var(--space-2);
}

.header__crumb-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: var(--control-compact);
  color: var(--colour-accent);
  font-size: var(--text-meta);
  font-weight: 600;
  text-decoration: none;
}

@media (min-width: 48rem) {
  .header__crumb {
    max-width: var(--measure-page);
    margin-inline: auto;
    padding-inline: var(--space-5);
  }
}

/* --- 193: Slet projekt on the project page -------------------------------- */

/* Quiet, at the foot of the page beneath the cards; its confirmation opens in
   its place. */

/* His, at the walk: in the cards' gutter, which the project view sets and
   the wide layout gives back to the screen. */
/* 206, his: *Slet* in the band under its action on both pages, the action
   and the verb one right-aligned column; the confirmation opens directly
   beneath the band. */
.project-band__actions,
.pattern-band__actions {
  display: flex;
  flex: none;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
}

.project-band__actions .project-band__edit {
  margin-top: var(--space-1);
}

.project-page > .project-confirm-host:not(:empty),
.pattern-page > .pattern-confirm-host:not(:empty) {
  margin: var(--space-3) var(--space-4) 0;
}

.project-page > .project-confirm-host:empty {
  display: none;
}

@media (min-width: 48rem) {
  .project-delete-host {
    padding-inline: 0;
  }
}

/* --- 197: the heading face — her choice from 196's trial -------------------- */

/* Lora for headings, today's colours kept: her verdict on 196's four looks,
   2026-10-11. Stored in the app, never fetched from a font service at
   runtime, under the Open Font License beside the file; one weight, the
   Latin subset with æ, ø and å. Body text stays the phone's own face. */

@font-face {
  font-family: "Lora";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/lora.woff2") format("woff2");
}

h1,
h2,
.page-title,
.header__brand,
.home-card__name,
.summary__tile-value {
  font-family: var(--font-display);
}

/* 203: the stash itself, as words above the period — always far larger
   than a month's ind and ud, so never a mark on their axis. */

.stats-stock__now {
  margin: 0 0 var(--space-4);
  font-size: var(--text-title);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* 208: the counting page. The main number large, the extras beneath it,
   and the lower part of the screen one +1 target for a thumb that is
   holding yarn; −1 sits above it, apart, so a slip cannot undo a row. */

.counter {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: 0 var(--space-4);
}

.counter-screen .page-title .counter__project {
  color: inherit;
  text-decoration: none;
}

.counter__project {
  color: var(--colour-accent);
  font-weight: 600;
}

.counter__reminders {
  padding: var(--space-3);
  border: 1px solid var(--colour-accent-border);
  border-radius: var(--radius-medium);
  background: var(--colour-accent-soft);
  font-weight: 600;
}

.counter__main {
  text-align: center;
}

.counter__number {
  margin: 0;
  font-size: calc(var(--text-hero) * 2);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.counter__row {
  margin: var(--space-1) 0 0;
  font-size: var(--text-title);
}

.counter__saved {
  margin: 0;
  color: var(--colour-error-text);
}

.counter__saved:empty {
  display: none;
}

.counter__extras {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--colour-border-soft);
}

.counter__extra {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--colour-border-soft);
}

.counter__extra-words {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-width: 0;
}

.counter__extra-name {
  font-weight: 600;
}

.counter__extra-line {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}


.counter__step {
  min-width: var(--tap-target);
  padding: 0;
}

.counter__wake {
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

.counter__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.counter__edit {
  min-height: 2.25rem;
}

.counter__closed {
  color: var(--colour-text-quiet);
  text-align: center;
}

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

.counter__minus {
  align-self: flex-end;
  min-width: calc(var(--tap-target) * 1.5);
}

.counter__tap {
  min-height: max(10rem, 30dvh);
  border: 1px solid var(--colour-accent-border);
  border-radius: var(--radius-medium);
  background: var(--colour-accent-soft);
  color: var(--colour-accent);
  font: inherit;
  font-size: var(--text-display);
  font-weight: 600;
  cursor: pointer;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
}

.counter__tap:active {
  background: var(--colour-accent);
  color: var(--colour-accent-text);
}

.counter-edit {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: 0 var(--space-4);
}

.counter-edit__done {
  margin-inline-start: auto;
}

/* 215: *Tællere | Afsnit* and **Luk** on one line, so the forms start right
   under it. The choice is two native radios drawn as one row of buttons,
   with room for a third; each radio stays in the page, unseen, so a key and
   the screen reader still reach it. */

.counter-edit__bar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.counter-edit__mode {
  position: relative;
  display: flex;
  min-height: var(--tap-target);
  border: 1px solid var(--colour-border-control);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.counter-edit__mode input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}

.counter-edit__mode label {
  display: flex;
  align-items: center;
  padding: 0 var(--space-4);
  color: var(--colour-text);
  cursor: pointer;
}

.counter-edit__mode label + input + label {
  border-inline-start: 1px solid var(--colour-border-control);
}

.counter-edit__mode input:checked + label {
  background: var(--colour-accent);
  color: var(--colour-accent-text);
}

.counter-edit__mode input:disabled + label {
  cursor: progress;
}

.counter-edit__mode input:focus-visible + label {
  outline: 2px solid var(--colour-accent);
  outline-offset: -4px;
}

.field--check input {
  width: var(--space-5);
  height: var(--space-5);
  margin: 0;
  accent-color: var(--colour-accent);
}

.field--check input[switch] {
  width: auto;
}

/* 214, his at the walk: each counter's form, the new one and each of the
   plan's sections a card, as the project page's — one level, never a card
   in a card. */

.counter-form--card,
.plan-section__box {
  min-width: 0;
  padding: var(--space-4);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-large);
  background: var(--colour-surface);
  box-shadow: var(--shadow-low);
}

.field--check {
  min-height: var(--tap-target);
}

/* 214: *Rækker*'s **Opdater** on the line of its field. */

.counter-form__beside {
  grid-row: 2;
  margin: 0;
}

.counter-edit__forms {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.counter-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px solid var(--colour-border-soft);
}

.counter-form__title {
  margin: 0;
  font-size: var(--text-title);
}

.counter-form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin: 0;
}

.counter-form__confirm {
  flex: 1 1 100%;
}

.field--check {
  display: flex;
  /* 214, his at the walk: the caption and its switch on one line; `.field`
     alone stacks them. */
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
}

/* 208: Tæl beside a card on Hjem, over the picture's corner. */

.home-card {
  position: relative;
}

.home-card__count {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  padding: 0 var(--space-3);
  border-radius: var(--radius-medium);
  background: var(--colour-accent);
  color: var(--colour-accent-text);
  font-weight: 600;
  text-decoration: none;
}

.counter__reset {
  padding: 0 var(--space-2);
  font-size: var(--text-small);
}

.counter__reset-confirm {
  flex: 1 1 100%;
  font-size: var(--text-meta);
}

.counter__extra {
  flex-wrap: wrap;
}

/* 208, his at the walk: a hidden part of the counter form takes no room,
   whatever display the field rows give it. */

.counter-form [hidden] {
  display: none;
}

/* 208: the following box beside the repeat, level with its input. */

.field-row > .field--check {
  display: flex;
  grid-row: span 4;
  align-items: center;
  align-self: end;
  min-height: var(--tap-target);
}

/* 210: the section plan on the counting page. The section's name over the
   row in it, *Rækker i alt* small beneath, and a doubt in the warning
   colour - the reserved orange, not the error red: a doubt is the reader
   saying it is unsure, not a refusal. */

.counter__section {
  margin: 0;
  font-size: var(--text-title);
}

.counter__plan-line {
  margin: var(--space-1) 0 0;
  font-size: var(--text-title);
  font-variant-numeric: tabular-nums;
}

.counter__total {
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

.counter__doubt,
.plan-section__doubt {
  margin: var(--space-1) 0 0;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--colour-reserved-border);
  border-radius: var(--radius-medium);
  background: var(--colour-reserved-surface);
  color: var(--colour-reserved);
  font-weight: 600;
}

.counter__must-be {
  margin: var(--space-1) 0 0;
  font-weight: 600;
}

.counter__main [hidden],
.counter [hidden],
.plan-form [hidden] {
  display: none;
}

.counter__section-done {
  align-self: center;
}

.plan-offer__question {
  margin: 0;
  font-size: var(--text-title);
  line-height: var(--line-body);
}

/* 215: the line beneath the question, what Ja will do. */

.plan-offer__line {
  margin: 0;
  color: var(--colour-text-quiet);
}

/* 232: the heading and its Gem on one line, Gem wrapping beneath on a
   narrow screen rather than shrinking. */

.plan-form__head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2) var(--space-3);
}

.plan-form__head .button {
  margin-top: 0;
}

.plan-form__reading {
  margin: 0;
  font-weight: 600;
}

.plan-form__doubts {
  padding: var(--space-3);
  border: 1px solid var(--colour-border-soft);
  border-radius: var(--radius-medium);
}

.plan-form__doubts--some {
  border-color: var(--colour-reserved-border);
  background: var(--colour-reserved-surface);
  color: var(--colour-reserved);
  font-weight: 600;
}

.plan-form__doubts p,
.plan-form__doubts ul {
  margin: 0;
}

.plan-form__sections {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.plan-section__box {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
}

.plan-section__legend {
  /* 214: a card's title inside it, not sitting on its border. */
  float: left;
  width: 100%;
  padding: 0;
  font-weight: 600;
}

.plan-section__legend + * {
  clear: both;
}

.plan-section__doubt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.plan-section__check {
  margin: 0;
  color: var(--colour-text-quiet);
  font-size: var(--text-small);
}

.plan-section__check--wrong {
  color: var(--colour-reserved);
  font-weight: 600;
}

/* --- 216: swatches ------------------------------------------------------- */

/* The list: cards in a grid of two, each the name, then the gauge large in
   tabular figures, then the needle, quiet. A card is the project card's
   surface, with no picture until 217. */

.swatches__filter {
  max-width: 12rem;
}

.swatch-cards {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.swatch-card__link {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  height: 100%;
  padding: var(--space-4);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-large);
  background: var(--colour-surface);
  box-shadow: var(--shadow-low);
  color: var(--colour-text);
  text-decoration: none;
}

.swatch-card__name {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.swatch-card__gauge {
  color: var(--colour-accent);
  font-size: var(--text-title);
  font-variant-numeric: tabular-nums;
}

.swatch-card__needle {
  color: var(--colour-text-quiet);
  font-size: var(--text-meta);
}

/* 217: the picture at the top of a card, square; a swatch without one keeps
   the neutral ground at the same size, so the grid stays even. */

.swatch-card__picture {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  margin-block-end: var(--space-2);
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  overflow: hidden;
}

.swatch-card__picture .photo-slot__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* 217: the *Billede* card on the swatch page. */

.swatch-picture__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.swatch-picture__open {
  display: block;
  width: 100%;
  max-width: 480px;
  padding: 0;
  border: none;
  border-radius: var(--radius-medium);
  background: none;
  font: inherit;
  color: inherit;
}

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

.swatch-picture__slot {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-medium);
  background: var(--colour-slot);
  overflow: hidden;
}

.swatch-picture__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.swatch-picture__image[hidden] {
  display: none;
}

.swatch-picture__status:empty {
  display: none;
}

/* The form: *Vasket* is a checkbox beside its label, the buttons beneath. */

.swatch-form__check {
  display: flex;
  /* His, at the walk: left, as a row — `.field` is a column. */
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
}

.swatch-form__check input {
  width: var(--space-5);
  height: var(--space-5);
  margin: 0;
  accent-color: var(--colour-accent);
}

.swatch-form__actions {
  display: flex;
  align-items: stretch;
  gap: var(--space-3);
  margin: var(--space-5) 0 0;
}

/* His, at the walk: **Gem** and **Fortryd** the same size, side by side. */

.swatch-form__actions .button {
  flex: 1 1 0;
  width: auto;
  /* **Gem** is `button--primary`, whose own top margin made the row taller
     than it and **Fortryd** with it. */
  margin: 0;
  min-height: var(--action-height);
}

/* His, at the walk: *Pind*, *Masker* and *Rækker* on one line, the boxes
   level when a label wraps. */

.swatch-form__row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  align-items: end;
  gap: var(--space-3);
  margin: 0 0 var(--space-3);
}

.swatch-form__row .field {
  min-width: 0;
  margin: 0;
}

/* A swatch's line on a lot's history leads to the swatch. */

.lot-history__swatch {
  color: inherit;
}

/* 216, his at the walk: the form keeps the screen's gutter, as every block
   on a plain `.screen` does; the list insets itself, `.projects`'s way. */

.swatch-form {
  padding-inline: var(--space-4);
}

/* 216, his at the walk: *Vasket* is a switch after its caption. */

.swatch-form__check input[switch] {
  width: auto;
}

/* 216, his at the walk: the saved line under the band keeps the gutter. */

.swatch-saved {
  padding-inline: var(--space-4);
}

/* --- 237: the door for a stranger, and the welcome page ------------------ */

/* Two ways in, stacked, the first the accent and the second quiet. The email
   way replaces them with its two forms in the same block, so the screen's
   height barely moves while she is looking at it. */

.signin__ways {
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

.signin__ways[hidden],
.signin__form[hidden] {
  display: none;
}

.signin__way--email {
  min-height: var(--tap-target);
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--colour-border-control);
  border-radius: var(--radius-pill);
  background: var(--colour-surface);
  color: var(--colour-accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.signin__way--email:disabled {
  cursor: default;
  color: var(--colour-text-quiet);
}

.signin__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: var(--space-6) 0 0;
}

.signin__form .field {
  margin: 0;
}

/* The address read back above the code field: she must be able to see she
   typed it right before she waits for a mail that will not come. */

.signin__sent {
  margin: 0;
  font-size: var(--text-meta);
  color: var(--colour-text-quiet);
  overflow-wrap: anywhere;
}

.signin__sent strong {
  color: var(--colour-text);
}

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

/* The link to the website, for the one who arrived without seeing it. */

.signin__about {
  margin: var(--space-5) 0 0;
  text-align: center;
  font-size: var(--text-meta);
}

/* The welcome page: one form, then the come-back block as a card, then the
   one button. `.welcome` is the gutter — `screen--form` gives none. */

/* `.app .screen`'s zero padding outranked a bare `.welcome`, so on a phone
   the page had no gutter at all until 244's walk; the heading keeps only its
   own vertical padding inside it. */
.app .welcome {
  padding-inline: var(--space-4);
}

.welcome .page-title {
  padding-inline: 0;
}

/* 244, his at the walk: *Sprog* and its select on one line. */
.welcome__language {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.welcome__language .field__select {
  width: auto;
  flex: 0 0 auto;
}

.welcome__lead {
  margin: 0 0 var(--space-5);
  color: var(--colour-text-quiet);
}

.welcome__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin: 0;
}

.welcome__comeback {
  padding: var(--space-4);
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-large);
  background: var(--colour-surface);
  box-shadow: var(--shadow-low);
}

.welcome__comeback h2 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-title);
}

.welcome__comeback p {
  margin: 0 0 var(--space-2);
  font-size: var(--text-meta);
  color: var(--colour-text-quiet);
}

.welcome__comeback ol {
  margin: 0 0 var(--space-2);
  padding-left: var(--space-5);
  font-size: var(--text-meta);
}

.welcome__comeback li {
  margin-bottom: var(--space-1);
}

/* Drawn inline so it is the glyph she will look for, at the text's size. */

.welcome__share-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  margin-inline: 0.1em;
  color: var(--colour-accent);
}

.welcome__actions {
  display: grid;
}

/* The profile's language select, 237: one field in its own section. */

.profile-language {
  max-width: 20rem;
}

/* The administrator's switch above the invitee list, 237: the caption first,
   as every switch in the app. */

.signup-open {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  margin: 0;
  padding: 0 var(--space-4) var(--space-3);
}

.signup-open input {
  width: var(--space-5);
  height: var(--space-5);
  margin: 0;
  accent-color: var(--colour-accent);
}

.signup-open input[switch] {
  width: auto;
}

/* 244: outside Safari on an iPhone, the address to take to Safari. */
.welcome__address {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  font-weight: 600;
}

/* 244, his at the walk: in a browser on an iPhone its own bars take about
   200 points, and Hjem's four tiles fell under the tab bar. On a short
   screen the card gives the height — wider and lower — and the tiles a
   little; the pinned app, which has the whole screen, is not touched. */
@media (max-height: 820px) {
  .home-cards--row .home-card__link {
    aspect-ratio: 16 / 7;
  }

  .home__actions {
    margin-top: var(--space-4);
  }

  .home__action {
    min-height: 4.5rem;
    padding: var(--space-2);
  }
}
