/*
 * Pølsefabrikken — the design tokens as real CSS.
 *
 * Ported from docs/design/build.py by VALUE, never by attribute: every artboard
 * there renders with inline style="…", and the strict-nonce CSP this phase ships
 * blocks every one of them. The token names are the contract with the canvas and
 * are kept verbatim; a test fails the build if one goes missing from either theme.
 *
 * Two changes from the canvas, both deliberate:
 *   1. Fonts are the system stack. The canvas pulls two webfonts from Google's
 *      font CDN; BRIEF forbids a CDN and font-src is 'self'. The system stack
 *      ships zero bytes and is a one-file change later. A test greps this file
 *      for those host and family names, so they are not written here either.
 *   2. The canvas frames every screen in a fixed 390x844 .phone div. This is a
 *      real page: 100dvh, a fixed tab bar and safe-area insets instead.
 */

:root {
  color-scheme: dark;

  --bg: #141210;
  --surface: #1c1916;
  --raised: #26221d;
  --line: rgba(255, 255, 255, .07);
  --line-strong: rgba(255, 255, 255, .14);
  --text: #f1ebe3;
  --dim: #a19588;
  --muted: #6e645b;
  --accent: #f08a3e;
  --accent-press: #d67428;
  --on-accent: #1a1008;
  --accent-soft: rgba(240, 138, 62, .14);
  --ok: #5cc491;
  --ok-soft: rgba(92, 196, 145, .14);
  --danger: #e2685f;
  --danger-soft: rgba(226, 104, 95, .14);
  --warn: #e8b13c;
  --shadow: 0 1px 2px rgba(0, 0, 0, .35), 0 8px 24px rgba(0, 0, 0, .28);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .2);

  /* Theme-independent: shape, motion and type are the same in both themes. */
  --content-gap: 14px;
  --radius: 14px;
  --radius-lg: 20px;
  --radius-sm: 10px;
  --dur: 240ms;
  --dur-fast: 140ms;
  --ease: cubic-bezier(.16, 1, .3, 1);
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* PULL TO REFRESH, and all four are UNITLESS on purpose: app.js writes one
     number — how far the FINGER has travelled — and these turn it into a
     position, a translation and an angle. A length could not be divided by a
     length to give the angle.

     RETUNED BY THE OWNER on 2026-09-09, having felt the first build: "it is
     trickered too easy, we need to adjust it to require a bit more pull". The
     threshold went 80 -> 120 and the disc stopped tracking the finger 1:1. THESE
     ARE FEEL, NOT GEOMETRY — expect the owner to judge them again in a real
     browser at 12-05, and expect the follow-up to be one of these numbers
     changing and nothing else. Keep it that way.

     --pull-threshold  how far the finger travels before releasing fetches. app.js
                       names the same number, because CSS cannot read a JS
                       constant; StylesheetTest asserts they agree, since a
                       disagreement would turn the disc accent at one distance and
                       fire the reload at another.
     --pull-resistance the disc LAGS the finger by this much. At 1 the page would
                       track the finger exactly, which is what felt soft: the last
                       stretch has to feel like pulling against something.

                       THESE FOUR NUMBERS ARE CHROME'S OWN, and that is the whole
                       of the tuning story. The owner judged four settings on
                       2026-09-09 — 80 px at 1:1 was "trickered too easy", 120 at
                       .6 was "way too loose", tripling it to 360 at .25 went
                       "tighter than chromes default" — and then asked for Chrome's
                       default itself. Chrome's pull-to-refresh on Android IS
                       androidx SwipeRefreshLayout, so its constants are readable
                       rather than guessable, and each token below is one of them
                       with dp read as px:

                         DRAG_RATE            .5   → --pull-resistance
                         mTotalDragDistance   64dp → the disc's travel that fires,
                                                     so 128 px of FINGER travel at
                                                     half rate → --pull-threshold
                         max drag             2 ×  → --pull-reach 128
                         DEFAULT_CIRCLE_TARGET 64dp → --pull-rest, which this file
                                                     had matched by accident since
                                                     the day it was written

                       So the disc stands at exactly --pull-rest when it arms, and
                       stays there while the page is fetched — which is what makes
                       Chrome's feel one movement rather than two.
     --pull-reach      the furthest the disc comes out, however hard anyone pulls.
                       Divided by the resistance it is also app.js's PULL_MAX, and
                       the threshold must stay under THAT or the gesture can never
                       arm — StylesheetTest asserts both halves.

     The dead zone before any of this — the first 60 px, where nothing moves at
     all — is app.js's PULL_START and is deliberately NOT a token here: no rule in
     this file reads it, because inside it there is nothing to draw.
     --pull-rest       where it waits while the page is fetched — panel 3 of the
                       `Opdater` sheet, and the one number the owner did not
                       change. */
  --pull-threshold: 128;
  --pull-resistance: .5;
  --pull-reach: 128;
  --pull-rest: 64;
}

/* The system decides, unless the person has said otherwise. */
/* System preference applies only while no theme is chosen. Any chosen theme —
   light, dark, or one added later — is a single :root[data-theme="<name>"] block
   below that redefines tokens and nothing else. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    color-scheme: light;

    --bg: #f4ede3;
    --surface: #fffaf3;
    --raised: #ffffff;
    --line: rgba(30, 20, 10, .08);
    --line-strong: rgba(30, 20, 10, .16);
    --text: #1f1913;
    --dim: #6d6259;
    --muted: #9a8f85;
    --accent: #c95f12;
    --accent-press: #ad500d;
    --on-accent: #ffffff;
    --accent-soft: rgba(201, 95, 18, .10);
    --ok: #2f8f5f;
    --ok-soft: rgba(47, 143, 95, .12);
    --danger: #c2453c;
    --danger-soft: rgba(194, 69, 60, .10);
    --warn: #9a6510;
    --shadow: 0 1px 2px rgba(40, 25, 10, .08), 0 8px 24px rgba(40, 25, 10, .08);
    --shadow-sm: 0 1px 2px rgba(40, 25, 10, .1);
  }
}

/* A manual override beats the system, in both directions. The values are
   repeated rather than shared through a second custom property: principle 10,
   boring over clever, and a reader can see the whole theme in one place. */
:root[data-theme="light"] {
  color-scheme: light;

  --bg: #f4ede3;
  --surface: #fffaf3;
  --raised: #ffffff;
  --line: rgba(30, 20, 10, .08);
  --line-strong: rgba(30, 20, 10, .16);
  --text: #1f1913;
  --dim: #6d6259;
  --muted: #9a8f85;
  --accent: #c95f12;
  --accent-press: #ad500d;
  --on-accent: #ffffff;
  --accent-soft: rgba(201, 95, 18, .10);
  --ok: #2f8f5f;
  --ok-soft: rgba(47, 143, 95, .12);
  --danger: #c2453c;
  --danger-soft: rgba(194, 69, 60, .10);
  --warn: #9a6510;
  --shadow: 0 1px 2px rgba(40, 25, 10, .08), 0 8px 24px rgba(40, 25, 10, .08);
  --shadow-sm: 0 1px 2px rgba(40, 25, 10, .1);
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: #141210;
  --surface: #1c1916;
  --raised: #26221d;
  --line: rgba(255, 255, 255, .07);
  --line-strong: rgba(255, 255, 255, .14);
  --text: #f1ebe3;
  --dim: #a19588;
  --muted: #6e645b;
  --accent: #f08a3e;
  --accent-press: #d67428;
  --on-accent: #1a1008;
  --accent-soft: rgba(240, 138, 62, .14);
  --ok: #5cc491;
  --ok-soft: rgba(92, 196, 145, .14);
  --danger: #e2685f;
  --danger-soft: rgba(226, 104, 95, .14);
  --warn: #e8b13c;
  --shadow: 0 1px 2px rgba(0, 0, 0, .35), 0 8px 24px rgba(0, 0, 0, .28);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .2);
}

/* ==========================================================================
   Navigation, from the platform (PLAT-02).

   THIS AT-RULE IS THE ENTIRE CROSS-DOCUMENT OPT-IN. No JavaScript, no
   document.startViewTransition, no feature detection: a browser that
   implements it cross-fades one server-rendered page into the next, and one
   that does not ignores an at-rule it has never heard of and navigates the way
   it always did. That is what "pure enhancement" means here, and it is why
   nothing in app.js knows this exists.

   The UA's own cross-fade is what plays. No ::view-transition-* animations of
   our own are written, deliberately: a custom transition is motion design, the
   canvas draws no motion, and a screen the canvas does not draw is not built.
   ========================================================================== */
@view-transition {
  navigation: auto;
}

* {
  box-sizing: border-box;
  /* No tap flash anywhere (owner, 2026-09-09): Android paints its own blue
     square over whatever was tapped, and every control here draws its own
     pressed state in tokens. */
  -webkit-tap-highlight-color: transparent;
}

/* Without !important a card is VISIBLE while carrying the hidden attribute: the
   UA sheet's [hidden]{display:none} loses to .card{display:flex}, and .card sets
   exactly that. The Python reference app carries this same rule for this reason. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  /* THE SCREEN'S HEIGHT IS MEASURED, NOT ASSUMED. `--screen` is written by
     app.js from window.innerHeight at load and on every resize; `100dvh` is
     the value before the module runs. On Android, in the installed app, the
     unit reported the height INCLUDING the strip under the system navigation
     bar that the page is not drawn under, and every page — short ones too —
     scrolled by exactly that strip (owner's phone, 2026-09-09). innerHeight
     is what is actually visible. */
  min-height: var(--screen, 100dvh);
  /* A COLUMN THE HEIGHT OF THE SCREEN, so a screen that wants to be exactly
     one screen can be (the front screen, below). `border-box` keeps the tab
     bar's padding inside the 100dvh; a page taller than the screen still
     scrolls as before, because this is a minimum and not a height. */
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.45;
  padding-bottom: calc(88px + env(safe-area-inset-bottom));

  /* The browser's OWN pull-to-refresh, switched off (the `Opdater` sheet's fifth
     rule). Without it the tab reloads on top of our reload and one drag becomes
     two navigations — which is what check 5 of 12-05's browser pass counts. */
  overscroll-behavior-y: contain;
}

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

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

/* NO horizontal padding, and the browser is what says so. On the canvas .top and
   .content are SIBLINGS inside the phone frame and each pads itself by 20 px; in
   this application the layout wraps the whole page in one <main class="content">,
   so a .top that padded itself again put every heading 40 px from the edge while
   the cards under it sat at 20. Measured in Chrome on 2026-08-30 against the
   Indstillinger artboard — and true of /start since phase 0. */
.top {
  /* THE HEADER SITS AT THE TOP OF THE SCREEN (owner, 2026-09-10, from a
     phone screenshot: "reduce the space by 80 % or so"). It stood 40 px down
     — 16 of .content's padding and 24 of its own — on a phone whose status
     bar is OUTSIDE the viewport, so all 40 were air. Now 8 px, plus the top
     inset where the status bar overlays the page (an iPhone opened from the
     home screen, with viewport-fit=cover), so the eyebrow never sits under
     the clock. The canvas's 55 is this 8 under the 47 px status-bar zone the
     phone frame includes and does not draw. */
  padding: calc(8px + env(safe-area-inset-top)) 0 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.eyebrow {
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

h1 {
  margin: 0;
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -.01em;
  line-height: 1.15;
}

.content {
  padding: 16px 20px 0;
  display: flex;
  flex-direction: column;
  gap: var(--content-gap);
  flex: 1 1 auto;
  box-sizing: border-box;
}

/* A page whose first thing is its header gives up the column's top padding:
   the header owns the distance to the screen's edge (see .top), and 16 px on
   top of it would be the 40 px the owner measured as too much. */
.content:has(> .top:first-child) {
  padding-top: 0;
}

/* THE TWO CLOCK SCREENS ARE ONE SCREEN EACH (owner, 2026-09-09): capped at
   the screen minus the tab bar, so a child that may shrink has something to
   shrink to — flex items shrink against a definite size, and the body's height
   is only a minimum. ONLY THOSE TWO. The cap first sat on every page, and a
   form taller than the screen then overflowed it without pushing the tab-bar
   padding down: the last rows of Redigér and Gem arbejde sat under the bar
   (owner's phone, 2026-09-09). Lists and forms scroll as pages, uncapped. The
   sections swap `hidden` as the clock changes state, and :has() follows. */
main.content:has(> .run-idle:not([hidden])),
main.content:has(> .run-running:not([hidden])) {
  max-height: calc(var(--screen, 100dvh) - 88px - env(safe-area-inset-bottom));
}

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

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.between {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--raised);
  border: 1px solid var(--line);
  font-size: 13px;
  color: var(--dim);
  font-weight: 500;
  white-space: nowrap;
}

.chip.on {
  background: var(--accent-soft);
  border-color: transparent;
  color: var(--accent);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 0 18px;
  border-radius: var(--radius);
  font-weight: 600;
  font-size: 16px;
  border: 1px solid transparent;
  background: var(--raised);
  color: var(--text);
  transition: background var(--dur-fast) var(--ease);
}

.btn.primary {
  background: var(--accent);
  color: var(--on-accent);
}

/* A held primary is the plain .btn in muted text — "still there, still in its
   place, and visibly not answering" (StopurGemUdenKunde, StopurGemSender). The
   tray holds Gem while a file is on its way, and the save handlers hold it while
   the server answers; both are this one rule. Nothing about the geometry
   changes, so the layout does not jump when the button wakes. */
.btn.primary:disabled {
  background: var(--raised);
  color: var(--muted);
}

.btn.ghost {
  background: transparent;
  color: var(--dim);
  border-color: var(--line-strong);
}

.btn.danger {
  background: var(--danger-soft);
  color: var(--danger);
}

/* The one taller button: the sign-in control on the login screen. A size is a
   variant of .btn, not an override on a page-specific class — the next screen
   that wants a big button asks for .btn.lg instead of copying two declarations. */
.btn.lg {
  min-height: 56px;
  font-size: 17px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.label {
  font-size: 13px;
  color: var(--dim);
  font-weight: 600;
}

/* The Tag field's `<datalist>` (owner, 2026-09-09) makes Chrome draw its own
   ▼ at the field's right edge, which no artboard draws. The list still opens on
   focus and narrows as the person types; only the extra glyph goes. */
input[list]::-webkit-calendar-picker-indicator {
  display: none;
}

.input {
  min-height: 48px;
  border-radius: var(--radius-sm);
  background: var(--raised);
  border: 1px solid var(--line);
  padding: 0 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text);
  font-size: 16px;
}

.input.placeholder {
  color: var(--muted);
}

.input.focus,
.input:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* THE OK WASH (04-08, drawn on NyKundeUdfyldtCvr and NyKundeUdfyldtMaps): the
   fields a pick actually wrote, for as long as `Fortryd` can take them back.
   It leaves a field the moment the person types in it — from then on the value
   is theirs, not the register's. */
.input.filled {
  background: var(--ok-soft);
}

.suggest {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  overflow: hidden;
}

.suggest > div {
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 0 14px;
  border-top: 1px solid var(--line);
}

.suggest > div:first-child {
  border-top: 0;
}

.dim {
  color: var(--dim);
}

.muted {
  color: var(--muted);
}

.small {
  font-size: 13px;
}

.xs {
  font-size: 12px;
}

.money {
  font-family: var(--mono);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.seg {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

/* ANCHORS AS WELL AS DIVS: the Faktura tab's `seg` navigates — `?tab=issued` is
   a URL that reloads, shares and goes back — so its two cells are real links and
   have to be reachable from a keyboard. `text-decoration` and the colour are what
   stop them looking like prose links; the colour beats `a:hover` because this rule
   is further down the file at the same specificity. */
.seg > div,
.seg > a,
.seg > span,
.seg > button {
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  font-weight: 600;
  color: var(--dim);
  font-size: 14px;
}

.seg > a {
  text-decoration: none;
}

/* BUTTONS AS WELL: the Besked screen's two kinds switch a panel and navigate
   nowhere, so its cells are buttons — reset to the cell's own look. */
.seg > button {
  border: 0;
  background: transparent;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  padding: 0;
}

.seg > .on {
  background: var(--raised);
  color: var(--text);
  box-shadow: var(--shadow-sm);
}

.thumb {
  width: 48px;
  height: 48px;
  border-radius: 10px;
  background: linear-gradient(135deg, var(--raised), var(--line-strong));
  border: 1px solid var(--line);
  flex: none;
}

/* The tab bar is fixed to a real viewport, not to a 390x844 artboard, and its
   padding clears the home indicator on a phone that has one. */
.tabs {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 8px 8px max(8px, env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 4px;
}

.tab {
  position: relative;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  border-radius: 12px;
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
}

.tab.on {
  color: var(--accent);
}

.tab svg {
  width: 24px;
  height: 24px;
}

/* The canvas draws this badge with an inline style. Under the CSP it has to be a
   class, and its count arrives in a data- attribute the ES module reads. */
.tab-badge {
  position: absolute;
  top: 2px;
  right: calc(50% - 22px);
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* A 24px control is a 24px control. Wrap it in .tap to give it a thumb-sized
   hit area without growing the thing you can see. */
.tap {
  min-height: 44px;
  min-width: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.check {
  position: relative;
  width: 24px;
  height: 24px;
  border-radius: 7px;
  border: 1.5px solid var(--line-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

/* THE REAL CHECKBOX, invisible and still there. Vælg arbejde's ticks are a form
   the server reads, so the box has to be an <input> — and it has to stay
   focusable, which `display:none` and `visibility:hidden` both take away. The
   lit state is then CSS rather than a class the module maintains, so a box
   ticked by a tap, by the keyboard and by the group control all look the same
   without app.js being involved at all. */
.check-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  appearance: none;
}

.check:has(.check-input:checked) {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.check:has(.check-input:checked) > svg {
  display: block;
}

/* The focus ring the invisible input cannot draw for itself. */
.check:has(.check-input:focus-visible) {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* The canvas draws an UNTICKED box as an empty one — `<div class="check"></div>`
   — and a ticked one as the same box with a glyph in it. A template cannot add
   and remove an inline SVG the way build.py adds and removes a string, so it
   renders the glyph once and this hides it: without the rule the box wore its
   tick in both states, which on `ingen starttid` says the exact opposite of the
   truth (found in the browser, 2026-09-02). */
.check > svg {
  display: none;
}

.check.on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.check.on > svg {
  display: block;
}

.readout {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1;
}

.sticky {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(100px + env(safe-area-inset-bottom));
  background: var(--raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 12px 12px 12px 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 6px var(--accent-soft);
  animation: pulse 2.4s ease-in-out infinite;
}

.hair {
  height: 1px;
  background: var(--line);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  background: var(--ok-soft);
  color: var(--ok);
  white-space: nowrap;
  flex: none;
}

.invoiced {
  opacity: .55;
}

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

/* The running state: a thin sweep completes one turn per minute — a second hand
   nobody has to read — and the halo breathes. Both stop when the person has
   asked for less motion. */
.ring {
  position: relative;
  /* The same clamp as the button inside it, at the ring's own size: 264 to
     220 is the artboard's ratio, kept as the screen shortens. MEASURED height
     (`--screen`), not the unit: on the owner's phone the unit was oversized,
     the ring stayed at 264 and the server note went under the tab bar. */
  width: clamp(160px, calc(var(--screen, 100dvh) * 0.28), 264px);
  height: clamp(160px, calc(var(--screen, 100dvh) * 0.28), 264px);
  flex: none;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* pointer-events: none is load-bearing, not tidiness. This is an absolutely
   positioned box over the whole 264 px ring, so it paints — and hit-tests —
   ABOVE the in-flow Stop button inside it. Measured in Chrome on 2026-08-30:
   document.elementFromPoint() at the centre of Stop returned div.ring, and every
   tap on the product's second action did nothing. The mask hides the pixels; it
   does not stop the box from catching the tap. */
.ring::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: 50%;
  background: conic-gradient(from 0deg, var(--accent) 0 22%, transparent 30% 100%);
  mask: radial-gradient(farthest-side, transparent calc(100% - 5px), #000 calc(100% - 4px));
  animation: sweep 60s linear infinite;
}

.halo {
  animation: breathe 2.4s ease-in-out infinite;
}

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

@keyframes breathe {
  0%, 100% {
    box-shadow: 0 0 0 12px var(--accent-soft);
  }

  50% {
    box-shadow: 0 0 0 20px var(--accent-soft);
  }
}

@keyframes pulse {
  0%, 100% {
    box-shadow: 0 0 0 5px var(--accent-soft);
  }

  50% {
    box-shadow: 0 0 0 9px var(--accent-soft);
  }
}

/* The Start screen. Three states in one page, so each is a section that lays
   ITSELF out — the layout's <main class="content"> is shared with every other
   screen and cannot centre for one of them. The animation half of the running
   state is the .ring/.halo/.dot block above, from phase 0, and is deliberately
   not repeated here: a second @keyframes would be a second thing to remember to
   name in the reduced-motion block. */
.run {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}

/* THE 14 PX THAT IS ALREADY THERE. On the canvas .top and .content are siblings
   inside the phone frame, so an artboard's `padding-top` is the WHOLE distance
   from the heading to the first thing under it. Here the header and the sections
   are children of one <main class="content"> whose `gap` is --content-gap, so
   every artboard number arrives that much larger unless it is taken off again.
   The subtraction is written as calc() rather than worked out here: each rule
   then reads as the ARTBOARD reads — 22, 44, 16 — and changing .content's gap
   moves the four frames with it instead of silently drifting from the canvas.

   Measured in Chrome on 2026-08-30, artboard beside the built view, heading
   bottom to first child: StopurKlar wants 22 and rendered 36; Main wants 44 and
   rendered 58; StopurGem wants 16 and rendered 14 (that one is 2 px SHORT,
   because .content's own 16 px padding is what the canvas uses there). The
   section's `gap` is the artboard's `.content` gap for the same reason — 0 here
   left every distance inside the running frame 14 px tight.

   REMEASURED 2026-08-31: the canvas moved. The five-row list and the raised
   Skift state cost the running frames their slack, and the artboards now give
   Main padding-top 32 (was 44) with the ring at 18 (was 26), the field at 16
   (was 20) and the server note at 14 (was 18); the banner state gives 10/8px
   gap with the readout at 0 and the ring at 10. Only whitespace moved — no
   type size changed. */
.run-idle {
  padding-top: calc(22px - var(--content-gap));
  gap: 12px;
  /* THE FRONT SCREEN IS ONE SCREEN (owner, 2026-09-09). Drawn for a 390×844
     frame, the idle column — clock, button, chips, field, list — is taller
     than a phone with its text size turned up, and the whole page scrolled
     with the list running under the tab bar. The section now fills what the
     screen has and no more: the clock and the button give up a little height
     on a short screen (the clamps below), and the suggestion list is the one
     thing that shrinks and scrolls inside itself. At the canvas's size every
     number is the artboard's. */
  flex: 1 1 auto;
  min-height: 0;
}

.run-idle .customer-field {
  flex: 1 1 auto;
  min-height: 0;
}

/* The two chips above the field wrap rather than run off a narrow screen:
   `Tilføj arbejde manuelt` ended at 349px on a 320px one (measured 2026-09-09). */
.customer-field > .between {
  flex-wrap: wrap;
}

.run-idle [data-customer-suggest] {
  flex: 0 1 auto;
  min-height: 88px;
  overflow-y: auto;
}

.run-running {
  padding-top: calc(32px - var(--content-gap));
  /* The gaps give way with the ring: 14 at the canvas's height, less on a
     short screen, so the server note at the foot stays above the tab bar. */
  gap: clamp(6px, calc(var(--screen, 100dvh) * 0.018), 14px);
  /* One screen, like the idle state: the section fills what the screen has,
     and the readout and ring give up height on a short one (their clamps). */
  flex: 1 1 auto;
  min-height: 0;
}

.run-stopped {
  padding-top: calc(16px - var(--content-gap));
  gap: 12px;
  align-items: stretch;
}

/* The long run is its OWN frame on the canvas, not the running frame with a card
   pushed in at the top: StartLangtLoeb gives its .content `padding-top:20px` and
   `gap:12px` where Main gives 44 and 14, and pulls the readout and the ring in
   with it (margin-top 2 and 18, against 8 and 26). The banner is 66 px of card;
   without this the column below it sits 14 px lower than it was drawn and the
   note falls off an 844 px screen. :has() rather than a class, because the
   module already says everything it has to say by unhiding the banner. */
.run-running:has(> .run-banner:not([hidden])) {
  padding-top: calc(10px - var(--content-gap));
  gap: 8px;
}

.run-running:has(> .run-banner:not([hidden])) .run-readout {
  margin-top: 0;
}

.run-running:has(> .run-banner:not([hidden])) .ring {
  margin-top: 10px;
}

/* The hero: 200 px of accent on the idle screen, 220 px of outline inside the
   sweep ring while it runs. A <button> and not the canvas's <div>, because this
   is the product's primary action and a keyboard has to reach it — the same
   reason the tab bar is anchors. Being a button, it is 44 px-plus by definition. */
.run-hero {
  width: clamp(140px, calc(var(--screen, 100dvh) * 0.24), 200px);
  height: clamp(140px, calc(var(--screen, 100dvh) * 0.24), 200px);
  /* NOT A SHRINKING FLEX ITEM. In the capped column a button's own content is
     a glyph and a word, so it was allowed to lose height and not width — the
     owner saw an egg (2026-09-09). Only the suggestion list gives way. */
  flex: none;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  border: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  margin-top: 10px;
  box-shadow: 0 0 0 14px var(--accent-soft), var(--shadow);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: .02em;
  font-family: inherit;
  cursor: pointer;
}

/* Written after .run-hero because it overrides it. The box-shadow: none is not
   tidiness: the breathing halo draws its own, and the two would stack. */
.run-hero-stop {
  width: clamp(133px, calc(var(--screen, 100dvh) * 0.233), 220px);
  height: clamp(133px, calc(var(--screen, 100dvh) * 0.233), 220px);
  margin-top: 0;
  gap: 4px;
  background: var(--raised);
  color: var(--accent);
  border: 2px solid var(--accent);
  box-shadow: none;
}

/* The header's back arrow is a POST, so it is a <button> where every drill-down
   uses an <a>. It keeps .tap's 44 px hit area and only has to undo the UA's
   button chrome. */
.run-back {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.run-running .ring {
  margin-top: clamp(6px, calc(var(--screen, 100dvh) * 0.023), 18px);
}

.run-readout-idle {
  font-size: clamp(34px, calc(var(--screen, 100dvh) * 0.055), 44px);
  flex: none;
}

.run-readout {
  font-size: clamp(40px, calc(var(--screen, 100dvh) * 0.07), 68px);
  margin-top: 8px;
  flex: none;
}

.run-readout-sm {
  font-size: 34px;
}

.run-estimate {
  margin: -4px 0 0;
}

.run-amount {
  color: var(--text);
}

.run-note {
  margin-top: clamp(4px, calc(var(--screen, 100dvh) * 0.018), 14px);
  text-align: center;
  max-width: 280px;
}

/* The >= 14 h card. `accent` on .card rather than a class of its own, because the
   same treatment is what FakturaGennemse's choice cards ask for in phase 7. */
.card.accent {
  flex-direction: row;
  align-items: center;
  gap: 12px;
  border-color: var(--accent-soft);
  background: var(--accent-soft);
}

.run-banner {
  width: 100%;
}

.run-banner-icon {
  color: var(--accent);
  display: flex;
  flex: none;
}

.run-banner-text {
  min-width: 0;
}

.run-banner-title {
  font-weight: 600;
}

/* THE CARD IS A COLUMN, and it is a column because phase 5 gave it a second row.
   Until 2026-09-02 this said `flex-direction: row`, from the days when the card
   held exactly two things: the readout block and the amount. The editable clock
   chips arrived as SIBLINGS of that pair, so the hairline and the chip row became
   the third and fourth items of a row — measured in Chrome, the chips started at
   x=318 on a 375 px viewport and ran to 545, which is why the owner saw two grey
   words at the screen's edge and no times. The artboard has always drawn a column
   card with a `.between` pair inside it, and both templates spell it that way, so
   the fix is that `.run-summary` no longer says anything about direction at all —
   `.card`'s own column and its 8 px gap are already right. */

/* THE MONEY STANDS AT THE RIGHT EDGE AND NEVER BREAKS (owner, 2026-09-10, from
   the phone with large text): `500,00 kr.` had wrapped into two lines pressed
   against the clock. The pair may wrap instead — the amount then drops under
   the clock, still right-aligned — and the gap between the two is wide enough
   that they never read as one figure. Every summary card wears this: Start's
   stopped run, Tilføj arbejde and Redigér arbejde all draw the same card. */
.run-summary .between {
  flex-wrap: wrap;
  gap: 8px 24px;
}

.run-summary-amount {
  font-size: 20px;
  margin-left: auto;
  text-align: right;
  white-space: nowrap;
}

/* One column: the stopped state's only button today is Slet, full width under
   where Gem arbejde arrives in phase 5. */
.run-actions {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 10px;
  margin-top: 4px;
}

.run-actions .btn {
  min-height: 48px;
}

/* The tap has been heard, before the server has answered. No colour, so a theme
   cannot get it wrong, and no transform, so nothing moves under a thumb that is
   still on the control. app.js adds it on click and removes it in a finally. */
.is-pressing {
  opacity: .85;
}

/* The login screen. The canvas draws all of this inline; under a style-src with
   no 'unsafe-inline' every one of those declarations has to be a class. */
.login {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  width: 100%;
  max-width: 350px; /* the LogInd artboard: 390 px phone minus 2 × 20 px padding */
  margin-inline: auto;
  min-height: 80dvh;
  padding-bottom: 40px;
}

/* The hero bleeds past the column, and the page must not scroll sideways for
   it — but the cut has to land on the SCREEN edge, not the column edge: a clip
   on .login sliced the film 20 px inside a phone and drew the straight line
   the baked fade exists to prevent (owner, 2026-09-02). Clipping the page-wide
   <main> puts the cut where the bezel already is. Scoped with :has so no other
   screen inherits a clip it never asked for. */
.content:has(.login) {
  overflow-x: clip;
}

/* The film. The clip is 16:9 and 130 % of the column wide, pulled up and in
   with negative margins, so the drawing reads at the size the LogInd artboard
   draws it and its fade has room to finish. The fade to the page colour is
   BAKED INTO THE CLIP, one per theme (docs/design/prototypes/login-video, v10):
   a CSS mask over a bright video bands on the dark end, and a mask on a playing
   video is the one thing iOS Safari has historically dropped. The mask here
   only touches the outer 6 % of the radius, where the clip is already the page
   colour, so it hides the element's own rectangle and nothing else — and if a
   browser ignores it the loss is one level of grey at the very edge. */
.login-hero {
  position: relative;
  width: 130%;
  margin: -4% -15% -8%;
  aspect-ratio: 16 / 9;
  animation: login-reveal 1600ms var(--ease) both;
}

.login-film {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  -webkit-mask-image: radial-gradient(closest-side at 50% 50%, #000 94%, transparent 100%);
  mask-image: radial-gradient(closest-side at 50% 50%, #000 94%, transparent 100%);
}

.login-heading {
  text-align: center;
}

.login-title {
  font-size: 30px;
}

/* The intro. The film fades up first; then the title, the tagline and the
   button rise into place in turn, each over 1.1 s on the house easing,
   350 → 550 → 800 ms after load — a slow, staggered entrance rather than a pop.
   `both` holds the start state before each delay so nothing flashes in at full
   opacity and then restarts. Stopped under reduced motion below. */
@keyframes login-reveal {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes login-rise {
  from {
    opacity: 0;
    transform: translateY(18px);
    filter: blur(10px);
  }

  to {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}

.login-title {
  animation: login-rise 1100ms var(--ease) 350ms both;
}

.login-tagline {
  animation: login-rise 1100ms var(--ease) 550ms both;
}

.login-signin {
  animation: login-rise 1100ms var(--ease) 800ms both;
}

/* The artboard's button, and since 2026-09-20 it IS the control: the link that
   leaves for Google. Google Identity Services used to lay its own button over
   this one at opacity 0, which an installed iOS PWA cannot click at all — a
   cross-origin frame may not navigate the top-level window — so the overlay and
   everything that positioned it are gone. Hover and focus are the anchor's own
   now; :focus-visible rather than :focus, so a thumb does not leave a ring
   behind on a phone. */
.login-signin {
  width: 100%;
  margin-top: 22px;
}

.login-google {
  width: 100%;
  border-color: var(--line-strong);
}

/* The colour is restated because the element is an <a> now: `a:hover` sets the
   accent colour, and the label of the one button on this screen must not change
   colour under a pointer — the artboard draws it in the text colour, hovered or
   not. */
.login-google:hover {
  background: var(--surface);
  color: var(--text);
}

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

/* Indstillinger — iPhone's Settings in shape (owner: "copy iPhone"). Every row is
   44 px, every group is a card, and the list simply scrolls: it is taller than a
   phone viewport on purpose (owner, 2026-08-30), which is why the artboard is
   drawn at 990 px rather than clipped to 844. */
.settings-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* THE CONTENT COLUMN AT GAP 12, and every screen the artboards draw as one:
   Indstillinger's drill-down (the card, the error line and the hint under it),
   Kunder's register, Registreringer's list and the rows inside it — so a slice
   the sentinel appends spaces itself exactly like the first paint did — and the
   two wrappers Redigér draws its rows in. ONE RULE, because a wrapper must not
   change where its rows sit, which is what `.readonly` exists to promise. */
.settings-screen,
.customer-list,
.reg-list,
.reg-rows,
.invoice-screen,
.invoice-rows,
.readonly,
.reg-when {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* A card that holds rows rather than prose: the hairlines do the separating, so
   the card's own gap and its vertical padding go away. */
.card.rows {
  gap: 0;
  padding: 2px 16px;
}

/* The three shapes a row comes in — a link, a button and a plain div — laid out
   identically, so a row that saves and a row that navigates are the same object
   to the eye. The button resets are what stop a <button> looking like one. */
.row-link,
.row-button,
.row-static {
  min-height: 44px;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: start;
}

/* a:hover would otherwise recolour the whole row: a settings row is not a link
   in prose, and turning it accent-orange on hover reads as a pressed state. */
.row-link:hover {
  color: var(--text);
}

/* The label keeps its whole width and the value truncates (owner, 2026-08-30):
   the row is fluid, so it fits every phone, and only a value longer than the
   screen is wide ever loses characters. Wrapping would break the 44 px rhythm. */
.row-label {
  flex: none;
}

.row-end {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.row-value {
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A required row that is still empty says so, in the accent colour. */
.row-value.missing {
  color: var(--accent);
  font-weight: 600;
}

.row-value.ok {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--ok);
}

/* Not .dot: that one pulses and is the running stopwatch's. This is a state
   light on a settings row and must sit perfectly still. */
.row-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ok);
  flex: none;
}

.row-chevron {
  display: flex;
  flex: none;
  color: var(--muted);
}

/* The ONE leading glyph in this list, and the exception is the point: every
   other row is a setting and reads as a word, while `Føj til hjemmeskærm` is an
   ACTION borrowed from the platform, and the glyph is what somebody recognises
   before reading anything. The 10 px gap comes from .row, which the label
   already carries beside .row-label. */
.row-glyph {
  display: flex;
  flex: none;
  color: var(--muted);
}

.row-button.danger {
  color: var(--danger);
  font-weight: 600;
}

/* Hidden rather than absent, so choosing another row moves the mark without the
   list reflowing by 18 px under the thumb that is still on it. */
.row-check {
  display: flex;
  flex: none;
  color: var(--accent);
  visibility: hidden;
}

.row-button[aria-checked="true"] .row-check {
  visibility: visible;
}

/* The account's e-mail is a name, not a section heading, so it keeps its case. */
.eyebrow.plain {
  text-transform: none;
}

/* Without this the eyebrow and the h1 refuse to shrink and push the back arrow
   off the header on a narrow phone. */
.top-title {
  min-width: 0;
}

/* THE TITLE ROW GIVES WAY BEFORE IT OVERLAPS. On a phone with its text size
   turned up (owner's, 2026-09-09) `Registreringer` and the two chips beside
   it no longer fit one line, and the chips were drawn over the heading. When
   the row is too narrow the chips take a line of their own, still on the
   right; at the canvas's size nothing moves, so the artboards stay 1:1. */
.top > .between {
  flex-wrap: wrap;
}

.top > .between > .row {
  margin-left: auto;
}

/* A real <input> inside the .input box: it inherits the box's own type instead
   of the UA's 13px Arial, and 16px is what stops iOS zooming on focus. */
input.input {
  width: 100%;
  font: inherit;
  font-size: 16px;
  appearance: none;
}

/* The muted `Mangler` the artboard draws in an empty box — the input equivalent
   of .input.placeholder, which only styles the drawn version. */
input.input::placeholder {
  color: var(--muted);
  opacity: 1;
}

/* The cash register: digits fill from the right and the decimal point never
   moves under them, so the unit stays pinned beside the last digit typed. */
.money-field {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 16px;
  font-variant-numeric: tabular-nums;
  text-align: end;
  appearance: none;
}

/* The box already draws the focus ring through .input:focus-within. */
.money-field:focus {
  outline: none;
}

.money-suffix {
  flex: none;
  color: var(--dim);
}

/* A refusal is a sentence on the screen it belongs to — not an alert, and not a
   toast that vanishes before it has been read. */
.form-error {
  color: var(--danger);
  font-size: 13px;
}

/* The per-field refusal: the same voice as .form-error, under the input the
   422's `fields` map named, so the complaint sits next to what it is about. */
.field-error {
  color: var(--danger);
  font-size: 13px;
}

/* A real <input> living inside an .input box that also holds an icon or a chip
   — the box draws the border and the focus ring; the input is bare. The 16px is
   what stops iOS zooming on focus, same as input.input. */
.bare-input {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 16px;
  appearance: none;
}

.bare-input:focus {
  outline: none;
}

.bare-input::placeholder {
  color: var(--muted);
  opacity: 1;
}

/* The search field's leading glyph, on Kunder, Registreringer and Udstedte. */
.customer-search-icon,
.reg-search-icon,
.invoice-search-icon {
  display: flex;
  flex: none;
}

/* Kunder's count line, Registreringer's totals strip and Opret's `Vælg kunde med
   åbent arbejde` line sit 2px in from the cards' edge, as all three artboards draw
   them. */
.customer-count,
.reg-strip,
.invoice-hint {
  padding: 0 2px;
}

/* A row is an anchor wearing the card: no link colour, the card's own text. */
.customer-row {
  gap: 4px;
  color: var(--text);
  text-decoration: none;
}

.customer-row:hover {
  color: var(--text);
}

.customer-row-name {
  font-weight: 600;
}

.customer-row-mail {
  gap: 6px;
}

/* KunderTom's card: centred, and roomier than a row on purpose. FakturaOpretTom
   and FakturaSendteTom draw the same card, which is 07-01's own note about them. */
.customer-empty,
.reg-empty,
.invoice-empty {
  align-items: center;
  text-align: center;
  gap: 6px;
  padding: 22px 16px;
}

.customer-empty-title,
.reg-empty-title,
.invoice-empty-title {
  font-weight: 600;
}

/* THE FILTER ROW IS FAKTURA'S SEGMENTED CONTROL (owner, 2026-09-10: "same
   style as the Opret / Udstedte tabs"), laid out as the owner asked the same
   night: Alle, Åbne and Afsluttet sized to their words and hugging the LEFT,
   `Tilføj` at the RIGHT edge — an action and not a filter, which is why it
   wears the accent and is never `on`. A flex row and not Faktura's grid, so
   the cells can be content-sized; the frame, the raised cell and the 44 px are
   `.seg`'s own. `Tilføj` keeps its word while the four cells fit one line and
   keeps only its `+` when they do not (owner, 2026-09-09): no width rule can
   know how wide the words are on a given phone, so app.js MEASURES — the word
   is shown, then hidden if the row overflows. The control never wraps.

   IT SCROLLS ONLY AS A LAST RESORT (review, 2026-09-10): at the largest text
   sizes the three filter words alone are wider than the phone, and `Afsluttet`
   — already past `Tilføj`'s `+` — was simply clipped off the end, so the third
   filter could not be reached at all. `overflow: hidden` is what clips it, so
   the escape is a class app.js adds when the row still overflows AFTER the
   word is gone: the cells keep their size and the row pans. Nothing moves on a
   phone where the words fit, which is every ordinary one. */
.reg-seg {
  display: flex;
  overflow: hidden;
}

.reg-seg-scroll {
  overflow-x: auto;
}

.reg-seg > a {
  flex: none;
  gap: 6px;
  padding: 0 14px;
  white-space: nowrap;
}

.reg-seg > .reg-add {
  margin-left: auto;
  padding: 0 12px;
  color: var(--accent);
}

/* The chip beside a heading sits on the heading's baseline, not centred on
   the eyebrow-and-title block (owner, 2026-09-09). Only where the right side
   is a chip row; a back arrow stays centred. */
.top > .between:has(> .row) {
  align-items: flex-end;
}

/* A dim line between the rows where the month changes. Capitalised HERE and not
   in the dictionary: `juli` is product copy that has to read as a word inside a
   sentence too, so the shout belongs to the stylesheet — the same split .eyebrow
   already makes with `august`. */
.reg-month {
  padding: 6px 2px 0;
  text-transform: capitalize;
}

.invoice-no-match {
  padding: 8px 2px;
}

/* A row is a card holding TWO links, so it has to be a positioning context for
   the stretched one below. */
.reg-row {
  position: relative;
  gap: 6px;
}

.reg-row-name {
  font-weight: 600;
}

/* The amount is one piece: with the phone's text size turned up, `500,00 kr.`
   broke into two lines beside a long customer name (owner, 2026-09-09). The
   name is the ellipsis and gives way; the figure never wraps. */
.reg-row .money {
  white-space: nowrap;
  flex: none;
}

/* THE STRETCHED LINK: the name is the row's link, and its ::after covers the
   whole card so the row is tappable everywhere the badge is not. The badge
   lifts itself above it. Two real anchors, no nesting, both reachable by tab. */
.reg-row-link {
  color: var(--text);
  text-decoration: none;
  min-width: 0;
}

.reg-row-link:hover {
  color: var(--text);
}

.reg-row-link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
}

.reg-tiles {
  gap: 6px;
  min-width: 0;
}

/* The list's own tag chip: the artboard's smaller one, which is why it drops
   the 44px tap target — nothing here is tappable, the row is. */
.reg-tag {
  padding: 3px 8px;
  font-size: 12px;
  min-height: 0;
}

/* 36px, and the same box whether it holds a photo or a document glyph. */
.reg-tile {
  width: 36px;
  height: 36px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--raised);
  object-fit: cover;
  flex: none;
}

.reg-tile-document {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
}

.reg-tile-more {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dim);
  font-size: 12px;
  font-weight: 600;
}

/* Above the stretched link, so the badge opens its document and the row opens
   the registration (06-CONTEXT). */
.reg-badge {
  position: relative;
  z-index: 1;
  text-decoration: none;
}

/* A credit note's badge: the same box in the quiet roles, with no check glyph. */
.reg-badge-closed {
  background: var(--raised);
  color: var(--dim);
}

/* A duration long enough to be a forgotten Stop rather than a shift. The figure
   itself carries the warning role and NOTHING is added to the row (owner,
   2026-09-02) — no badge, no chip, no extra line. */
.reg-warn {
  color: var(--warn);
}

/* ==========================================================================
   FAKTURA — Opret's cards and Udstedte's rows.

   Every rule below is an artboard's inline style moved into a class, and every
   colour is a token: `FakturaOpret` and `FakturaSendte` are the only sources,
   and a literal here would be a theme this stylesheet cannot answer for.
   ========================================================================== */

/* The unbilled nudge: a tinted strip rather than a card, because it is a remark
   about the list and not a member of it. */
.invoice-nudge {
  gap: 10px;
  padding: 10px 12px;
  border-radius: 12px;
  background: var(--accent-soft);
}

.invoice-nudge-icon {
  display: flex;
  flex: none;
  color: var(--accent);
}

/* Without min-width:0 the sentence refuses to shrink and pushes the strip wider
   than the column on a narrow phone. */
.invoice-nudge-text {
  min-width: 0;
}

.invoice-nudge-count {
  font-weight: 600;
}

/* Opret's card is an anchor wearing the card: no link colour, the card's own
   text — `.customer-row`'s rule, for its reason. Nothing inside it is a second
   destination, so it needs neither the stretched `::after` a registration row
   carries nor a positioning context. */
.invoice-card {
  color: var(--text);
  text-decoration: none;
  gap: 4px;
}

.invoice-card:hover {
  color: var(--text);
}

/* Udstedte's row holds TWO links since phase 8 (07 deferred item 6): the card
   opens the document and the download arrow downloads it. So it is a positioning
   context for the stretched one below — `.reg-row`'s pattern, for `.reg-row`'s
   reason. */
.invoice-row {
  position: relative;
  gap: 6px;
}

/* THE STRETCHED LINK: the customer's name is the row's link, and its ::after
   covers the whole card so the row is tappable everywhere the arrow is not. */
.invoice-row-link {
  color: var(--text);
  text-decoration: none;
  min-width: 0;
}

.invoice-row-link:hover {
  color: var(--text);
}

.invoice-row-link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
}

/* Above the stretched link, so the arrow downloads the A4 and the row opens the
   document — `.reg-badge`'s rule, for its reason. */
.invoice-row-pdf {
  position: relative;
  z-index: 1;
  display: flex;
  color: var(--muted);
  text-decoration: none;
}

.invoice-card-head,
.invoice-row-head {
  gap: 8px;
  min-width: 0;
}

.invoice-card-name,
.invoice-row-name {
  font-weight: 600;
}

/* The amount never shrinks — it is the figure the card exists to state, and an
   ellipsised price is a wrong price. */
.invoice-amount,
.invoice-row-date {
  flex: none;
}

.invoice-row-meta {
  min-width: 0;
}

.invoice-row-state {
  gap: 8px;
}

/* `Senest` and `Send`: the canvas's smaller chips, which is why they drop the
   44px tap target — neither is tappable, the card is. `.reg-tag` already makes
   this trade, and it is the same one. */
.invoice-recent {
  padding: 2px 8px;
  font-size: 11px;
  min-height: 0;
}

.invoice-chip-send {
  padding: 3px 8px;
  font-size: 12px;
  min-height: 0;
  color: var(--accent);
}

/* The draft a customer is holding, in the accent role BY CLASS. */
.invoice-draft {
  color: var(--accent);
}

/* A credit note: the kind badge, and its amount, both in the danger role. The
   minus sign itself is U+2212 and comes from the view — colour alone would not
   say `negative` to somebody who cannot see it. */
.invoice-kind,
.invoice-badge-danger {
  background: var(--danger-soft);
  color: var(--danger);
}

.invoice-negative {
  color: var(--danger);
}

/* A state that merely states — an unoffset credit note's `Åben` — in the same box
   as the confirming badges but in the quiet roles, which is `.reg-badge-closed`'s
   own trade. */
.invoice-badge-quiet {
  background: var(--raised);
  color: var(--dim);
}

/* ==========================================================================
   VÆLG ARBEJDE — `FakturaByg`, element by element.
   ========================================================================== */

/* The screen is a <form>, so it has to lay itself out like the content column
   every other screen gets for free, and it has to clear the sticky bar it puts
   over its own last card. 88px is the bar's height plus the gap the canvas
   leaves under it. */
.invoice-select {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-bottom: 88px;
}

.invoice-select-name {
  gap: 8px;
  min-width: 0;
}

.invoice-select-icon {
  display: flex;
  flex: none;
}

.invoice-select-customer-name {
  font-weight: 600;
}

/* A card of rows separated by hairlines: its own gap goes away, and the head
   keeps the canvas's tighter top padding. */
.invoice-group {
  gap: 0;
  padding-top: 10px;
}

.invoice-group-head {
  padding-bottom: 6px;
}

.invoice-group-tag {
  font-weight: 600;
}

/* One evening: a hairline above it, and the note tucked under the date. */
.invoice-pick {
  padding: 10px 0;
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 3px;
}

/* A row another draft holds: everything steps back, and the box is dead. The
   sentence under it is what says why — opacity alone explains nothing. */
.invoice-pick.is-held {
  opacity: .55;
}

.invoice-pick-date {
  font-weight: 600;
}

.invoice-pick-figures {
  gap: 14px;
  flex: none;
}

/* The amount in the row is the page's text colour, not the dim of the duration
   beside it: the canvas prints the money as the fact and the hours as its
   qualifier. */
.invoice-pick-amount {
  color: var(--text);
}

/* The note sits under the tick, not under the box — 24px of control plus the
   row's 10px gap. */
.invoice-pick-note {
  padding-left: 34px;
}

.invoice-sticky-text {
  min-width: 0;
}

.invoice-sticky-count {
  font-weight: 600;
}

.invoice-sticky-amount {
  color: var(--text);
}

/* `Videre` sits in the bar and never shrinks; disabled it is dim and inert,
   which is exactly how the canvas draws the first paint. */
.invoice-next {
  padding: 0 16px;
  flex: none;
}

.invoice-next:disabled {
  opacity: .4;
}

/* ==========================================================================
   GENNEMSE — `FakturaGennemse` and `FakturaGennemseMangler`, element by element.
   Every rule is one of their inline styles moved into a class; every colour is a
   token, because a literal here would be a theme this stylesheet cannot answer
   for.
   ========================================================================== */

.invoice-review {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* The document card is tighter than an ordinary card and roomier at the sides:
   it is a page, not a row. */
.invoice-doc {
  gap: 6px;
  padding: 16px;
}

/* The four uppercase labels — Faktura, Faktureres til, the lines header, Billeder
   på fakturaen — are one role drawn four times. */
.invoice-doc-label {
  letter-spacing: .06em;
  text-transform: uppercase;
}

.invoice-doc-number {
  font-size: 20px;
}

.invoice-doc-recipient {
  min-width: 0;
}

.invoice-doc-name {
  font-weight: 600;
}

/* Anything still missing, in the accent role BY CLASS — and each complaint stands
   only where the missing thing would print (owner, 2026-09-05): the issuer line,
   the recipient's address line, the lines themselves. */
.invoice-missing {
  color: var(--accent);
}

/* A complaint that has somewhere to go REPLACES the line it stands on, so it must
   still be the block that line was: `ellipsis` needs a box to trim, and the
   address sits among block siblings. The underline is the only thing that says
   "this text is where you go" — no colour of its own, the token is already
   right. */
a.invoice-missing {
  display: block;
  text-decoration: underline;
}

/* A draft with nothing on it says so where its lines would print, so the sentence
   wears the printed line's own spacing and hairline. */
.invoice-doc-empty {
  padding: 7px 0;
  border-top: 1px solid var(--line);
}

.invoice-doc-edit,
.invoice-doc-add {
  flex: none;
}

.invoice-doc-lines-head {
  gap: 8px;
}

/* One printed line: a hairline above it, the four columns, and the note under
   them. An Andet line is an <a> and an hours line a <div> — the same box either
   way, because one of them navigates and neither should look different for it. */
.invoice-doc-line {
  padding: 7px 0;
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 2px;
  color: var(--text);
  text-decoration: none;
}

.invoice-doc-line:hover {
  color: var(--text);
}

.invoice-doc-line-head {
  gap: 8px;
}

/* The date, the Timer figure and the amount never shrink; the description is the
   one column that gives way, because it is the only one that can be re-read. */
.invoice-doc-date,
.invoice-doc-hours,
.invoice-doc-amount {
  flex: none;
}

.invoice-doc-title {
  flex: 1;
  min-width: 0;
  font-weight: 700;
}

.invoice-doc-note {
  padding-left: 0;
}

/* The Moms row's right-hand side: the chip pair and then the figure. */
.invoice-doc-vat {
  gap: 8px;
}

.invoice-vat-chip {
  padding: 2px 8px;
  font-size: 11px;
  min-height: 0;
}

.invoice-doc-pay {
  font-size: 17px;
}

.invoice-doc-pay-label {
  font-weight: 700;
}

.invoice-doc-pay-amount {
  font-size: 18px;
}

/* THE MODREGN BLOCK — `FakturaGennemseModregn` and `…ForStor`, element for
   element. The label is the same uppercase role as the four above it and only
   moves down two pixels; the rows are the canvas's own box; the amount and the
   refusal wear the danger role, because both are money coming off. */
.invoice-doc-offset-title {
  margin-top: 2px;
}

.invoice-offset-row {
  width: 100%;
  padding: 7px 10px;
  margin: 0 -6px;
  border-radius: 10px;
}

/* A ROW IS A BUTTON ON A DRAFT, and a `.btn` it is not: these four lines undo
   what a browser does to a submit control, so the drawn row and the control that
   posts it are the same object. */
button.invoice-offset-row {
  background: none;
  border: 0;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.invoice-offset-row.is-chosen {
  background: var(--accent-soft);
}

/* A note larger than the invoice steps back and stays where it is. An UNTICKED
   one is rendered as a div and has nothing to click; a ticked one is still a
   button, because taking it off is the way out of the state. */
.invoice-offset-row.is-refused {
  opacity: .45;
}

.invoice-offset-text {
  min-width: 0;
}

.invoice-offset-lines {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.invoice-offset-number {
  font-weight: 600;
}

.invoice-offset-number .dim {
  font-weight: 400;
}

.invoice-offset-amount {
  flex: none;
  color: var(--danger);
}

.invoice-offset-refusal {
  color: var(--danger);
}

.invoice-doc-images-text {
  min-width: 0;
}

.invoice-doc-tiles {
  gap: 6px;
  flex: none;
}

/* A picture the document may attach: the thumb, and a tick badge that is only
   there when it is attached. A tile that is left out steps back rather than
   disappearing — it is still a picture somebody took. */
.invoice-tile {
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: var(--raised);
  padding: 0;
  flex: none;
  overflow: visible;
  opacity: .35;
}

.invoice-tile.is-chosen {
  opacity: 1;
}

.invoice-tile-image {
  width: 100%;
  height: 100%;
  border-radius: 8px;
  object-fit: cover;
  display: block;
}

.invoice-tile-check {
  position: absolute;
  right: -4px;
  top: -4px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  display: none;
  align-items: center;
  justify-content: center;
}

.invoice-tile.is-chosen .invoice-tile-check {
  display: flex;
}

.invoice-issue {
  min-height: 56px;
  font-size: 17px;
  /* THE FORM AROUND IT IS NOT THE CANVAS'S LAYOUT. On `FakturaGennemse` the
     button is a direct child of `.content`, a stretching flex column, so it
     spans the card; here it lives inside the `<form>` that posts it and a
     `.btn` is `inline-flex`, so it shrank to its words while `Slet kladde`
     under it stayed full width. The one irreversible control on the screen is
     the one that must not be the smallest. */
  width: 100%;
}

.invoice-issue:disabled {
  opacity: .4;
}

/* `Slet kladde` is quiet and dangerous at once: a ghost button in the danger
   role, which is how the canvas draws it. */
.invoice-delete {
  min-height: 44px;
  width: 100%;
  color: var(--danger);
}

.invoice-delete-confirm {
  gap: 10px;
  min-height: 44px;
}

.invoice-delete-actions {
  gap: 8px;
  flex: none;
}

.invoice-delete-yes {
  color: var(--danger);
}

/* ==========================================================================
   UDSTEDT and the FROZEN DOCUMENT — `FakturaUdstedt`, `FakturaFrossen`.
   ========================================================================== */

.invoice-issued {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* The status card is the one card on this screen laid out as a ROW: the money on
   the left and where it stands on the right, which is how the canvas draws it. */
.invoice-issued-status {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
}

.invoice-issued-figures {
  min-width: 0;
}

.invoice-issued-amount {
  font-size: 22px;
}

/* WHAT PAID THE REST, under the net headline. `FakturaUdstedtModregn`'s third
   line: quiet, small, and directly beneath the figure it explains — the head
   card's left column is three lines on an offset invoice and two on every other
   one, with nothing else about the page changed. */
.invoice-issued-offset {
  margin-top: 2px;
}

/* `Ikke sendt` is a chip in the ACCENT role and not the dim one: it is the state
   the whole page is about. */
.invoice-issued-state {
  color: var(--accent);
  flex: none;
}

/* Once it has gone, the same chip wears the ok role and a check instead of the
   clock — the way the Gmail row in Indstillinger says Forbundet. Phase 9 made
   that promise good: `document_sends` now has something in it. */
.invoice-issued-state.is-sent {
  color: var(--ok);
}

/* `Send til`, `Besked`, the buttons and the reason line are ONE FORM since phase
   9, and the form is now what carries the canvas's 12px rhythm between them —
   `.invoice-issued`'s own gap only sees the status card, the form and the rows
   card. Same spacing, one more element in the tree. */
.invoice-send-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.invoice-send-to-value {
  gap: 8px;
  min-width: 0;
}

/* `Skift` IS THE DISCLOSURE. A checkbox wearing the word, invisible and still
   focusable by `.chip-radio`'s rule, with `:has(:checked)` doing the revealing —
   so the field opens with no JavaScript at all and ONE thing says whether it is
   open (07-04's rule, with CSS as that one thing). */
.invoice-send-to-change {
  position: relative;
  flex: none;
  color: var(--accent);
}

.invoice-send-to-field {
  display: none;
}

.invoice-send-to:has(.invoice-send-to-change .chip-radio:checked) .invoice-send-to-field {
  display: flex;
}

.invoice-send-to:has(.invoice-send-to-change .chip-radio:focus-visible) .invoice-send-to-change {
  text-decoration: underline;
}

/* The message field is an input that grew: same box, same border, room for three
   lines and its text starting at the top instead of centred. */
.invoice-message {
  min-height: 76px;
  align-items: flex-start;
  padding-top: 12px;
  padding-bottom: 12px;
  font-family: inherit;
  font-size: 15px;
  color: var(--dim);
  resize: vertical;
}

/* Gmail takes the room it can and PDF takes what it needs — the canvas's own
   two-column grid, and the reason PDF is not half the width of the page. */
.invoice-issued-actions {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
}

.invoice-send,
.invoice-pdf {
  min-height: 56px;
}

/* `.invoice-pdf` IS AN ANCHOR since phase 8 while `.invoice-send` is still a
   button, and the two sit in one grid row. `.btn` already gives the anchor the
   box — inline-flex, the 48px floor, the centring — and `.btn.ghost` already
   gives it `color: var(--dim)`, which beats the browser's link colour on
   specificity. The ONE thing neither covers is the underline. */
.invoice-pdf {
  padding: 0 16px;
  text-decoration: none;
}

/* A PDF control fetching the document for the share sheet (installed app on a
 * platform that shares files): held, and dimmed to say so — no colour of its
 * own, so every theme keeps it. */
.invoice-pdf[aria-busy="true"],
.invoice-row-pdf[aria-busy="true"] {
  opacity: 0.5;
  pointer-events: none;
}

/* The ONE complaint on this page that leads somewhere: `Kunden har ingen e-mail.
   Giv kunden en e-mail.` opens Redigér kunde. The accent role plus the underline
   this product uses to say "this text is a control" — the same pair
   DocumentCard's missing-fields sentence wears, and the whole line is the link
   (ruling 3), never a sentence cut around an inner clause. */
.invoice-send-fix {
  color: var(--accent);
  text-decoration: underline;
}

/* THE DEAD CONTROLS ARE DRAWN, NOT HIDDEN (07-CONTEXT): the page looked final
   from day one, and each said its own true reason. THERE ARE NONE LEFT.
   `.invoice-pdf` woke in phase 8, `Send med Gmail` in phase 9 — `:disabled` now
   dims it only while the account has no Gmail or the customer has no e-mail,
   which is a live control saying why it cannot run — and `Kreditér faktura` woke
   in phase 10, which is why `.invoice-issued-row.is-dead` and its `opacity:.4`
   are gone with it (10-01 took the same opacity off the artboard). */
.invoice-send:disabled {
  opacity: .4;
}

.invoice-issued-rows {
  gap: 2px;
  padding: 10px 16px;
}

.invoice-issued-row {
  min-height: 36px;
  color: inherit;
  text-decoration: none;
}

/* `Kreditér faktura` is DANGEROUS, not merely missing: the canvas paints it in
   the danger role, the same role `Slet kladde` wears, because crediting an
   issued invoice is the one destructive thing left to do to it. It wore
   `--accent` — the role that means "something is missing here" — which is a
   different sentence. */
.invoice-issued-credit {
  color: var(--danger);
}

.invoice-issued-row-hint {
  gap: 4px;
}

/* The frozen view is the review with nothing under it: no refusal card, no
   Udsted, no Slet. It needs no rule of its own beyond saying so — the card
   inside it is `card.php` with `frozen` true, which is the whole design. */
.invoice-frozen {
  gap: 12px;
}

/* ==========================================================================
   KREDITNOTA UDSTEDT — the two cards a note's page has and an invoice's has not.

   `KreditUdstedt` and its five siblings, element by element. Everything above
   this block is shared with the invoice's page unchanged, which is the point:
   one page, two kinds, and the send form spelled once.
   ========================================================================== */

/* The reversal wears the danger role — the amount is the one figure on the page
   that says which direction the money went. */
.invoice-negative {
  color: var(--danger);
}

/* The released-work card is the accent card the canvas draws: soft accent fill
   AND border, so it reads as a statement about what happened rather than as
   another row of the document. */
.invoice-released {
  gap: 6px;
  padding: 12px 16px;
  border-color: var(--accent-soft);
  background: var(--accent-soft);
}

.invoice-released-head {
  font-weight: 600;
}

/* `Opret ny faktura` is a lit chip that is an ANCHOR: `.chip.on` already gives it
   the fill, and the only thing it does not cover is the underline. */
.invoice-released-chip {
  flex: none;
  gap: 4px;
  text-decoration: none;
}

/* `Til gode 5.391,10 kr.` is an ordinary card of rows, tighter than the document
   card above it because its rows are controls rather than figures. */
.invoice-balance {
  gap: 2px;
  padding: 10px 16px;
}

.invoice-balance-title {
  min-height: 36px;
  font-weight: 600;
}

/* WHERE THE TWO CLOSES WERE, on a note an open draft is holding. One sentence at
   the rows' own vertical rhythm, and no colour of its own: `.dim` already carries
   the role, and `KreditUdstedtReserveret` draws it exactly so. */
.invoice-balance-reserved {
  padding: 10px 0;
}

/* Each close is a BUTTON wearing a row: it opens a confirm rather than going
   anywhere, so it must be a button — and the reset is what makes a button look
   like the anchor rows above it. */
.invoice-credit-row {
  width: 100%;
  min-height: 36px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
}

/* `Afskriv` carries its own line under it, so the row and its helper are one
   block with the card's own air around them. */
.invoice-credit-writeoff {
  padding: 6px 0;
}

.invoice-credit-writeoff .invoice-credit-row {
  min-height: 24px;
}

/* The confirm replaces BOTH rows: the sentence over its two buttons, which is
   what the two confirm frames draw. */
.invoice-credit-confirm {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 10px 0;
}

.invoice-credit-actions {
  gap: 8px;
}

.invoice-credit-actions .btn {
  min-height: 44px;
  padding: 0 16px;
}

/* ==========================================================================
   GENNEMSE KREDITNOTA — `KreditGennemse`, element by element.

   The note's card is the invoice's card seen from the other side: the same
   roles, the same hairlines, one badge in the danger role and one total with
   the minus on it. Its accent cards are the phase's whole user input.
   ========================================================================== */

.invoice-credit {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Roomier than `.invoice-doc` by two pixels of gap, which is how the canvas
   draws it: this card has five blocks and not fifteen lines. */
.invoice-credit-doc {
  gap: 8px;
  padding: 16px;
}

/* The `Krediterer` badge is the sentence the page exists for, and the canvas
   paints it in the DANGER role — the same role `Kreditér faktura` wears on the
   invoice's own page. `.badge` is the ok role by default, so both tokens are
   restated here rather than a second badge shape being invented. (No document
   number in this comment: the colour-literal detector reads a four-digit hash
   as a hex colour, and it is right to.) */
.invoice-credit-badge {
  background: var(--danger-soft);
  color: var(--danger);
}

/* The lines row is a LINK and must not look like one: it is a row of the
   document, and the chevron on its right is the affordance — the Udstedt page's
   own lines row is built exactly this way. */
.invoice-credit-lines {
  color: inherit;
  text-decoration: none;
  gap: 8px;
}

.invoice-credit-total {
  font-size: 17px;
}

.invoice-credit-total-label {
  font-weight: 700;
}

.invoice-credit-total-amount {
  font-size: 18px;
  color: var(--danger);
}

/* THE CHOICE CARD. The list is an OVERLAY, so it costs no height until it is
   open — which is why this screen has neither of the artboard's two 118 px
   spacers, and why `credit_review()` in build.py explains them as the room its
   two simultaneously-open dropdowns need. */
.invoice-choice {
  position: relative;
}

.invoice-choice-card {
  gap: 6px;
  padding: 12px 16px;
  border-color: var(--accent-soft);
  background: var(--accent-soft);
}

.invoice-choice-question {
  font-weight: 600;
  min-width: 0;
}

/* The chip is lit (`.chip.on`) but sits on the raised surface rather than the
   accent one: it stands ON an accent card, and accent on accent would vanish.
   Two classes in the selector, because `.chip.on` has two and would otherwise
   win the background back — it did, until browser pass 11-05 read the colour. */
.chip.invoice-choice-chip {
  position: relative;
  flex: none;
  background: var(--raised);
}

/* AND IT IS LIT THE MOMENT EITHER OPTION IS PICKED — the same `:has(:checked)`
   that hides the card's complaint below, so ONE thing says the card is answered
   (07-04's rule); the server renders `on` for the same state on a full page.
   Only the ink and the border: `:has()` carries its argument's specificity and
   would otherwise take the raised background back to the accent one. */
.invoice-choice:has(.invoice-choice-option .check-input:checked) .invoice-choice-chip {
  border-color: transparent;
  color: var(--accent);
}

.invoice-choice-caret {
  display: flex;
  transform: rotate(90deg);
}

/* THE DISCLOSURE, with CSS as the one thing that says whether the list is open
   (07-04's rule, the shape `Skift` already uses). The chip's checkbox is
   invisible and still focusable — `.chip-radio`'s rule — so the list opens, a
   radio is picked and the form posts with no JavaScript at all. */
.invoice-choice-list {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 6px);
  box-shadow: var(--shadow);
  border-color: var(--line-strong);
  z-index: 2;
}

.invoice-choice:has(.invoice-choice-chip .chip-radio:checked) .invoice-choice-list {
  display: block;
}

.invoice-choice:has(.invoice-choice-chip .chip-radio:focus-visible) .invoice-choice-chip {
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* `.suggest > div`'s geometry, restated for a <label>: the options have to be
   labels so a tap anywhere on one picks its radio. */
.invoice-choice-option {
  display: flex;
  align-items: center;
  min-height: 56px;
  padding: 8px 14px;
  border-top: 1px solid var(--line);
}

.invoice-choice-list > .invoice-choice-option:first-child {
  border-top: 0;
}

/* The canvas draws these ticks ROUND, unlike Vælg arbejde's square boxes: one
   of two is chosen here, and the shape is what says so before the words do. */
.invoice-choice-check {
  border-radius: 50%;
}

.invoice-choice-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.invoice-choice-label {
  font-weight: 600;
}

/* The scope card's rows choose between two SUMS, so the sum is on the row —
   right-aligned, in the money column every other screen keeps it in. `.between`
   pushes it there; this rule only stops it being squeezed by a long helper. */
.invoice-choice-amount {
  flex: none;
}

/* The `row` inside a `between` option: it has to be able to shrink, or a helper
   that wraps to two lines pushes the amount off the card. */
.invoice-choice-body {
  min-width: 0;
}

/* MANGLER'S SHAPE, one card over: the complaint stands where the answer would
   print, in the accent colour and underlined — which is how this canvas says
   "this text is a control" (owner, 2026-09-05, on FakturaGennemseMangler). */
.invoice-choice-missing {
  color: var(--accent);
  text-decoration: underline;
}

/* And it goes the moment either option is picked, with CSS as the ONE thing that
   says whether this card has been answered (07-04's rule, and the same
   `:has(:checked)` family the disclosure two rules above uses). A line in
   `wireChoiceCards()` would be a second thing saying it, and the first tap would
   settle which of the two was wrong. */
.invoice-choice:has(.invoice-choice-option .check-input:checked) .invoice-choice-missing {
  display: none;
}

/* ==========================================================================
   TILFØJ LINJE — `FakturaTilfoejLinje`, element by element.
   ========================================================================== */

.invoice-sheet,
.invoice-sheet-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* The chip pair is a pair of RADIOS wearing chips, so a keyboard reaches them
   and the lit half survives a page with no JavaScript at all. The input itself
   is invisible and still focusable — `.check-input`'s rule, for its reason. */
.chip-radio {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  appearance: none;
}

.invoice-sheet-vat > .chip {
  position: relative;
}

.invoice-sheet-vat > .chip:has(.chip-radio:checked) {
  background: var(--accent-soft);
  border-color: transparent;
  color: var(--accent);
}

.invoice-sheet-vat > .chip:has(.chip-radio:focus-visible) {
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.invoice-sheet-save {
  min-height: 56px;
  font-size: 17px;
}

/* The small line tucked under a field: `Hentet fra CVR-registret …` after a
   number lookup, and 04-08's `Udfyldt fra …` show-then-undo line after a pick.
   Same geometry, because the canvas draws them as the same line. */
.customer-note {
  margin-top: -6px;
  gap: 6px;
}

/* `Erstat alt` and `Fortryd`: words in the accent, not boxes — the same
   no-chrome button as Skift on Start. */
.fill-note-action {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.customer-pin {
  display: flex;
  flex: none;
}

/* Gem kunde beside Slet on the edit screen; Opret kunde alone, full width. */
.customer-actions {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  margin-top: 4px;
}

.customer-actions.single {
  grid-template-columns: minmax(0, 1fr);
}

.customer-delete {
  min-height: 56px;
}

/* The Kunde field on Start: full width inside the centred run sections, at the
   artboards' own distances — 14px under the idle hero, 20px under the ring. */
.customer-field {
  width: 100%;
}

.run-idle .customer-field {
  margin-top: 14px;
}

.run-running .customer-field {
  margin-top: 16px;
}

.customer-chosen-name {
  gap: 8px;
  min-width: 0;
}

/* The WHOLE folded row is the tap target (owner, 2026-09-01): Skift is a word
   on it, not the only door — and with no customer while the clock runs, the
   row's `Vælg kunde` placeholder is dim, like every other placeholder. */
[data-customer-chosen] {
  cursor: pointer;
}

.customer-chosen-empty {
  color: var(--dim);
}

/* Skift is a button that looks like the artboard's dim word — no box. */
.customer-change {
  padding: 0;
  border: 0;
  background: none;
  color: var(--dim);
  font: inherit;
  font-size: 13px;
  text-decoration: none;
}

/* `Redigér · Skift` (owner, 2026-09-10): the link and the button as one group
   at the row's right edge, a dot between them, neither wrapping. */
.customer-change-group {
  flex: none;
  gap: 6px;
  white-space: nowrap;
}

/* One suggestion row: a real button (or the Ny kunde anchor) wearing the
   artboard's .suggest > div geometry — 44px, split, a hairline above. */
.suggest-row {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 44px;
  padding: 0 14px;
  border: 0;
  border-top: 1px solid var(--line);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 15px;
  text-align: left;
  text-decoration: none;
}

.suggest-row:hover {
  color: var(--text);
}

.suggest > :first-child {
  border-top: 0;
}

.suggest-row-name {
  gap: 8px;
  min-width: 0;
}

.suggest-row-icon {
  display: flex;
  flex: none;
}

.suggest-row-hint {
  white-space: nowrap;
}

/* The plus on Ny kunde … and on the create-from-CVR row, in the accent. */
.suggest-new-icon {
  color: var(--accent);
}

/* THE RAISED SKIFT STATE (owner's sketch, 2026-08-31 — StartSkiftKunde).
   Tapping Skift while the clock runs raises the SAME field to EXACTLY the
   resting screen's position: top 419px, measured pixel-equal to StopurKlar's
   field on the 390x844 artboard, and the same figure on every device because
   the idle column is top-anchored. The clock ghosts behind the scrim; the
   movement is one transform transition the module runs FLIP-style, forwards on
   Skift and in reverse on a pick or Annuller. Both sit above the tab bar,
   which carries no z-index of its own. */
.customer-scrim {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: var(--bg);
  opacity: .9;
}

/* Doubled with .customer-field on purpose: `.run-running .customer-field`
   sets margin-top 16 at (0,2,0), and a fixed element's top is top PLUS its
   margin — a single-class raised rule lost that fight and landed at 435. */
.customer-field.customer-field-raised {
  position: fixed;
  left: 20px;
  right: 20px;
  /* AT THE TOP OF THE SCREEN, not at the resting position (owner, 2026-09-09,
     reversing the 2026-08-31 StartSkiftKunde placement at 419px): on a phone
     the keyboard rises over the lower half, and a field that animated down to
     sit under the clock was behind it, list and all. From the top the field
     and its list are the only things above the keyboard, which is the point
     of the scrim anyway. The keyboard shrinks the viewport (head.php's
     `interactive-widget=resizes-content`), so the list's cap below follows it. */
  top: calc(env(safe-area-inset-top) + 12px);
  z-index: 21;
  margin-top: 0;
  /* .customer-field's width: 100% would beat the right edge — a fixed element
     with left AND width ignores `right` and ran off the screen on desktop. */
  width: auto;
}

.customer-field-moving {
  transition: transform var(--dur) var(--ease);
}

/* The raised list ends where the screen (or the keyboard) does, and scrolls
   inside itself past that: 12px above the field, ~100px of chips row and
   input, and a little air at the foot. */
.customer-field-raised [data-customer-suggest] {
  max-height: calc(var(--screen, 100dvh) - env(safe-area-inset-top) - 140px);
  overflow-y: auto;
}

/* THE SAVE FORMS OPEN THE LIST WITHOUT MOVING ANYTHING (owner, 2026-09-02 —
   StopurGemNyKunde). The field stays where it was tapped and the list hangs off
   its foot, OVER Tag, Note and Filer: on a form with four rows under Kunde,
   Start's raise would be the wrong answer twice, once for moving the row the
   person just touched and once for washing the form out behind a scrim. Same
   spelling as `.name-options`, which is the same idea one screen over. */
.customer-field-anchor {
  position: relative;
}

/* Above the tap-catching scrim, or the scrim would swallow the taps on the very
   list it exists to close. */
.customer-field-anchor[data-customer-choosing] {
  z-index: 21;
}

.customer-field-anchor[data-customer-choosing] [data-customer-suggest] {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 6px);
  z-index: 5;
  box-shadow: var(--shadow);
  border-color: var(--line-strong);
}

/* The scrim an anchored list opens behind is a tap target and nothing else: a
   dropdown that dimmed the page would be the raised state wearing a different
   position. It still closes on a tap, which is the half of the scrim that is
   actually load-bearing. */
.customer-scrim.customer-scrim-clear {
  opacity: 0;
}

/* The raised Skift state's pinned row: the run's own customer, its distance and
   its check — the check in accent, like every chosen state on the canvas. */
.suggest-row-current {
  gap: 8px;
}

.suggest-current-check {
  display: flex;
  color: var(--accent);
}

/* Ny kunde's Navn overlay (04-07, drawn as NyKundeForslag): absolute under the
   field, OVER the fields below on purpose — moving on to the next field is the
   "no thanks" that dismisses it. The anchor is the label, so the overlay hangs
   off the whole field's foot. */
.name-suggest-anchor {
  position: relative;
}

.name-options {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 6px);
  z-index: 5;
  display: block;
  box-shadow: var(--shadow);
  border-color: var(--line-strong);
}

/* The CVR field's matches state (owner, 2026-09-01): the input's own box,
   as a button reading `Fandt {n} virksomheder`, its chevron pointing at the
   dropdown it opens. */
.customer-matches {
  font: inherit;
  color: var(--text);
  cursor: pointer;
  text-align: left;
}

.customer-matches-chevron {
  display: flex;
  flex: none;
}

/* With nothing chosen the count IS the field's contents: it stands at the left
   and its chevron at the far edge, as NyKundeCvrFundet draws it. With a number
   beside it (NyKundeCvrOmvalg) the pair moves to the right and dims — the
   number is what is CHOSEN, the count only what is AVAILABLE. */
.customer-matches-count {
  flex: 1;
  justify-content: space-between;
  gap: 6px;
  white-space: nowrap;
}

.customer-matches.has-number .customer-matches-count {
  flex: none;
  justify-content: flex-end;
  color: var(--dim);
}

/* The one dim line that says which kind of empty an empty list is. */
.suggest-notice {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 14px;
}

/* While the rate field shows its `Standard · …` placeholder the unit stays
   hidden — the placeholder already ends in the unit, and the artboard draws
   nothing beside it. The first digit typed replaces one with the other. */
.input:has(.money-field:placeholder-shown) .money-suffix {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .ring::before,
  .halo,
  .dot,
  .login-hero,
  .login-title,
  .login-tagline,
  .login-signin {
    animation: none;
  }

  .btn,
  .customer-field-moving {
    transition: none;
  }

  /* A SEPARATE RULE, and the separation is the point: one unknown selector in a
     list invalidates the WHOLE list, so a browser that has never heard of
     ::view-transition-group() would silently drop the rule above with it and
     every animation this block exists to stop would come back on. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none;
  }
}

/* ==========================================================================
   Saving the work: the Tag/Note rows, the file tray, the viewer and the two
   editable clock chips. Every number here is off `StopurGem`, `StopurGemFiler`,
   `StopurGemBilag` and `StopurGemManuelt`; every colour is a token, because a
   theme is exactly one :root[data-theme] block that redefines tokens and
   nothing else.
   ========================================================================== */

/* Note is a real textarea inside an .input box: top-aligned and three lines
   tall, which is the artboard's 76 px. No maxlength — the server refuses a long
   note with a sentence rather than silently swallowing the characters. */
.work-note {
  min-height: 76px;
  align-items: flex-start;
  padding-top: 12px;
  width: 100%;
  font: inherit;
  font-size: 16px;
  resize: vertical;
  appearance: none;
}

/* ==========================================================================
   ADMIN — `Admin`, `AdminBruger`, `AdminLog`: the rows are the settings card's
   rows with a lead and an end, and the day bar is a `<progress>` the browser
   fills from a count and a maximum. Read-only screens, nothing to press but
   the cards on the users page.
   ========================================================================== */
.admin-facts {
  gap: 4px;
}

.admin-mail {
  gap: 6px;
}

.admin-rows {
  padding: 2px 16px;
}

.admin-row,
.admin-day {
  min-height: 40px;
  border-top: 1px solid var(--line);
}

.admin-row:first-child,
.admin-day:first-child {
  border-top: 0;
}

.admin-row-lead {
  gap: 8px;
  min-width: 0;
}

.admin-row-end {
  gap: 8px;
  flex: none;
}

.admin-number {
  font-weight: 600;
}

.admin-to {
  max-width: 110px;
}

.admin-refused {
  color: var(--danger);
}

/* The day bar is a <progress>: the count against the busiest day, drawn by
   the browser — no width is computed and none is written inline, which the
   CSP would drop. Both engines' pseudo-elements are painted the same accent. */
.admin-bar {
  width: 120px;
  height: 8px;
  border: 0;
  border-radius: 4px;
  overflow: hidden;
  background: var(--surface);
  appearance: none;
  accent-color: var(--accent);
}

.admin-bar::-webkit-progress-bar {
  background: var(--surface);
}

.admin-bar::-webkit-progress-value {
  background: var(--accent);
  border-radius: 4px;
}

.admin-bar::-moz-progress-bar {
  background: var(--accent);
  border-radius: 4px;
}

/* ==========================================================================
   BESKED — `IndstillingerBesked`, element by element: the chip palette wraps,
   the draft is a taller Note, and the example keeps the draft's line breaks.
   ========================================================================== */
.message-tokens {
  flex-wrap: wrap;
  gap: 8px;
}

.message-draft {
  min-height: 132px;
  align-items: flex-start;
  padding-top: 12px;
  width: 100%;
  font: inherit;
  font-size: 15px;
  line-height: 1.4;
  resize: vertical;
  appearance: none;
}

.message-example {
  font-size: 15px;
  line-height: 1.4;
  color: var(--dim);
  white-space: pre-line;
  overflow-wrap: anywhere;
}

/* Start: ONE field, sized to its own content, with the box beside it on the same
   row (owner, 2026-09-02). `Dato` and `Starttid` merged, and the width is the
   point — a full-width box with a calendar glyph floating out at the far edge
   says "type a sentence here" about a value nobody types at all. */
.work-start-row {
  gap: 12px;
  /* `ingen starttid` drops under the date when the two do not fit one line —
     the row ran 5px past a 360px screen (measured 2026-09-09), and further
     with the text size turned up. At the canvas's width they share the line. */
  flex-wrap: wrap;
}

/* `input.input` is what this has to beat: it sets width:100% at (0,1,1) so a
   real input fills its box, and a single class could not reach it. MEASURED in
   Chrome before that fix — the old field came out 335 px wide on a 375 px
   viewport and pushed `ingen starttid` to x=367, twelve pixels past the content
   column, which is what the owner saw as a checkbox off the screen.

   `width: auto` and NOT a number, which is the difference from the field this
   replaced. A native date control has an intrinsic width: the browser lays out
   the fields it renders — and it renders them in the DEVICE's locale, the
   owner's own ruling — so `auto` is exactly as wide as `01.09.2026 14:23` here
   and as wide as `09/01/2026 02:23 PM` on a phone set to English, with neither
   clipped and neither padded out. It is also what makes the artboard's two
   widths true with no second rule: ticking `ingen starttid` swaps the type to
   `date`, the content becomes `01.09.2026`, and the field narrows on its own. */
input.input.work-start {
  width: auto;
  flex: none;
  justify-content: flex-start;
}

.work-untimed {
  gap: 8px;
  cursor: pointer;
}

/* THE READOUT IS THE DURATION FIELD (owner, 2026-09-02, StopurGemManuelt). Two
   inputs wearing the readout's own type, underlined in accent because that is how
   the canvas says "this text is a control", and nothing else about them is a box:
   no background, no border, no padding, so the figure reads as one 34 px number
   with two of its parts writable. `font: inherit` takes the mono face and the
   34 px from .readout; the colour is inherited too, which is what makes the muted
   waiting state grey the digits and their placeholders together. */
/* Baseline, not centre: the two inputs carry an underline and are therefore
   taller than the colons between them, and centring made the colons ride up. */
.work-readout {
  display: flex;
  align-items: baseline;
}

.work-readout-seg {
  width: 2ch;
  padding: 0;
  border: 0;
  border-bottom: 2px solid var(--accent);
  border-radius: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: center;
  appearance: none;
  cursor: pointer;
}

/* Three digits — a hundred hours and more — widen the box by one character
   rather than scrolling inside it, so the readout stays readable at 137:30:00. */
.work-readout-seg.wide {
  width: 3ch;
}

.work-readout-seg::placeholder {
  color: inherit;
  opacity: 1;
}

/* A real control that has to stay focusable and reachable by a screen reader
   while the drawn box beside it is what anybody sees. `display:none` would take
   it out of the tab order and out of the accessibility tree, which is the
   difference between a styled checkbox and a picture of one. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* THE TRAY'S GEOMETRY IS MEASURED, NOT CHOSEN (owner, 2026-09-01): five whole
   thumbs and half a sixth. 5 x 60 + 5 x 8 + 30 = 370 against a 350 px content
   column, so the row bleeds 20 px into the page's own right padding and the cut
   lands on the screen's edge. The half thumb IS the scroll affordance — on a
   phone nothing else says "there is more this way". */
.media-choosers {
  gap: 8px;
}

.media-chooser {
  flex: 1;
  min-height: 48px;
  padding: 0 8px;
}

/* The twentieth file: the choosers stay in place and stop answering, rather
   than disappearing and moving everything under them. */
.media-choosers.is-full .media-chooser {
  opacity: .4;
}

.media-row {
  display: flex;
  gap: 8px;
  margin-top: 8px;
  margin-right: -20px;
  padding-right: 20px;
  overflow-x: auto;
  scrollbar-width: none;
}

.media-row::-webkit-scrollbar {
  display: none;
}

.media-row:empty {
  display: none;
}

.thumb-tile {
  position: relative;
  width: 60px;
  height: 60px;
  flex: none;
  /* Failed and uploading files are pulled to the FRONT of the row and finished
     ones drift right, over 200 ms (owner, 2026-09-01) — the two thumbs that
     need a thumb are the two within reach. */
  transition: transform 200ms var(--ease);
}

.thumb-face {
  position: absolute;
  inset: 0;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: linear-gradient(135deg, var(--raised), var(--line-strong));
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: 0;
  color: var(--text);
}

.thumb-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.thumb-document {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  max-width: 56px;
  color: var(--dim);
}

.thumb-name {
  font-size: 9px;
  max-width: 54px;
}

/* The badge takes NO opacity: opacity on the span dims the glyph with the disc,
   and a play arrow you have to look for is not a play arrow (measured on the
   canvas, 05-01). */
.thumb-play {
  position: absolute;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  padding-left: 2px;
}

/* Uploading: a ring with the PERCENTAGE inside it. The ring is a conic gradient
   driven by --media-progress, which app.js sets as a custom property — a number
   and not a colour, so no theme name and no literal reaches the module. */
.thumb-progress {
  position: absolute;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: conic-gradient(var(--accent) 0 calc(var(--media-progress, 0) * 1%), var(--line-strong) 0 100%);
}

.thumb-percent {
  width: 31px;
  height: 31px;
  border-radius: 50%;
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 700;
}

.thumb-tile.is-uploading .thumb-image,
.thumb-tile.is-uploading .thumb-play,
.thumb-tile.is-failed .thumb-image,
.thumb-tile.is-failed .thumb-play {
  opacity: .25;
}

/* Failed: the triangle and a small looping arrow, and THE RETRY IS THE TAP ON
   THE TILE. There is no `Prøv igen` chip — the owner deleted it from the
   product, because a chip beside a thumb costs a row of height per failed file
   and says nothing the arrow does not. */
.thumb-tile.is-failed .thumb-face {
  border: 1.5px solid var(--danger);
  background: var(--danger-soft);
}

.thumb-failed {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--danger);
}

/* The ↻ is the ONE mark that tells the two reds apart (owner, 2026-09-02): a
   transport failure keeps it and a tap tries again, a format refusal has none and
   a tap re-shows its sentence. It is hidden with the `hidden` attribute like
   every other slot on this tile, and the sheet's own `[hidden]{display:none
   !important}` at the top is what makes that beat the display below. */
.thumb-retry {
  position: absolute;
  right: 3px;
  bottom: 3px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--danger);
  color: var(--on-accent);
  display: flex;
  align-items: center;
  justify-content: center;
}

.thumb-remove {
  position: absolute;
  right: 3px;
  top: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--raised);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/* The two recorded clock times, as chips in the card (owner, 2026-09-01). The
   input lies over its chip at zero opacity rather than being hidden: a hidden
   input cannot be tapped, and showPicker() is refused without a user gesture on
   some browsers — this way the tap lands ON the native control. */
.run-times {
  gap: 8px;
}

.run-times-icon {
  display: flex;
  flex: none;
}

.run-time-chip {
  position: relative;
  min-height: 34px;
  padding: 0 12px;
  gap: 8px;
}

.run-time-value {
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
}

.run-time-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  border: 0;
  padding: 0;
  font: inherit;
  appearance: none;
}

.run-time-chip:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* StopurGemBilag: the file full screen, and the video PLAYS here. Fixed rather
   than absolute, because the page under it scrolls and a viewer that scrolled
   with it would be a picture halfway off the screen. */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: var(--bg);
  display: flex;
  flex-direction: column;
}

.viewer-bar {
  padding: 58px 20px 0;
}

/* Bare accent glyphs, as the artboard draws them — a button's own UA
   background is a grey box the canvas never had (owner, 2026-09-02). */
.viewer-tool {
  background: none;
  border: 0;
  padding: 0;
  color: var(--accent);
}

.viewer-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.viewer-stage {
  position: relative;
  width: 100%;
  min-height: 220px;
  background: linear-gradient(135deg, var(--raised), var(--line-strong));
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
}

.viewer-image,
.viewer-video {
  max-width: 100%;
  max-height: 60vh;
}

.viewer-document {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 28px 20px;
  color: var(--dim);
  max-width: 100%;
}

/* Positioned against the MEDIA BOX and not the column: the filename sits in
   that column too, and 50 % of it put both arrows above the picture they page
   through (measured in headless Chrome, 05-01). */
.viewer-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--raised);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

.viewer-prev {
  left: 12px;
}

.viewer-next {
  right: 12px;
}

.viewer-caption {
  padding: 16px 20px 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.viewer-title {
  font-weight: 600;
}

/* ------------------------------------------------------------------------- *
 * Redigér arbejde and the read-only view of billed work (HIST-03, HIST-04).
 * One template in two states, so one block of rules in two states.
 * ------------------------------------------------------------------------- */

/* `RegistreringLaast` draws every row as a FACT. A CLASS and not an inline
   colour, and that is the whole reason it exists: .input, .label and .readout
   each set their own colour, so an inline colour on the wrapper would lose to
   all three and half the screen would stay at full contrast (measured on the
   canvas, 06-01). Its geometry is the content column's, above. */
.readonly .input,
.readonly .label,
.readonly .readout {
  color: var(--muted);
}

/* THE UNDERLINE IS HOW THIS DESIGN SAYS "THIS TEXT IS A CONTROL", so a screen
   where nothing is a control may not draw one. The digits stay; the accent goes. */
.readonly-readout .work-readout-seg {
  border-bottom: 1px solid var(--line);
}

/* Afrunding: a NATIVE select wearing .input, keeping the phone's own picker
   wheel and its own indicator. No `appearance: none` — a hand-drawn chevron
   would be a colour literal in a component rule and a control that is no longer
   the one iOS knows how to open. */
.reg-rounding {
  display: block;
  width: 100%;
  height: 48px;
  line-height: normal;
}

/* The badge and its chip, under the header on billed work. */
.reg-badge-row {
  gap: 8px;
}

/* The opened Slet: the sentence over the two answers, in the grid the button
   stood in (RedigerArbejdeUdenTid). */
.reg-delete-confirm {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.reg-delete-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 10px;
}

/* THE OFFLINE PAGE (PLAT-01; the `Offline` and `LysOffline` frames). The canvas
   draws all of this inline, and under a style-src with no 'unsafe-inline' every
   one of those declarations has to be a class — the same move .login made.
   Nothing here names a colour: .dim, .muted and .btn.primary already carry the
   three roles this screen uses, so a theme that does not exist yet is one
   :root block away from being right here too.

   The column centres itself the way .login does, and for the same reason: the
   bare layout's <main class="content"> is the full page, and this screen is one
   short stack in the middle of it. The gap is --content-gap (14px), which is
   what the frame draws, so it is inherited rather than restated. */
.offline {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--content-gap);
  width: 100%;
  max-width: 350px; /* the Offline artboard: 390 px phone minus 2 × 20 px padding */
  margin-inline: auto;
  min-height: 80dvh;
  padding-bottom: 40px;
}

/* The two places the mark is drawn as a photograph in a rounded box: /offline at
   the frame's 120 px, and Indstillinger's install sheet at 96. The rounding is on
   the WRAPPER with overflow hidden, exactly as both artboards do it — a radius on
   the <img> alone leaves the corners of the photograph showing through on the
   browsers that round the box but not the raster. line-height 0 is what stops the
   inline image's baseline gap adding a stripe under it. Only the radius differs,
   and each frame states its own below. */
.offline-mark,
.install-mark {
  overflow: hidden;
  line-height: 0;
  box-shadow: var(--shadow);
}

.offline-mark {
  border-radius: 26px;
}

.offline-heading {
  margin-top: 10px;
}

.offline-title {
  font-size: 26px;
}

/* The one sentence, capped where the frame caps it so it breaks into the same
   three lines rather than running the width of a tablet. */
.offline-body {
  margin: 8px 0 0;
  max-width: 280px;
}

/* Taller than the standard .btn and full width: on this screen it is the only
   control there is, and the frame draws it as the whole width of the column. */
.offline-retry {
  min-height: 52px;
  width: 100%;
  margin-top: 16px;
}

.offline-countdown {
  margin: 0;
}

/* ==========================================================================
   PULL TO REFRESH (the `Opdater` sheet; owner, 2026-09-08, approved as drawn on
   2026-09-09). Ported by VALUE from build.py's pull_disc() — every colour there
   was already a token, which is why the disc's own rules come across unchanged.

   ONE CUSTOM PROPERTY DRIVES ALL OF IT, and it is unitless on purpose: --pull is
   a plain number of pixels-worth-of-drag that this block multiplies by 1px for a
   position and a translation, and DIVIDES by --pull-threshold for a rotation. A
   length could not do the third.

   TWO TRAPS, both real and both silent, and both are why the rules read the way
   they do:

   1. The transform is on `main.content` and never on `body` or `:root`. A
      transformed element becomes the containing block for its position: fixed
      descendants, so a transform further up would take .tabs, .sticky and
      .viewer scrolling with the page.
   2. It exists only DURING a drag. .sticky (Faktura's sum bar) and .viewer (the
      full-screen photo) are fixed elements INSIDE main, so while the transform
      is applied they travel with it — which is fine for the half-second the
      finger is down and wrong for the rest of the app's life. `is-pulling` is
      added on the first move and removed when the gesture ends, so on every
      other frame there is no transform at all.

   The disc's own box is OUTSIDE main for the same reason; templates/partials/
   pull.php says so where the markup is.
   ========================================================================== */

/* The clip the disc comes out of. At rest the disc's top is above this edge, so
   it is genuinely hidden rather than merely transparent — the same shape the
   artboard's clipped panel has, and the reason the disc can only appear by being
   pulled out. It never takes a tap: the page underneath owns every pixel. */
.pull-box {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 160px;
  overflow: hidden;
  pointer-events: none;
  z-index: 5;
}

/* PULL_DISC_REST is the canvas's -52: the disc's own 44 plus 8 of air. The
   resistance and the cap are applied HERE and in the page's translation, and
   nowhere else — --pull is raw finger travel wherever it is read. */
.pull {
  position: absolute;
  left: 50%;
  margin-left: -22px;
  top: calc(-52px + min(var(--pull, 0) * var(--pull-resistance), var(--pull-reach)) * 1px);
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--raised);
  border: 1px solid var(--line-strong);
  box-shadow: var(--shadow);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dim);
  opacity: .6;
}

/* Past the threshold the disc takes the accent, and THAT is the feedback: it
   says "let go now" before the finger lifts, with no copy to read. */
.pull.armed {
  color: var(--accent);
  opacity: 1;
}

/* The arrow winds up under the finger — 270 degrees over the whole threshold. */
.pull-arrow {
  display: flex;
  transform: rotate(calc(var(--pull, 0) / var(--pull-threshold) * 270deg));
}

/* .9s, on the sweep keyframes the running ring already owns (60s per turn). A
   second @keyframes for the same rotation would be a second thing to keep. */
.pull-spin {
  display: none;
  animation: sweep .9s linear infinite;
}

.pull.spinning .pull-arrow {
  display: none;
}

.pull.spinning .pull-spin {
  display: block;
}

/* THE ARC STOPS AND THE DISC DOES NOT, which is why this is a rule of its own
   and not another name on the reduced-motion list far above. Two reasons, and
   the second is the one that bites: the declarations differ — the pull disc must
   still come out and go back, or the gesture has no acknowledgement at all, so
   nothing here touches .pull's or .content's transitions — and 12-03 measured
   what one unparseable selector does to a whole list. The `Opdater` sheet's
   sixth rule is exactly this: "skiven kommer, buen drejer ikke".

   AND IT SITS HERE, AFTER THE RULE IT OVERRIDES, WHICH IS THE WHOLE REASON IT IS
   NOT UP THERE WITH THE OTHERS. A media query adds no specificity: `.pull-spin`
   in the block at the top of this file and `.pull-spin` beside it are equal, so
   the LATER one wins — and the later one was `animation: sweep .9s`. Measured in
   Chrome on 2026-09-09 (12-05's browser pass) with the preference emulated:
   matchMedia answered true, the login screen's headings read `animation: none`,
   and this arc read `animation: sweep 0.9s` — it went on turning for exactly the
   people who had asked it not to. Every other selector in that block is defined
   ABOVE it, which is why this was the only one that lost.

   (The lines of this comment deliberately never put another animated selector's
   name beside this rule's: the test scans every line of this file that mentions
   the arc and forbids a second selector on it, and a sentence is not an
   exception — 12-04's precedent, reworded prose rather than a loosened
   assertion.) */
@media (prefers-reduced-motion: reduce) {
  .pull-spin {
    animation: none;
  }
}

/* Fetching: the disc leaves the drag behind and waits at one fixed place. It is
   pinned by a class rather than by a number app.js writes, because 64 is where
   the sheet says it waits — not a distance anybody's finger happened to travel. */
.pull.spinning {
  top: calc(-52px + var(--pull-rest) * 1px);
}

/* `html.` and not `:root.`, which is the same element by a different name: this
   file's token blocks are the `:root …{` lines, and StylesheetTest's colour
   scanner finds them by exactly that prefix — a component rule wearing it would
   be read as a token block. The class is written by app.js onto
   document.documentElement and nowhere else. */
html.is-pulling .content {
  transform: translateY(calc(min(var(--pull, 0) * var(--pull-resistance), var(--pull-reach)) * 1px));
}

/* AFTER the rule above on purpose: same specificity, so source order is what
   decides, and while the answer is being fetched the page waits with the disc. */
html.is-refreshing .content {
  transform: translateY(calc(var(--pull-rest) * 1px));
}

/* The spring back, on both the page and the disc, for the one transition the
   gesture ends with. */
html.is-springing .content,
html.is-springing .pull {
  transition: transform var(--dur) var(--ease), top var(--dur) var(--ease);
}

/* ==========================================================================
   FØJ TIL HJEMMESKÆRM (the `InstallerIphone` and `LysInstallerIphone` frames;
   owner, 2026-09-08 "Yes!", the hide rule 2026-09-09 "Love it"). Every number
   here is off those two frames and every colour is a token — the sheet is
   stroke glyphs on cards, which is why the light frame was drawn at all, and it
   is one :root block away from being right in a theme that does not exist yet.
   ========================================================================== */

.install-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding-top: 6px;
}

/* 22% is the install frame's own figure — iOS's squircle, near enough. The box
   itself is drawn with .offline-mark above. */
.install-mark {
  border-radius: 22%;
}

.install-name {
  font-weight: 600;
}

.install-step {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 56px;
}

.install-num {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 12px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

.install-glyph {
  display: flex;
  color: var(--accent);
  flex: none;
}

.install-text {
  min-width: 0;
}

.install-dismiss {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding-top: 2px;
  text-align: center;
}

/* Pinned above our own tab bar, pointing past it at Safari's toolbar outside the
   viewport. The artboard puts it 8 px above an 88 px bar inside a phone frame;
   here the bar is fixed to a real viewport, so the same 8 px of air is
   `88 + 8` plus whatever the home indicator takes — which is exactly what body's
   own padding-bottom already reckons the bar's footprint to be. */
.install-hint {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(96px + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  color: var(--muted);
  pointer-events: none;
}

.install-arrow {
  display: flex;
}

/* ==========================================================================
   GENDANNET KLADDE (the four `*Gendannet` frames and `restored_note()`; owner,
   2026-09-09). The line above a form whose fields came back from a kept draft.
   Almost all of it is already drawn by the utilities the artboard itself uses —
   `.between`, `.row`, `.dim`, `.small`, `.ellipsis`, `.muted` and `.hair` — so
   what is here is the three things those do not give, and nothing else. Every
   inline style on the artboard had to become a rule because the strict CSP drops
   inline styles and an architecture test greps every template for them.
   ========================================================================== */

.restored-label {
  gap: 8px;
  min-width: 0;
}

.restored-glyph {
  display: flex;
  flex: none;
}

/* The one thing in the line that can be pressed, and the canvas says so in the
   accent — `.fill-note-action` is its nearest neighbour and this is that button
   at a row's height. */
.restored-discard {
  min-height: 44px;      /* the canvas's own number: this is the only escape from
                            a draft, and this project's tap targets are >= 44 px */
  padding: 0 2px;
  flex: none;
  border: 0;
  background: none;
  color: var(--accent);  /* the ONE accent thing in the line — on this canvas an
                            accent word is how something says "you can press me" */
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
