/* Design tokens first, so a colour is changed in one place.

   Light only, deliberately: there is no theme switcher, and a dark palette that
   nothing can select is dead code. Add one here if a toggle is ever built. */
:root {
  /* How far down the page a sticky element has to start to clear a fixed bar
     above it. Zero here, because the public pages have no such bar; the
     signed-in shell sets its own below. */
  --sticky-top: 0px;

  /* The logo's blue. The mark itself is #769FCD on #B9D7EA, which is too pale
     to carry text: white on it measures 2.8:1, and so does it on white, against
     the 4.5:1 a label needs. So the interface takes the logo's *hue* (211°) at
     a step dark enough to be read - 6.5:1 on white - and the logo's own colours
     stay where they are only ever a fill: the tints below, and the bright
     middle of the recommended plan's rule. */
  --brand: #2f6094;
  --brand-dark: #26527f;
  --brand-light: #dbe9f5;
  --brand-tint: #f2f7fc;
  /* The mark's own two colours, for fills that carry no text. */
  --logo-blue: #769fcd;
  --logo-blue-pale: #b9d7ea;

  --ink: #171e28;
  --ink-muted: #6c7a89;
  --ink-faint: #9aa7b4;

  --surface: #ffffff;
  --canvas: #f6f7fb;
  --line: #e6e9ef;
  --line-strong: #d3d9e2;
  /* A rule inside a banded table: the full `--line` between every pair of rows
     draws a grid, which is one visual system too many once the rows are
     striped. */
  --line-soft: #eef1f5;
  /* The band on every second table row. Two percent of the brand rather than a
     grey, so it belongs to the same palette as the header above it - and slight
     enough that the hover state still reads on top of it. */
  --row-band: #f7f9fc;

  --ok: #157347;
  /* The ground behind it, for the one place a success is a shape rather than a
     line of text: the tick on the two screens that only report a result. */
  --ok-bg: #e6f4ec;
  /* Amber text on an amber ground: `--warn` itself only reaches 4.2:1 there. */
  --warn-ink: #92400e;
  --danger: #b42318;
  --danger-bg: #fdf1f0;
  --warn: #b45309;

  --radius: 12px;
  --radius-sm: 8px;
  --gap: 18px;
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

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

body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--canvas);
}

h1 { font-size: 1.6rem; margin: 0 0 4px; }
h2 { font-size: 1.05rem; margin: 0; }
h3 { font-size: 1rem; margin: 0 0 4px; }
a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }
p { margin: 0 0 10px; }

.muted { color: var(--ink-muted); }
.tiny { font-size: 0.8rem; }
.strong { font-weight: 600; }
.nowrap { white-space: nowrap; }
.center { text-align: center; }
.break { word-break: break-all; }
.mono { font-family: var(--mono); font-size: 0.94em; }
.mb-1 { margin-bottom: 8px; }
.mb-2 { margin-bottom: 16px; }
.mb-3 { margin-bottom: 24px; }
.mt-1 { margin-top: 8px; }
.mt-2 { margin-top: 16px; }

/* --- Icons ------------------------------------------------------------------
   overflow: visible is not decoration. Most Bootstrap glyphs are drawn flush to
   the edge of their 0 0 16 16 viewBox, and an <svg> clips at its viewport by
   default - so at a fractional em size the outermost sliver of a circle is cut
   rather than antialiased, and a ring loses its bottom edge. */
.icon {
  width: 1em;
  height: 1em;
  fill: currentColor;
  flex: none;
  vertical-align: -0.125em;
  overflow: visible;
}
/* --- Brand -------------------------------------------------------------------
   The lockup is one image, so it is sized by height and left to work out its own
   width - the three files have different aspect ratios and hard-coding a width
   would squash whichever one is used next. */
.brand-logo { display: block; height: 40px; width: auto; max-width: 100%; }
.sidebar-brand .brand-logo { height: 38px; }
.auth-shell .brand-logo { height: 44px; margin: 0 auto; }
@media (max-width: 720px) { .brand-logo { height: 32px; } }

/* --- Buttons ---------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 9px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 0.89rem;
  font-weight: 550;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.btn:hover { background: var(--brand-tint); text-decoration: none; }
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
/* Both ends of the gradient clear 6.5:1 against the white label, so the sheen
   costs nothing in legibility. */
.btn-primary {
  background: linear-gradient(180deg, var(--brand), var(--brand-dark));
  border-color: var(--brand-dark); color: #fff;
}
.btn-primary:hover { background: var(--brand-dark); border-color: var(--brand-dark); }
.btn-ghost { border-color: transparent; background: transparent; }
.btn-danger { color: var(--danger); border-color: #e8c4c1; }
.btn-danger:hover { background: var(--danger-bg); }
.btn-block { width: 100%; }
.btn-sm { padding: 5px 10px; font-size: 0.82rem; }
.btn-icon {
  padding: 6px; width: 30px; height: 30px;
  color: var(--ink-muted); border-color: transparent; background: transparent;
}
.btn-icon:hover { background: var(--brand-light); color: var(--brand); }
.btn-icon-danger:hover { background: var(--danger-bg); color: var(--danger); }
.row-actions { display: inline-flex; gap: 2px; align-items: center; }

/* --- Layout ----------------------------------------------------------------- */
/* The shell's top bar is sticky, so everything sticky inside the shell starts
   below it. Without this the builder's preview pane slid up behind the bar and
   the code - the one thing on that page worth looking at - was the part that
   went under it. */
/* The sidebar's width and the bar's height as tokens, so the two numbers the
   shell is built from are named rather than repeated. */
:root { --sidebar-width: 232px; --topbar-height: 63px; }
.shell {
  --sticky-top: var(--topbar-height);
  display: grid; grid-template-columns: var(--sidebar-width) minmax(0, 1fr); min-height: 100vh;
}
.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
}
.sidebar-brand { padding: 16px 18px; border-bottom: 1px solid var(--line); }
.sidebar-brand a { display: block; }
/* One list now - the account menu in the bar holds plan, billing and profile -
   so the nav starts at the top rather than under a heading that had nothing
   left to distinguish it from. */
.sidebar-nav { padding: 12px; flex: 1; }
.nav-group {
  font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-faint); padding: 14px 10px 6px;
}
.nav-link {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px; border-radius: var(--radius-sm);
  color: var(--ink); font-size: 0.92rem;
}
.nav-link .icon { flex: 0 0 16px; opacity: 0.9; }
.nav-link:hover { background: var(--brand-tint); text-decoration: none; }
.nav-link.is-active { background: var(--brand-light); color: var(--brand); font-weight: 600; }
.plan-chip {
  display: block; padding: 9px 11px; border-radius: var(--radius-sm);
  background: linear-gradient(135deg, var(--brand-light), var(--brand-tint));
  border: 1px solid rgb(47 96 148 / 0.14);
  margin-bottom: 8px;
}
.plan-chip strong { color: var(--brand); }
.plan-chip .icon { margin-right: 6px; opacity: 0.6; }

.main { display: flex; flex-direction: column; min-width: 0; }
/* Sticky, because it now carries the plan and the way out. Above the date
   picker's popup, which is the only other thing on a page that floats -
   messages are in the flow of the page and float over nothing.

   Anything else on the page that sticks has to start below it - the builder's
   preview is the one that does - so the bar's height is a variable rather than
   a number buried in two places. It is measured, not guessed: 12px of padding
   top and bottom around a 39px row. Change the padding here and change it
   there. */
.topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 12px 24px;
  background: var(--surface); border-bottom: 1px solid var(--line);
}
.topbar-title { font-weight: 600; }
.topbar-actions { display: flex; align-items: center; gap: 10px; }

/* --- The account menu --------------------------------------------------------
   Who, which tier, and the way out - folded into one control, because a bar
   carrying all three side by side is three things to read before the page. */
.account { position: relative; }
.account > summary {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 9px 5px 5px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); cursor: pointer; list-style: none;
  transition: border-color 0.12s ease, background 0.12s ease;
}
/* Safari and Chrome each draw their own marker on a <summary>; both are the
   wrong triangle in the wrong place next to a caret of our own. */
.account > summary::-webkit-details-marker { display: none; }
.account > summary::marker { content: ""; }
.account > summary:hover { background: var(--brand-tint); border-color: var(--brand-light); }
.account[open] > summary { background: var(--brand-tint); border-color: rgb(47 96 148 / 0.35); }
.account > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The initial, which is identity enough at this size - an address in a bar is
   read once and then only ever takes up room. It is in the panel below. */
.account-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%;
  background: linear-gradient(135deg, var(--brand), var(--brand-dark));
  color: #fff; font-size: 0.78rem; font-weight: 650;
}
.account-plan { font-size: 0.82rem; font-weight: 600; color: var(--ink); }
.account-caret { width: 11px; height: 11px; color: var(--ink-faint); }
.account[open] .account-caret { transform: rotate(180deg); }

.account-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 40;
  min-width: 244px; padding: 6px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius); box-shadow: 0 12px 28px rgb(16 24 40 / 0.16);
}
.account-who {
  display: grid; gap: 1px; padding: 9px 10px 11px;
  border-bottom: 1px solid var(--line); margin-bottom: 6px;
}
.account-who strong { font-size: 0.9rem; }
.account-who .tiny { overflow: hidden; text-overflow: ellipsis; }
.account-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 10px; border: none; border-radius: var(--radius-sm);
  background: none; color: var(--ink); font: inherit; font-size: 0.88rem;
  text-align: left; cursor: pointer;
}
.account-item:hover { background: var(--brand-tint); text-decoration: none; }
/* Where you are. The sidebar used to say it; these three pages left it when
   they moved into this menu, and a menu that opens on the page you are already
   looking at should admit as much. */
.account-item.is-active { background: var(--brand-light); color: var(--brand); font-weight: 600; }
.account-item.is-active .icon { color: var(--brand); }
.account-item .icon { color: var(--ink-faint); flex: none; }
.account-item:hover .icon { color: var(--brand); }
/* The tier sits at the end of its own row, so the row reads as a fact with a
   link on it rather than as a menu entry that happens to have a word after. */
.account-value { margin-left: auto; color: var(--brand); }
.account-signout { border-top: 1px solid var(--line); border-radius: 0 0 var(--radius-sm) var(--radius-sm); margin-top: 6px; padding-top: 11px; }
.account-signout:hover { background: var(--danger-bg); color: var(--danger); }
.account-signout:hover .icon { color: var(--danger); }

/* On a phone the tier drops off the button; the initial and the caret are
   enough to say what it opens, and the panel still names it. */
@media (max-width: 720px) { .account-plan { display: none; } }
/* The signed-in content column is capped, not stretched. Copied from the
   invoices project, including the two widths and the left alignment: a table or
   a four-card pricing row pulled across a 2560px monitor puts the row label and
   its number so far apart that reading one off the other needs a ruler.

   Which cap a page gets is chosen by `content_class` in core/base_app.html -
   wide for anything with a table or a grid, narrow for a single form. */
.content { padding: 24px; min-width: 0; }
.content-wide { max-width: 1200px; }
.content-narrow { max-width: 780px; }

.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.page-head h1 { display: flex; align-items: center; gap: 10px; }
.page-head h1 .icon { color: var(--ink-faint); }
.page-head .sub { color: var(--ink-muted); font-size: 0.9rem; }
/* Which subset of the codes is on screen, and the way back to all of them. */
.filter-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px; border-radius: 999px;
  background: var(--brand-tint); border: 1px solid rgb(47 96 148 / 0.16);
  color: var(--brand); font-size: 0.82rem; font-weight: 550;
}
.filter-clear { margin-left: 10px; font-size: 0.82rem; }
.page-actions { display: flex; gap: 8px; }

/* --- Cards and grids -------------------------------------------------------- */
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
.card + .card { margin-top: var(--gap); }
/* Capped, but not centred: inside the shell a narrow card floated in the middle
   of the column with its own page heading still hard against the left, which
   reads as two pages. The sign-in and sign-up cards are unaffected - they sit
   in `.auth-shell`, which is what centres them there. */
.card.narrow { max-width: 460px; }
/* The head takes a wash of the brand, fading out before the border. Shallow on
   purpose: it is there to separate the head from the body without a second
   line, not to be a coloured bar. */
.card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 15px 18px; border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--brand-tint), var(--surface));
  border-radius: var(--radius) var(--radius) 0 0;
}
.card-body { padding: 18px; }
.card-body.tight { padding: 0; }

.grid { display: grid; gap: var(--gap); }
/* minmax(0, 1fr) rather than an implicit `auto` column: an auto column will not
   shrink below its content's minimum, so a card holding a wide table grows past
   the page instead of letting .table-wrap scroll inside it. */
.grid-1 { grid-template-columns: minmax(0, 1fr); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid > .card + .card { margin-top: 0; }

/* --- Stats ------------------------------------------------------------------ */
/* A tile carries one number, so everything else in it is kept quiet: a tinted
   chip for the icon, a wash that fades out before it reaches the figure, and
   the QR checkerboard from the plan cards in the corner. The texture is the one
   piece of decoration, at the same weight it has there - it should register as
   grain, not as a pattern anybody tries to read. */
.stat {
  position: relative; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 16px 18px; box-shadow: var(--shadow);
  background: linear-gradient(180deg, var(--brand-tint), var(--surface) 62%);
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
/* The tiles are links to the rows they counted, so they inherit the page's ink
   rather than the link colour - the number is the subject and a card of blue
   figures reads as a menu. The lift on hover was already here and is now doing
   a second job: saying the tile can be followed. */
.stat { display: block; color: inherit; text-decoration: none; }
/* No underline on hover either: `a:hover` underlines every link on the page,
   which on a card means a line under the label and another under the note. The
   lift and the border are the hover state here. */
.stat:hover { text-decoration: none; }
.stat:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgb(16 24 40 / 0.08); border-color: var(--brand); }
.stat:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.stat::after {
  content: ""; position: absolute; inset: 0 0 auto auto; width: 132px; height: 132px;
  background-image: repeating-conic-gradient(rgb(47 96 148 / 0.08) 0% 25%, transparent 0% 50%);
  background-size: 9px 9px;
  /* Faded from the corner outwards, so the pattern has reached nothing by the
     time it meets an edge - a linear fade leaves it looking like a pasted
     rectangle. Same mask as `.plan-card .card-head::after`. */
  -webkit-mask-image: radial-gradient(96px 96px at 100% 0, #000 6%, transparent 66%);
  mask-image: radial-gradient(96px 96px at 100% 0, #000 6%, transparent 66%);
  pointer-events: none;
}
/* Everything real sits over the texture. */
.stat > * { position: relative; z-index: 1; }

.stat-label {
  display: flex; align-items: center; gap: 9px;
  font-size: 0.7rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-faint);
}
.stat-icon {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 28px; height: 28px; border-radius: 9px;
  background: var(--brand-light); color: var(--brand);
}
.stat-icon .icon { width: 15px; height: 15px; opacity: 1; }
.stat-value { font-size: 1.8rem; font-weight: 650; margin: 6px 0 2px; }
.stat-value .of-limit { font-size: 1rem; color: var(--ink-faint); font-weight: 500; }
.stat-note { font-size: 0.8rem; color: var(--ink-muted); }

/* The track is a lighter step of the fill's own colour rather than a grey, so
   the whole bar reads as one scale in whichever state it is in. */
/* The quota meter. Thicker than it was, inset so the empty part reads as a
   channel rather than as a pale line, and the fill carries a gradient along its
   own length - which is what makes a bar look like a level rather than a rule.
   The rounded cap on the fill means a tiny value still draws as a mark instead
   of a sliver, so `min-width` keeps it from becoming a dot. */
.meter {
  height: 9px; border-radius: 999px; margin: 10px 0 8px;
  background: var(--brand-light); overflow: hidden;
  box-shadow: inset 0 1px 2px rgb(16 24 40 / 0.12);
}
.meter span {
  display: block; height: 100%; min-width: 9px; border-radius: 999px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--brand) 72%, white), var(--brand));
  box-shadow: 0 1px 2px rgb(16 24 40 / 0.18);
  transition: width 0.35s ease;
}

/* Nearly out, and out. Only this tile ever takes them, and it takes the chip
   and the meter together - a warning in one of the two reads as a mistake. */
.stat.is-high { border-color: rgb(180 83 9 / 0.35); background: linear-gradient(180deg, rgb(180 83 9 / 0.07), var(--surface) 62%); }
.stat.is-high .stat-icon { background: rgb(180 83 9 / 0.13); color: var(--warn); }
.stat.is-high .meter { background: rgb(180 83 9 / 0.15); }
.stat.is-high .meter span {
  background: linear-gradient(90deg, color-mix(in srgb, var(--warn) 70%, white), var(--warn));
}
.stat.is-high::after { background-image: repeating-conic-gradient(rgb(180 83 9 / 0.08) 0% 25%, transparent 0% 50%); }

.stat.is-full { border-color: rgb(180 35 24 / 0.4); background: linear-gradient(180deg, var(--danger-bg), var(--surface) 62%); }
.stat.is-full .stat-icon { background: rgb(180 35 24 / 0.13); color: var(--danger); }
.stat.is-full .stat-value { color: var(--danger); }
.stat.is-full .meter { background: rgb(180 35 24 / 0.15); }
.stat.is-full .meter span {
  background: linear-gradient(90deg, color-mix(in srgb, var(--danger) 70%, white), var(--danger));
}
.stat.is-full::after { background-image: repeating-conic-gradient(rgb(180 35 24 / 0.08) 0% 25%, transparent 0% 50%); }
/* A shape or a look the plan cannot draw. Shown rather than removed - a picker
   with one tile in it says nothing about what the other tiers are for - and the
   padlock is what separates "not available" from "broken". */
.shape-card.is-locked, .template-card.is-locked { cursor: not-allowed; }
.shape-card.is-locked .shape-sample, .template-card.is-locked .swatch { opacity: 0.45; }
.shape-card.is-locked .shape-name, .template-card.is-locked .template-name { color: var(--ink-faint); }
.shape-card.is-locked:hover .shape-sample, .template-card.is-locked:hover .swatch { border-color: var(--line); }
.shape-sample, .template-card .swatch { position: relative; }
.lock {
  position: absolute; right: -5px; bottom: -5px;
  display: inline-flex; align-items: center; justify-content: center;
  width: 19px; height: 19px; border-radius: 50%;
  background: var(--surface); color: var(--ink-muted);
  box-shadow: 0 1px 3px rgb(16 24 40 / 0.22);
}
.lock .icon { width: 10px; height: 10px; }

/* The plan chip, in a header rather than in the sidebar. */
.plan-chip-inline {
  display: inline-flex; align-items: center; gap: 7px; margin-bottom: 0;
  padding: 7px 11px; color: var(--ink); white-space: nowrap;
}
.plan-chip-inline:hover { text-decoration: none; border-color: rgb(47 96 148 / 0.35); }
.plan-chip-inline .icon { color: var(--brand); }
.plan-chip-inline strong { color: var(--brand); }
.public-actions form { display: inline-flex; }

/* --- Date picker -------------------------------------------------------------
   The popup for `core/js/datepicker.js`. Anchored to the field rather than the
   page, so it follows the input inside a scrolling step; `z-index` clears the
   fields under it and nothing else. */
.datepicker-wrap { position: relative; }
/* The calendar button, inside the field's own box. `pointer-events` stay on it
   alone - the input keeps the rest of the width, so clicking the text still
   puts the caret where it was clicked. */
.datepicker-wrap .control { padding-right: 38px; }
.datepicker-toggle {
  position: absolute; top: 50%; right: 6px; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;
  border: none; border-radius: var(--radius-sm);
  background: transparent; color: var(--ink-faint); cursor: pointer;
}
.datepicker-toggle:hover { background: var(--brand-light); color: var(--brand); }
.datepicker-toggle .icon { width: 16px; height: 16px; }
.datepicker {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 20;
  width: 268px; padding: 12px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius); box-shadow: 0 10px 26px rgb(16 24 40 / 0.14);
}
.datepicker-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 8px; font-size: 0.9rem;
}
.datepicker-head strong { text-transform: capitalize; }
/* Two steps each side: a month, and a year. The year arrows are the double
   chevrons, which is the one pairing people already read as "further". */
.datepicker-navs { display: inline-flex; gap: 2px; }
.datepicker-nav {
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-muted);
  width: 26px; height: 26px; border-radius: var(--radius-sm);
  font: inherit; line-height: 1; cursor: pointer;
}
.datepicker-nav .icon { width: 13px; height: 13px; }
.datepicker-nav:hover { background: var(--brand-tint); color: var(--brand); border-color: var(--brand-light); }

/* Seven columns, so the weekday row and the days share one grid and a month
   that starts on a Thursday cannot drift out of line with its own header. */
.datepicker-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.datepicker-weekday {
  text-align: center; padding: 4px 0; font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.02em; text-transform: uppercase; color: var(--ink-faint);
}
.datepicker-day {
  padding: 6px 0; border: none; border-radius: var(--radius-sm);
  background: transparent; color: var(--ink); font: inherit; font-size: 0.82rem;
  cursor: pointer; font-variant-numeric: tabular-nums;
}
.datepicker-day:hover { background: var(--brand-tint); }
/* Today is outlined, the chosen day is filled: one is a reference point and the
   other is an answer, and they have to be told apart when they are the same day. */
.datepicker-day.is-today { box-shadow: inset 0 0 0 1px var(--line-strong); }
.datepicker-day.is-chosen { background: var(--brand); color: #fff; font-weight: 600; }

.datepicker-time {
  display: flex; align-items: center; gap: 6px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line);
}
.datepicker-time select {
  flex: 1; padding: 6px 8px; font: inherit; font-size: 0.85rem;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
}
.datepicker-colon { color: var(--ink-faint); }
.datepicker-time > .icon { color: var(--ink-faint); flex: none; }
/* One button, and it confirms. Emptying the field is what the keyboard is for
   - a Clear beside a Done is a second decision on a row that has one. */
.datepicker-done {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 32px; height: 32px; padding: 0;
}
.datepicker-done .icon { width: 15px; height: 15px; }
.datepicker-time select { min-width: 0; }

/* --- Charts ------------------------------------------------------------------
   Two shapes: a line over twelve months, and a ranked bar per row. All of it is
   HTML apart from the line itself, which is one stretched <svg>. The
   percentages come from `core.services.dashboard`; everything about how big the
   result is lives here.

   One colour throughout. Both charts plot a single thing - a count - so a
   second hue would be decoration, and shading each bar by its own size would
   encode the length twice while saying nothing the length does not. Marks stay
   thin and the rules stay a step off the surface, so the data is the only part
   of the picture with any weight. */
.chart { --plot: 150px; --gutter: 30px; position: relative; padding-left: var(--gutter); }

/* The plot: the scale, and the line drawn over it. Sized and placed once here,
   so the SVG and the markers in the columns below share one coordinate space -
   both are positioned in percentages of this box. */
.chart-plot { position: absolute; left: var(--gutter); right: 0; top: 0; height: var(--plot); }
/* Each rule is a zero-height box pinned by its `bottom`, so the border lands
   exactly on the value it stands for; the figure hangs in the gutter beside it. */
.chart-rule { position: absolute; left: 0; right: 0; height: 0; border-top: 1px solid var(--line); }
.chart-rule.is-baseline { bottom: 0; border-top-color: var(--line-strong); }
.chart-rule b {
  position: absolute; right: calc(100% + 8px); top: -0.75em;
  font-size: 0.68rem; font-weight: 500; color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* The line itself, and the wash under it. The wash is a tenth-opacity fill
   rather than a paler colour of its own, so it stays the line's own hue however
   the brand colour is changed. */
.chart-line { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
/* Four series on one plot. The colours are set once here and read by both the
   line and its dot through `data-series`, so the key, the strokes and the
   markers cannot drift apart - and the template names no colour at all.

   Deliberately not four shades of the brand: these are four different
   quantities, not four sizes of one, and a reader has to tell them apart at a
   glance in a 200px-tall plot. Deleted is the only one that is red, because it
   is the only one that is a loss. */
.chart {
  --series-generated: var(--brand);
  --series-saved: #157347;
  --series-deleted: var(--danger);
  --series-downloaded: #7c5cbf;
}

.chart-stroke {
  fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round;
}
.chart-stroke[data-series="generated"]  { stroke: var(--series-generated); }
.chart-stroke[data-series="saved"]      { stroke: var(--series-saved); }
.chart-stroke[data-series="deleted"]    { stroke: var(--series-deleted); }
.chart-stroke[data-series="downloaded"] { stroke: var(--series-downloaded); }

/* Kept as a rule rather than deleted with the markup: "saved" is a subset of
   "generated" and the two lines sit on top of one another whenever nothing has
   been deleted. Dashed, the one underneath still reads. */
.chart-stroke[data-series="saved"] { stroke-dasharray: 5 3; }

/* --- The key ----------------------------------------------------------------
   Above the plot: four lines need naming before they are read, not after. */
.chart-key {
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  list-style: none; margin: 0 0 14px; padding: 0;
  font-size: 0.8rem; color: var(--ink-muted);
}
.chart-key-item { display: inline-flex; align-items: center; gap: 7px; }
.chart-key-item b { color: var(--ink); font-variant-numeric: tabular-nums; }
.chart-key-mark { width: 14px; height: 3px; border-radius: 2px; flex: none; }
.chart-key-item[data-series="generated"] .chart-key-mark  { background: var(--series-generated); }
.chart-key-item[data-series="saved"] .chart-key-mark      { background: var(--series-saved); }
.chart-key-item[data-series="deleted"] .chart-key-mark    { background: var(--series-deleted); }
.chart-key-item[data-series="downloaded"] .chart-key-mark { background: var(--series-downloaded); }
/* The key sits outside `.chart`, so it carries the palette itself. */
.chart-key {
  --series-generated: var(--brand);
  --series-saved: #157347;
  --series-deleted: var(--danger);
  --series-downloaded: #7c5cbf;
}

/* No gap between the columns: they are hover targets sitting under the line,
   and a gap would shift each column's centre away from the point the line was
   drawn through. */
.chart-cols { display: flex; align-items: flex-end; list-style: none; margin: 0; padding: 0; }
.chart-col { flex: 1; min-width: 0; text-align: center; }
/* `display: block` is load-bearing: the slot is a <span>, and height does
   nothing to an inline box - left inline it collapses, the months ride up over
   the plot and the line spills out of the card. */
.chart-slot { display: block; position: relative; height: var(--plot); }
/* The hit target is the whole month, not the marker - a marker is eight pixels
   across and eight pixels is not something to ask anyone to aim at. The tint
   makes the target visible once the pointer is inside it. */
.chart-col:hover .chart-slot { background: var(--brand-tint); }
.chart-col:hover .chart-dot { transform: translate(-50%, 50%) scale(1.35); }
/* Pinned by its `bottom` in the same percentages the line was drawn from, then
   pulled back by half its own size so the point - not its corner - lands on the
   value. The ring is the surface colour: it keeps the marker legible where the
   line runs under it, and is what stops twelve dots reading as beads on a
   string. */
/* Smaller than they were: four markers can land on the same column, and eight
   pixels each turned a busy month into one blob. The ring in the surface colour
   is what keeps two that overlap readable as two. */
.chart-dot {
  position: absolute; left: 50%; width: 7px; height: 7px;
  transform: translate(-50%, 50%);
  border-radius: 50%; box-shadow: 0 0 0 2px var(--surface);
  transition: transform 0.12s ease;
}
.chart-dot[data-series="generated"]  { background: var(--series-generated); }
.chart-dot[data-series="saved"]      { background: var(--series-saved); }
.chart-dot[data-series="deleted"]    { background: var(--series-deleted); }
.chart-dot[data-series="downloaded"] { background: var(--series-downloaded); }
.chart-tick {
  padding-top: 6px; font-size: 0.68rem; color: var(--ink-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* A ranked list: label, bar, count. The bar is scaled against the largest row,
   so the longest one always fills its track and the rest are read against it. */
.rank { list-style: none; margin: 0; padding: 0; }
.rank-row {
  display: grid; grid-template-columns: minmax(90px, 8rem) minmax(40px, 1fr) auto;
  align-items: center; gap: 12px; padding: 7px 0;
}
.rank-row + .rank-row { border-top: 1px solid var(--line); }
.rank-label {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  font-size: 0.88rem; color: var(--ink-muted);
}
.rank-label .icon { flex: none; opacity: 0.55; }
.rank-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A lighter step of the same colour as the bar, so an empty track still reads
   as part of the same scale rather than as a separate grey object. */
/* The same treatment on the ranked bars, so a bar is a bar wherever it appears:
   an inset channel, a fill lit along its length, and a cap that keeps the
   smallest row visible. */
.rank-track {
  display: block; height: 10px; border-radius: 999px;
  background: var(--brand-light); overflow: hidden;
  box-shadow: inset 0 1px 2px rgb(16 24 40 / 0.10);
}
.rank-bar {
  display: block; height: 100%; min-width: 10px; border-radius: 999px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--brand) 66%, white), var(--brand));
  box-shadow: 0 1px 2px rgb(16 24 40 / 0.16);
}
/* The longest row is the scale everything else is read against, so it gets the
   full colour - the rows under it are the same ramp, only shorter. */
.rank-row:first-child .rank-bar {
  background: linear-gradient(90deg, var(--brand), color-mix(in srgb, var(--brand) 80%, black));
}
.rank-value { font-size: 0.88rem; font-variant-numeric: tabular-nums; }

/* --- Tables ----------------------------------------------------------------- */
.table-wrap { overflow-x: auto; }
table.data { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
table.data th .icon { opacity: 0.5; margin-right: 6px; vertical-align: -0.15em; }
table.data th {
  text-align: left; font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--brand); padding: 11px 14px; border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--brand-light), var(--brand-tint));
}
table.data td { padding: 12px 14px; border-bottom: 1px solid var(--line-soft); }
/* Banded, so the eye can carry a value across a wide row without losing the
   line it is on. The band is very slight on purpose - a stripe strong enough to
   read as a colour turns the table into a pattern, and the hover state has to
   stay visible on top of it. */
table.data tbody tr:nth-child(even) { background: var(--row-band); }
/* A row lights up under the pointer, which is also what tells anyone the row is
   a thing rather than five separate cells. Stronger than the band, and it wins
   on both kinds of row. */
table.data tbody tr { transition: background 0.12s ease; }
table.data tbody tr:hover,
table.data tbody tr:nth-child(even):hover { background: var(--brand-tint); }
table.data tbody tr:last-child td { border-bottom: none; }
table.data .shrink { width: 1%; white-space: nowrap; }

/* --- What a cell is, said in the type ----------------------------------------
   Every column used to be the same 0.9rem grey, so a table read as a wall and
   the one value somebody came for had to be hunted. Three weights now:

   the subject   - the name of the thing, the only link in the row, in ink
   the figures   - counts and money, tabular and heavier, so a column of them
                   lines up digit under digit and reads as a column
   the rest      - type, style, dates: context, in muted ink, and dates a size
                   down because they are the same shape in every row anyway
*/
table.data td .strong,
table.data td a.strong { font-weight: 650; color: var(--ink); }
table.data tbody tr:hover td a.strong { color: var(--brand); }
table.data .num {
  text-align: right; font-variant-numeric: tabular-nums;
  font-weight: 600; color: var(--ink);
}
/* A zero is not news. It stays in the column but steps back, so the rows that
   do have a number are the ones the eye lands on. */
table.data .num.is-zero { font-weight: 500; color: var(--ink-faint); }
table.data td.nowrap { font-variant-numeric: tabular-nums; font-size: 0.85rem; }

/* An empty state is the first thing a new account sees, so it gets a ground of
   its own rather than being a grey glyph on white. The icon sits in the same
   tinted chip the stat tiles use. */
.empty {
  text-align: center; padding: 44px 20px;
  background: radial-gradient(120% 90% at 50% 0, var(--brand-tint), transparent 70%);
}
.empty-icon {
  box-sizing: content-box; width: 30px; height: 30px; padding: 13px; margin-bottom: 12px;
  border-radius: 50%; background: var(--brand-light); color: var(--brand);
}

/* --- Forms ------------------------------------------------------------------ */
.field { margin-bottom: 14px; }
/* The field's own label, and only that one. Left as `.field label` this reached
   every label *inside* a control as well - the flag picker is built from radios
   with a label each, and each of them was picking up the 5px bottom margin
   meant to separate a caption from its input. That margin sat inside the
   picker's border, under the row and not over it, which is what left the
   control looking bottom-heavy. Both `.field` markups put the caption directly
   inside the wrapper, so the child combinator costs nothing. */
.field > label {
  display: flex; align-items: center; gap: 6px;
  font-size: 0.82rem; font-weight: 600; color: var(--ink-muted); margin-bottom: 5px;
}
.field .req { color: var(--danger); margin-left: -2px; }
.field-icon { opacity: 0.55; }
.control, .field input, .field select, .field textarea {
  width: 100%; padding: 9px 11px; font: inherit; font-size: 0.92rem;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
}
/* --- Colour controls ---------------------------------------------------------
   A swatch, the hex beside it, and the palette under both. The native input is
   still the control - it is the only picker that needs no script - but a
   full-width bar of colour reads as a banner rather than as a value, and gives
   no way to type a brand colour in. */
.colour-control {
  display: flex; align-items: stretch; gap: 0;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); overflow: hidden;
}
.colour-control:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-light); }
/* `appearance: none` is what removes the browser's own frame; without it the
   swatch keeps a chrome border and inset that no padding here can reach. */
.field .colour-swatch {
  appearance: none; -webkit-appearance: none;
  width: 46px; height: 38px; flex: none; padding: 0;
  border: none; border-right: 1px solid var(--line); border-radius: 0;
  background: none; cursor: pointer;
}
.colour-swatch::-webkit-color-swatch-wrapper { padding: 5px; }
.colour-swatch::-webkit-color-swatch { border: 1px solid rgb(16 24 40 / 0.18); border-radius: 5px; }
.colour-swatch::-moz-color-swatch { border: 1px solid rgb(16 24 40 / 0.18); border-radius: 5px; }
.field .colour-hex {
  flex: 1; min-width: 0; width: auto; height: 38px; padding: 0 10px;
  border: none; border-radius: 0; background: none;
  font: inherit; font-family: var(--mono); font-size: 0.84rem; color: var(--ink);
  text-transform: lowercase;
}
.field .colour-hex:focus { outline: none; box-shadow: none; }
.field .colour-hex.is-invalid { color: var(--danger); }
.field .colour-swatch[disabled], .field .colour-hex[disabled] { cursor: not-allowed; opacity: 0.55; }

/* The quick picks. Small, round and quiet: they are shortcuts, and a row of
   large blocks would outweigh the value they set. */
.colour-dots { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
.colour-dot {
  width: 17px; height: 17px; padding: 0; border-radius: 50%; cursor: pointer;
  border: 1px solid rgb(16 24 40 / 0.18);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.colour-dot:hover { transform: scale(1.18); box-shadow: 0 1px 4px rgb(16 24 40 / 0.25); }
.colour-dot:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* A checkbox stretched to the width of the column is a horizontal rule with a
   tick at one end. */
.field input[type="checkbox"] { width: auto; height: 16px; accent-color: var(--brand); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-light);
}
.field input[disabled], .field select[disabled] { background: var(--canvas); cursor: not-allowed; }
.field-help { font-size: 0.78rem; color: var(--ink-muted); margin: 4px 0 0; }
/* One line shared by a pair of fields - see `parameter_groups`. It belongs to
   the row above it, so it sits close under the pair and keeps the full gap
   before whatever comes next. Written after `.field-help` on purpose: the two
   have the same weight, and this one has to win. */
.field-row.is-tight .field { margin-bottom: 5px; }
.field-help.field-help-row { margin: 0 0 14px; }
.field-error {
  display: flex; align-items: center; gap: 5px;
  font-size: 0.78rem; color: var(--danger); margin: 4px 0 0;
}
.field.has-error .control { border-color: var(--danger); }
.field-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.form-actions { display: flex; gap: 8px; align-items: center; margin-top: 16px; }

.alert {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 12px 14px; border-radius: var(--radius-sm); margin-top: 16px;
}
.alert-error { background: var(--danger-bg); color: var(--danger); }
/* Amber text on amber: `--warn` itself only reaches 4.2:1 there, which is why
   `--warn-ink` exists and is used here rather than the border colour. */
.alert-warning { background: #fef6e7; color: var(--warn-ink); }
.alert-warning .icon { color: var(--warn); }
/* Neither a failure nor a warning: a statement of where something stands. */
.alert-info { background: var(--brand-tint); color: var(--ink); border: 1px solid var(--brand-light); }
.alert-info .icon { color: var(--brand); }
.alert .icon { margin-top: 0.15em; }

/* --- Public builder --------------------------------------------------------- */
/* Wraps rather than overflows. On a phone the mark, three flags and two buttons
   come to about 460px against a 430px screen - and a header that does not wrap
   does not get narrower, it takes the whole page sideways with it. */
.public-head {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: 12px 16px; padding: 14px 28px; background: var(--surface); border-bottom: 1px solid var(--line);
}
.public-brand { display: inline-flex; align-items: center; gap: 9px; font-weight: 700; color: var(--ink); }
.public-actions { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
@media (max-width: 560px) {
  .public-head { padding: 12px 16px; }
  /* The two calls to action stay; the second one loses its words rather than
     the row losing the button. */
  .public-actions .btn { padding: 8px 11px; }
}

.hero { text-align: center; padding: 40px 20px 24px; }
.hero h1 { font-size: 2.1rem; }
.hero-sub { color: var(--ink-muted); max-width: 620px; margin: 0 auto; }

.builder { max-width: 1120px; margin: 0 auto 60px; padding: 0 24px; }
/* The public page has no shell, so the form carries its own gutter, width cap
   and bottom margin. Inside the signed-in shell `.content` already provides all
   three, and the second gutter was what set the builder's controls a step in
   from the page heading above them - the heading is not inside the form. */
.content .builder { max-width: none; margin: 0; padding: 0; }
.builder-panes { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(300px, 0.6fr); gap: var(--gap); align-items: start; }
/* `minmax(0, 1fr)`, not the implicit `auto`: an auto track is sized to its
   widest content, so the moment something inside scrolls sideways - the looks
   picker - the whole column grows to that content's width and takes the page
   with it. The same reason the dashboard grids are written this way. */
.builder-controls { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--gap); }

/* --- Folding steps -----------------------------------------------------------
   Steps 2-5 are <details>. The first step is not: it is the one thing that has
   to be filled in, and a required field behind a fold is a form people submit
   empty.

   A <summary> carries the heading and, once JavaScript has run, a line of what
   is currently chosen - so the fold can be read without being opened, which is
   the only thing that makes folding better than scrolling. */
.step-fold > summary {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; cursor: pointer; list-style: none;
}
.step-fold > summary::-webkit-details-marker { display: none; }
.step-fold > summary h2 { margin-bottom: 0; }
.step-fold[open] > summary { margin-bottom: 12px; }
.step-fold > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 4px; }
.step-fold > summary:hover .step-choice { color: var(--brand); }

/* The chevron, drawn rather than an icon so it can rotate on open. */
.step-fold > summary::after {
  content: ""; flex: none;
  width: 8px; height: 8px; margin-left: 2px;
  border-right: 2px solid var(--ink-faint); border-bottom: 2px solid var(--ink-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.15s ease;
}
.step-fold[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }

.step-choice {
  margin-left: auto;
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 0.8rem; color: var(--ink-muted);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis; max-width: 45%;
}
.step-choice .swatch-dot {
  width: 13px; height: 13px; flex: none;
  border-radius: 3px; border: 1px solid var(--line-strong);
}
.step-fold[open] .step-choice { display: none; }

.step { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px; box-shadow: var(--shadow); }
.step h2 { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.step-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--brand); color: #fff; font-size: 0.8rem; font-weight: 700;
}

/* The content type picker. Eighteen tiles, so they are small and the glyph does
   the work - the label under it confirms the choice rather than making it. */
.type-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(82px, 1fr));
  gap: 8px; margin: 10px 0 18px;
}
.type-card {
  position: relative; cursor: pointer; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 11px 5px; border: 1px solid var(--line);
  border-radius: var(--radius-sm); background: var(--surface);
}
.type-card input { position: absolute; opacity: 0; pointer-events: none; }
.type-card:hover { border-color: var(--brand); background: var(--brand-tint); }
.type-card .type-mark { font-size: 1.3rem; line-height: 1; color: var(--ink-muted); }
.type-card .type-name { font-size: 0.73rem; line-height: 1.2; color: var(--ink-muted); }
.type-card.is-selected { border-color: var(--brand); background: var(--brand-light); }
.type-card.is-selected .type-mark,
.type-card.is-selected .type-name { color: var(--brand); font-weight: 600; }
/* Focus lands on the radio, which is invisible, so the ring is drawn on the
   card around it - otherwise the picker cannot be used from the keyboard. */
.type-card:focus-within { outline: 2px solid var(--brand); outline-offset: 2px; }

/* One type's extra fields. Hidden unless that type is selected; see the
   <noscript> rule in builder.html, which shows every group when there is no
   JavaScript to switch between them. */
.param-group { display: none; border-left: 2px solid var(--line); padding-left: 12px; margin-bottom: 14px; }
.param-group.is-active { display: block; }
.param-group-head {
  display: flex; align-items: center; gap: 7px; margin-bottom: 9px;
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--ink-faint);
}

/* --- Module shape picker -----------------------------------------------------
   Nine little modules per sample, drawn in CSS. The shape of the sample is the
   shape the code will use, which is the only honest way to offer "gapped". */
.picker-label {
  display: flex; align-items: center; gap: 6px;
  font-size: 0.82rem; font-weight: 600; color: var(--ink-muted); margin-bottom: 5px;
}
.picker-label .req { color: var(--danger); }
/* One row that scrolls, rather than a row that wraps. There are seven corner
   shapes and seven centres now, and in a narrow column the last of them dropped
   onto a second line - which reads as a second group of choices rather than as
   the same one continued. `grid-auto-flow: column` fixes the count of rows at
   one and lets the length be the free dimension, as in the look and frame
   pickers below; the scrollbar only appears when they do not fit. */
.shape-grid {
  display: grid; gap: 10px; margin-bottom: 14px;
  /* Without this the grid takes its content's width rather than its box's, and
     there is nothing left for `overflow-x` to do. */
  min-width: 0;
  grid-auto-flow: column; grid-auto-columns: minmax(96px, 1fr);
  overflow-x: auto; overscroll-behavior-x: contain;
  padding-bottom: 4px;
  scroll-snap-type: x proximity;
  scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent;
}
.shape-grid::-webkit-scrollbar { height: 8px; }
.shape-grid::-webkit-scrollbar-track { background: var(--canvas); border-radius: 999px; }
.shape-grid::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; }
.shape-grid::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }
/* `position: relative` keeps the hidden radio inside its card; see the note on
   `.template-card`, where the same input gave the page a phantom scrollbar. */
.shape-card { display: block; position: relative; text-align: center; cursor: pointer; scroll-snap-align: start; }
.shape-card input { position: absolute; opacity: 0; pointer-events: none; }
.shape-sample {
  display: flex; align-items: center; justify-content: center;
  height: 68px;
  border: 2px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface);
}
/* Matched to the template swatch beside it - same height, same border, same
   radius - so the two pickers read as one row of choices rather than as a light
   one and a heavy one. The sample is a rendered code, not a glyph, so it is
   *not* pixelated: at this size a nearest-neighbour circle is a lumpy octagon,
   and the whole point of the sample is to show the shape. The rule against
   smoothing applies to codes that get scanned. */
.shape-sample img { width: 46px; height: 46px; image-rendering: auto; }
.shape-card:hover .shape-sample { border-color: var(--brand); }
.shape-card.is-selected .shape-sample { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-light); }
.shape-card:focus-within .shape-sample { outline: 2px solid var(--brand); outline-offset: 2px; }
.shape-name { display: block; font-size: 0.74rem; color: var(--ink-muted); margin-top: 5px; }

/* --- Logo gallery -------------------------------------------------------------
   Small dense tiles, because there are nearly forty of them and the mark is
   what identifies each one - the name under it only confirms the choice. The
   grid scrolls rather than pushing the rest of the step down the page. */
.logo-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: 8px; margin: 6px 0 14px;
  max-height: 224px; overflow-y: auto;
  padding: 4px 4px 2px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--canvas);
}
.logo-card { display: block; text-align: center; cursor: pointer; }
.logo-card input { position: absolute; opacity: 0; pointer-events: none; }
.logo-sample {
  display: flex; align-items: center; justify-content: center;
  height: 48px; border-radius: var(--radius-sm);
  border: 2px solid transparent; background: var(--surface);
}
.logo-sample img { width: 34px; height: 34px; }
.logo-none { color: var(--ink-faint); font-size: 1.1rem; }
.logo-card:hover .logo-sample { border-color: var(--brand); }
.logo-card.is-selected .logo-sample { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-light); }
.logo-card:focus-within .logo-sample { outline: 2px solid var(--brand); outline-offset: 2px; }
.logo-name {
  display: block; font-size: 0.68rem; color: var(--ink-muted); margin-top: 3px;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
/* A tier without logos gets the gallery greyed rather than removed - a control
   that vanishes teaches nothing about what the paid tiers are for. */
.logo-grid:has(input:disabled) { opacity: 0.5; pointer-events: none; }

/* --- Frame picker ------------------------------------------------------------
   Each tile is a real framed code, rendered by the generator - "bubble" and
   "label above" mean nothing until you have seen them, and a CSS drawing of a
   frame is a second implementation that would drift from the first. */
/* Twenty of them, laid out the way the looks are: two rows filled column by
   column, scrolling sideways. See the note above `.template-grid` - everything
   said there applies here for the same reasons, and the two pickers sit one
   above the other, so they had better behave the same. */
.frame-grid {
  display: grid; gap: 10px; margin: 10px 0 14px;
  min-width: 0;
  grid-auto-flow: column; grid-template-rows: repeat(2, auto); grid-auto-columns: 92px;
  overflow-x: auto; overscroll-behavior-x: contain;
  padding-bottom: 8px;
  scroll-snap-type: x proximity;
  scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent;
}
.frame-grid::-webkit-scrollbar { height: 8px; }
.frame-grid::-webkit-scrollbar-track { background: var(--canvas); border-radius: 999px; }
.frame-grid::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; }
.frame-grid::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }
/* `position: relative` for the same reason the look cards need it: the radio
   inside is absolutely positioned, and without a positioned ancestor it lands
   at its static position past the right edge of the scroller and gives the page
   a phantom horizontal scrollbar. */
.frame-card { display: block; position: relative; text-align: center; cursor: pointer; scroll-snap-align: start; }
.frame-card input { position: absolute; opacity: 0; pointer-events: none; }
.frame-sample {
  display: flex; align-items: center; justify-content: center;
  height: 84px; padding: 7px;
  border: 2px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface);
}
/* Fit whichever way the frame is taller or wider; a captioned frame is not
   square and must not be squashed to fit a square tile. */
.frame-sample img { max-width: 100%; max-height: 100%; width: auto; height: auto; image-rendering: auto; }
.frame-card:hover .frame-sample { border-color: var(--brand); }
.frame-card.is-selected .frame-sample { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-light); }
.frame-card:focus-within .frame-sample { outline: 2px solid var(--brand); outline-offset: 2px; }
.frame-name { display: block; font-size: 0.74rem; color: var(--ink-muted); margin-top: 5px; }

/* Twenty presets do not belong in a wall that pushes the rest of the builder
   off the screen. Two rows, filled column by column, and the row scrolls
   sideways - `grid-auto-flow: column` is what makes the pair of rows the fixed
   dimension and the length the free one. */
.template-grid {
  display: grid; gap: 10px; margin-top: 10px;
  /* Without this the grid takes its content's width rather than its box's, and
     there is nothing left for `overflow-x` to do. */
  min-width: 0;
  grid-auto-flow: column; grid-template-rows: repeat(2, auto); grid-auto-columns: 92px;
  overflow-x: auto; overscroll-behavior-x: contain;
  padding-bottom: 8px;
  scroll-snap-type: x proximity;
  scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent;
}
.template-card { scroll-snap-align: start; }
/* WebKit needs the bar drawn by hand, and the default is a grey slab that
   looks like part of the form rather than part of the scrolling. */
.template-grid::-webkit-scrollbar { height: 8px; }
.template-grid::-webkit-scrollbar-track { background: var(--canvas); border-radius: 999px; }
.template-grid::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; }
.template-grid::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }
/* `position: relative` is what keeps the hidden radio inside the card. The
   input is absolutely positioned, and with no positioned ancestor its
   containing block is the page itself - so once the looks scrolled sideways,
   twenty invisible radios sat at their static positions past the right edge,
   outside the scroller's clipping, and gave the whole page a phantom
   horizontal scrollbar with nothing in it. */
.template-card { display: block; position: relative; text-align: center; cursor: pointer; }
.template-card input { position: absolute; opacity: 0; pointer-events: none; }
/* The tile is the code itself now, not a glyph tinted twice, so the box only
   has to hold it: the ground is the look's own background and the render sits
   in the middle with a little air around it. */
.template-card .swatch {
  display: flex; align-items: center; justify-content: center;
  height: 68px; padding: 5px; border-radius: var(--radius-sm); border: 2px solid var(--line);
  overflow: hidden;
}
.template-card .swatch img { display: block; width: auto; height: 100%; border-radius: 3px; }
.template-card.is-selected .swatch { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-light); }
.template-name { display: block; font-size: 0.78rem; color: var(--ink-muted); margin-top: 5px; }

.builder-preview {
  /* Clear of whatever is fixed above: 20px on the public builder, which has
     nothing, and 20px below the bar inside the shell. */
  position: sticky; top: calc(var(--sticky-top) + 20px);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 18px; box-shadow: var(--shadow);
}
/* The mat behind the preview takes the code's own background colour, which is
   what `--preview-mat` carries. The box is square and a framed code is not - the
   caption bar hangs below the square - so the image is fitted by height and
   there is spare width either side of it. Against the brand tint that spare
   width read as two pale strips down the sides of the picture, as if the render
   had edges on it. Matched to what the render's own margin is drawn in - white
   around a frame, the code's background without one - there is nothing to see.
   The tint is the fallback, and it is what the empty state still shows. */
.preview-frame {
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 1; background: var(--preview-mat, var(--brand-tint));
  border-radius: var(--radius-sm); margin-bottom: 14px; overflow: hidden;
}
/* Height as well as width. A framed code is taller than it is wide - the
   caption bar is outside the square - and the preview box is square with
   `overflow: hidden`, so constraining only the width crops the caption off and
   the frame looks like it did nothing.

   Smoothed, not pixelated. `image-rendering: pixelated` only helps when an
   image is drawn *larger* than it was made; the preview box is 278px and the
   render is 320px or more, so it is always drawn smaller - and a nearest
   downscale throws pixels away instead of averaging them, which unevens the
   modules and turns the logo's curves to gravel. This is the on-screen
   picture, not the file: the PNG itself is still resampled with NEAREST where
   that matters. */
.preview-frame img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  object-fit: contain;
  image-rendering: auto;
}
.preview-empty { text-align: center; padding: 20px; }
.preview-placeholder { width: 46px; height: 46px; opacity: 0.25; }
/* --- Scannability meter ------------------------------------------------------
   The band drives the colour, not the number: green at a glance means "print
   it", amber means "read the line under it". The notes below are the actual
   product - a score with no reason attached cannot be acted on. */
/* A read-out, not a rule across the panel, and three versions got here: a 6px
   line with a caption reads as a section divider, a large number beside a thin
   bar reads as two unrelated things, and the number set inside the bar had to be
   drawn twice to stay legible where the fill ended under it.

   So there is no number. The track is the whole scale - red at nought through
   amber to green at a hundred - and a marker sits at the reading, the way a
   slider shows where it has been set. The scale is the same picture every time,
   which is what makes a mark two thirds along it mean something at a glance;
   the word beside the label says which band that is. */
.scan-meter {
  margin: 14px 0; padding: 12px 14px 11px;
  background: var(--canvas); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.scan-meter[data-empty="1"] { opacity: 0.55; }
.scan-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.scan-label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.76rem; font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--ink-faint);
}
.scan-gauge { margin-top: 12px; }
/* The verdict wears its own state as a chip. This is the one place a status
   colour is the subject rather than a warning about the page, and it always
   arrives with the word beside it - never colour alone. */
.scan-band {
  padding: 2px 8px; border-radius: 999px;
  background: var(--canvas); color: var(--ink-muted); font-size: 0.78rem; font-weight: 600;
}
.scan-meter[data-band="excellent"] .scan-band,
.scan-meter[data-band="good"] .scan-band { background: rgb(21 115 71 / 0.12); color: var(--ok); }
.scan-meter[data-band="fair"] .scan-band { background: rgb(180 83 9 / 0.13); color: var(--warn-ink); }
.scan-meter[data-band="poor"] .scan-band { background: rgb(180 35 24 / 0.12); color: var(--danger); }

/* `--score` is the reading, and the marker's position is the only thing that
   uses it: the track never changes. The stops are where the bands change hands,
   so the colour under the marker is the colour of the verdict beside it. */
.scan-bar {
  --score: 0;
  position: relative; height: 14px; border-radius: 999px;
  background: linear-gradient(
    to right,
    var(--danger) 0%, var(--danger) 12%,
    var(--warn) 46%,
    #4a9d6a 74%,
    var(--ok) 100%
  );
  box-shadow: inset 0 1px 2px rgb(16 24 40 / 0.18);
}
/* The quarter marks. White at low opacity reads on every part of the ramp,
   which a single ink colour does not. */
.scan-bar::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background:
    linear-gradient(to right, transparent calc(25% - 1px), rgb(255 255 255 / 0.45) calc(25% - 1px), rgb(255 255 255 / 0.45) 25%, transparent 25%),
    linear-gradient(to right, transparent calc(50% - 1px), rgb(255 255 255 / 0.45) calc(50% - 1px), rgb(255 255 255 / 0.45) 50%, transparent 50%),
    linear-gradient(to right, transparent calc(75% - 1px), rgb(255 255 255 / 0.45) calc(75% - 1px), rgb(255 255 255 / 0.45) 75%, transparent 75%);
}
/* The marker. White with a ring in the band's colour, so it is visible against
   every part of the ramp it can land on - a solid coloured dot disappears into
   the stretch of track that shares its colour, which is exactly the stretch it
   sits on. `clamp` keeps it on the track at nought and at a hundred instead of
   hanging half off each end. */
.scan-marker {
  position: absolute; top: 50%; z-index: 1;
  left: clamp(11px, calc(var(--score) * 1%), calc(100% - 11px));
  transform: translate(-50%, -50%);
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--surface); border: 3px solid var(--ink-muted);
  box-shadow: 0 1px 4px rgb(16 24 40 / 0.28);
  transition: left 0.25s ease;
}
.scan-meter[data-band="excellent"] .scan-marker,
.scan-meter[data-band="good"] .scan-marker { border-color: var(--ok); }
.scan-meter[data-band="fair"] .scan-marker { border-color: var(--warn); }
.scan-meter[data-band="poor"] .scan-marker { border-color: var(--danger); }
/* The scale the marker is placed on. Numerals only - they read the same in all
   three languages and need no catalogue entry. */
.scan-scale {
  display: flex; justify-content: space-between;
  margin-top: 7px; font-size: 0.68rem; color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.scan-notes { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 5px; }
.scan-notes li {
  display: flex; align-items: flex-start; gap: 7px;
  font-size: 0.78rem; color: var(--ink-muted); line-height: 1.35;
}
/* `display: flex` above beats the browser's own `[hidden] { display: none }`,
   so without this every note in the table shows at once and the panel reads as
   a list of everything wrong with a code nobody has built yet. */
.scan-notes li[hidden] { display: none; }
/* Always shown, whatever the score. The number grades the settings; this is the
   half of the answer the settings cannot reach. */
.scan-caveat { color: var(--ink-faint); margin: 8px 0 0; line-height: 1.35; }
.scan-notes li .icon { flex: none; margin-top: 0.15em; color: var(--warn); }

.preview-actions { display: grid; gap: 8px; }
/* The same reasoning, more so: a 1024px download shown at 280px is a four-fold
   reduction, and doing that by nearest neighbour drops three pixels in four. */
.code-image { max-width: 280px; width: 100%; height: auto; image-rendering: auto; }
.download-row { display: flex; gap: 8px; justify-content: center; margin-top: 16px; }

.detail-meta .row { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0; border-bottom: 1px solid var(--line); }
.detail-meta .row:last-child { border-bottom: none; }
.detail-meta .row > span:first-child {
  display: inline-flex; align-items: center; gap: 8px;
  color: var(--ink-muted); font-size: 0.85rem; white-space: nowrap;
}
.detail-meta .row > span:first-child .icon { opacity: 0.5; }

/* The heading and the period switch are centred over the four cards - there is
   nothing else on this page to align them to the left of. The column itself is
   left where `.content-wide` puts it on every other page; only what sits above
   the grid moves. */
.plan-page .page-head { justify-content: center; text-align: center; }
.plan-page .page-head h1 { justify-content: center; }
/* Column flex rather than `text-align`, which would centre every feature row
   inside all four cards. The grid takes its full width back, since a column
   flex would otherwise shrink it to its content. */
.plan-page .plan-periods { display: flex; flex-direction: column; align-items: center; }
.plan-page .plan-periods > .grid { width: 100%; }

/* A column, so the action can be pinned to the bottom of whichever card is
   tallest - see .plan-features below. Without it the four buttons sit wherever
   their own feature list happens to end, which is what made the row ragged. */
.plan-card {
  display: flex; flex-direction: column; position: relative; overflow: hidden;
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.plan-card:hover { transform: translateY(-3px); box-shadow: 0 10px 24px rgb(16 24 40 / 0.09); }
.plan-card .card-body { display: flex; flex-direction: column; flex: 1; }
.plan-card .card-head { position: relative; }

/* A quiet checkerboard behind the tier name - the product is QR codes, and a
   pricing table for it may as well look like it knows that. Kept under 6%
   opacity: it should register as texture, not as a pattern to read. */
.plan-card .card-head::after {
  content: ""; position: absolute; inset: 0 0 0 auto; width: 150px;
  background-image: repeating-conic-gradient(rgb(23 30 40 / 0.06) 0% 25%, transparent 0% 50%);
  background-size: 10px 10px;
  /* Faded from the top-right corner outwards rather than sideways. A linear
     fade leaves the pattern with a hard edge along the top, the right and the
     header's own bottom border, which reads as a pasted rectangle instead of
     texture. The radius is set so it has reached zero by the time it meets
     that border. */
  -webkit-mask-image: radial-gradient(105px 105px at 100% 0, #000 8%, transparent 66%);
  mask-image: radial-gradient(105px 105px at 100% 0, #000 8%, transparent 66%);
  pointer-events: none;
}

.plan-card.is-current { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-light); }
.plan-card.is-current:hover { box-shadow: 0 0 0 3px var(--brand-light), 0 10px 24px rgb(16 24 40 / 0.09); }

/* The one tier the table argues for. A rule across the top, a warmer ground
   behind the name, and a solid button - three small signals rather than one
   loud one. */
.plan-card.is-recommended { border-color: var(--line-strong); }
.plan-card.is-recommended::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 3px; z-index: 1;
  background: linear-gradient(90deg, var(--brand), var(--logo-blue) 55%, var(--brand-dark));
}
.plan-card.is-recommended .card-head { background: var(--brand-tint); }
.plan-card.is-recommended .card-head h2 { color: var(--brand); }
.plan-card.is-recommended .card-head::after {
  background-image: repeating-conic-gradient(rgb(47 96 148 / 0.13) 0% 25%, transparent 0% 50%);
}
.plan-flag {
  display: inline-flex; align-items: center; gap: 5px; position: relative; z-index: 1;
  padding: 3px 9px; border-radius: 999px; white-space: nowrap;
  background: linear-gradient(135deg, var(--brand), var(--brand-dark));
  box-shadow: 0 1px 3px rgb(38 82 127 / 0.35);
  color: #fff;
  font-size: 0.68rem; font-weight: 650; letter-spacing: 0.03em; text-transform: uppercase;
}
.plan-flag .icon { width: 12px; height: 12px; }

.plan-price { font-size: 1.6rem; font-weight: 650; margin-bottom: 10px; }
.plan-per { font-size: 0.8rem; font-weight: 500; color: var(--ink-muted); margin-left: 4px; }
/* Beside the price, not under it. On its own line the badge pushes the feature
   list of the two tiers that have one out of line with the two that do not, and
   the four cards stop reading as a row. */
.plan-save {
  display: inline-flex; align-items: center; margin-left: 8px;
  padding: 3px 9px; border-radius: 999px; white-space: nowrap;
  background: var(--brand-light); color: var(--brand);
  font-size: 0.72rem; font-weight: 600; letter-spacing: 0.01em;
  vertical-align: 3px;
}
.plan-save .icon { width: 12px; height: 12px; margin-right: 4px; vertical-align: -1px; }

/* The monthly/yearly switch. Every rule below hangs off `:checked ~`, so the
   two inputs have to remain siblings of the switch and the grid - see the
   comment in plan.html. Hidden with clip rather than display:none, because a
   radio that is not rendered is a radio the keyboard cannot reach. */
.plan-period-input {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%);
}
.plan-switch {
  display: inline-flex; gap: 4px; margin-bottom: 18px; padding: 4px;
  border: 1px solid var(--line); border-radius: 999px; background: var(--canvas);
}
.plan-switch label {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 16px; border-radius: 999px;
  font-size: 0.85rem; font-weight: 600; color: var(--ink-muted);
  cursor: pointer; user-select: none;
  transition: background 0.15s ease, color 0.15s ease;
}
.plan-switch label:hover { color: var(--ink); }
.plan-switch label .icon { width: 14px; height: 14px; opacity: 0.6; }
#plan-period-monthly:checked ~ .plan-switch label[for="plan-period-monthly"],
#plan-period-yearly:checked ~ .plan-switch label[for="plan-period-yearly"] {
  background: var(--surface); color: var(--brand); box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
}
#plan-period-monthly:checked ~ .plan-switch label[for="plan-period-monthly"] .icon,
#plan-period-yearly:checked ~ .plan-switch label[for="plan-period-yearly"] .icon { opacity: 1; }
/* The group takes the focus ring, since the radio itself is clipped away. */
.plan-period-input:focus-visible ~ .plan-switch { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The euro restatement, under the figure rather than beside it: the mark price
   is what is charged and has to stay the largest thing in the card. The two
   spans inside it are caught by the same `:checked ~ .grid` rules below, which
   are descendant selectors and so reach a nested one. */
.plan-price-eur {
  font-size: 0.78rem; font-weight: 500; color: var(--ink-faint);
  /* Held open on the two tiers that have no euro figure, or their feature lists
     start a line above the other two and the row of four comes out ragged. */
  margin-top: -4px; min-height: 1.15em;
}

/* One price is shown, the other is removed. `.plan-save` belongs to yearly.

   Two containers, because the switch drives the plan page's `.grid` and the
   landing page's `.pricing-grid`, which are different layouts showing the same
   table. The ids are shared on purpose - the two pages are never on screen
   together, and a second set of names would mean a second set of these rules to
   keep in step. */
#plan-period-monthly:checked ~ .grid .price-yearly,
#plan-period-monthly:checked ~ .grid .plan-save,
#plan-period-yearly:checked ~ .grid .price-monthly,
#plan-period-monthly:checked ~ .pricing-grid .price-yearly,
#plan-period-monthly:checked ~ .pricing-grid .plan-save,
#plan-period-yearly:checked ~ .pricing-grid .price-monthly { display: none; }
/* flex:1 so the list absorbs the slack in a short card and the action below it
   lands on the same line in all four. */
.plan-features { list-style: none; margin: 0 0 16px; padding: 0; font-size: 0.88rem; flex: 1; }
.plan-features li {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 0; border-bottom: 1px solid var(--line);
}
/* The tick or dash, in its own slot so every label starts on the same column
   whichever of the two it is - the glyphs are not the same width. */
.plan-feature-mark { display: inline-flex; flex: none; width: 16px; justify-content: center; }
.plan-features li .icon { flex: none; color: var(--ok); }
.plan-feature-label { min-width: 0; }
.plan-features li.is-excluded { color: var(--ink-faint); }
.plan-features li.is-excluded .icon { color: var(--ink-faint); }
.plan-features li.is-excluded strong { font-weight: 550; color: inherit; }

/* --- What paying here means, under a price ----------------------------------
   Shared by the plan page and the landing page's price block - see
   core/_pay_terms.html.

   A card, not a centred column of small print. The first version of this was
   the marks and two sentences centred under the grid with no ground of their
   own, which read as loose text somebody had left there rather than as the
   payment terms: nothing bounded it, the long currency sentence set its own
   ragged measure, and the policy links wrapped into a third centred line. */
/* Full width of whatever column it is in, so its edges line up with the price
   cards above it. Capped and centred it read as a separate narrower thing
   floating under the grid rather than as the last row of it. */
.pay-terms { margin-top: 26px; }
.pay-terms.is-flush { border: none; background: none; box-shadow: none; }
.pay-terms .legal-callout { margin: 0; }

/* The marks at one end and the policy links at the other, stacking when there
   is no room for both. */
.pay-terms-row {
  display: flex; flex-wrap: wrap;
  align-items: flex-start; justify-content: space-between;
  gap: 14px 24px; margin-top: 14px;
}

.pay-terms-links {
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  margin: 0; padding: 0; list-style: none; font-size: 0.86rem;
}
.pay-terms-links li { display: inline-flex; align-items: center; gap: 7px; }
.pay-terms-links .icon { color: var(--ink-faint); flex: none; }

/* The landing page's arrangement of the same facts: no card, stacked and
   centred under the price grid, the policies as one sentence. It sits at the
   foot of a centred section rather than inside a column of content, and a
   bordered box there reads as a fifth card in a row of four. */
.pay-terms.is-centred {
  display: flex; flex-direction: column; align-items: center;
  gap: 14px; text-align: center;
}
.pay-terms.is-centred .legal-callout { max-width: 78ch; text-align: left; }
.pay-terms.is-centred .card-marks { align-items: center; }
.pay-terms-read { margin: 0; max-width: 86ch; }
/* A policy name is one thing to click, so it wraps as one - the line broke
   between "Payment" and "security" and left half a link on its own. */
.pay-terms-read a { white-space: nowrap; }

/* Directly under the block above, and to the same edges. */
.pay-terms-note { margin-top: 12px; }
.plan-features li.is-excluded .icon { color: var(--ink-faint); }

/* The current-plan marker is sized as a button, not as a badge: it stands in
   the same slot as one, and matching its box is what keeps the bottom of the
   four cards on one line. Borrowed metrics from .btn - padding, radius, weight
   - so the two stay the same height if either is ever restyled. */
.plan-action { margin-top: auto; }
.plan-current {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  width: 100%; padding: 9px 16px;
  border: 1px dashed var(--brand); border-radius: var(--radius-sm);
  background: var(--brand-light); color: var(--brand);
  font-size: 0.89rem; font-weight: 600;
}
/* A badge always has a ground. Without one the plain variant was bare text in a
   column of pills, which reads as a missing value rather than a state. */
.badge {
  display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px;
  border-radius: 999px; font-size: 0.75rem; font-weight: 600;
  background: var(--canvas); color: var(--ink); border: 1px solid var(--line);
}
.badge-current {
  background: linear-gradient(180deg, var(--brand-light), var(--brand-tint));
  color: var(--brand); border-color: rgb(47 96 148 / 0.22);
}

/* --- The logo on the menu settings ------------------------------------------
   The file that is on the menu now, and the way to take it off. Shown as the
   thing itself rather than as "Currently: menu-logos/a8f3.png", which is a path
   nobody chose and cannot recognise. */
.logo-current {
  display: flex; align-items: center; gap: 14px;
  margin: 2px 0 10px; padding: 10px 12px;
  background: var(--canvas);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.logo-current img {
  flex: none; width: 64px; height: 64px; object-fit: contain;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 5px;
}
.logo-current-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; }

/* --- The confirmation dialog -------------------------------------------------
   One per page, filled in by whichever trigger opened it. A <dialog>, so the
   focus trap, Esc and the backdrop are the browser's job rather than ours. */
.modal {
  border: none; padding: 0;
  max-width: 440px; width: calc(100% - 32px);
  border-radius: var(--radius);
  background: var(--surface); color: var(--ink);
  box-shadow: 0 18px 44px rgb(16 24 40 / 0.22), 0 2px 8px rgb(16 24 40 / 0.10);
}
.modal::backdrop { background: rgb(22 32 44 / 0.45); }

.modal-head { display: flex; align-items: center; gap: 11px; padding: 18px 20px 0; }
.modal-head h3 { margin: 0; font-size: 1.05rem; }

.modal-icon {
  flex: none; width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; background: var(--brand-light); color: var(--brand);
}
/* Red only when the thing being confirmed cannot be undone. A warning triangle
   on every question teaches people to click through it. */
.modal.is-danger .modal-icon { background: var(--danger-bg); color: var(--danger); }

.modal-body {
  margin: 0; padding: 12px 20px 20px;
  color: var(--ink-muted); font-size: 0.93rem; line-height: 1.5;
}

.modal-foot {
  display: flex; justify-content: flex-end; gap: 8px;
  padding: 14px 20px; border-top: 1px solid var(--line);
  background: var(--brand-tint);
  border-radius: 0 0 var(--radius) var(--radius);
}
.modal.is-danger .modal-foot { background: var(--danger-bg); }

/* The accept button is filled here, unlike `.btn-danger` elsewhere in the app.
   Outlined, it sits on a foot of the same tint and reads as the quieter of the
   two - and the quieter of the two is Cancel, which is already the one with
   focus. The dialog asks a question; the button that answers it has to look
   like the answer. */
.modal-foot .btn-danger {
  background: var(--danger); border-color: var(--danger); color: #fff;
}
.modal-foot .btn-danger:hover { background: #9d2018; border-color: #9d2018; }
.modal-foot .btn-danger .icon { color: currentColor; }

/* --- The signed-out shell ----------------------------------------------------
   One centred column on the canvas, shared by sign-in, sign-up and the four
   screens of the forgotten-password flow - see core/base_auth.html.

   `min-height: 100dvh` rather than `100vh`: on a phone the browser's own bars
   are counted into `vh`, so a card centred in it sits low enough that the
   footer link falls under the address bar.

   The column used to be a fixed 460px cap with a 60px top margin, which put the
   card near the top of a tall window and gave the wider screens - the sign-up
   form and the two-box reset form - no way to ask for more room. The width is a
   block each page overrides now. */
.auth-shell {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px 20px;
  background: var(--canvas);
}

.auth-column { width: 100%; }

/* The mark and the language, with the switcher under the logo rather than
   beside it - the column is 520px at its widest and the two do not share a line
   comfortably in every language. */
.auth-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  margin-bottom: 22px;
}
.auth-brand .brand-logo { margin: 0 auto; max-width: 210px; height: auto; }

/* The gap under the heading belongs to the heading, because three of these
   screens have no lede at all - sign in, and the two that only report a result.
   With the gap on the lede instead, "Sign in" sat directly on the first label. */
.auth-title { font-size: 1.32rem; margin: 0 0 16px; }
/* The lede then pulls back up against it. A negative margin under a *heading* is
   safe in a way one under a field is not: a heading's height is its own line and
   does not grow, whereas a field grows by however many errors it has. */
.auth-lede { color: var(--ink-muted); font-size: 0.88rem; margin: -12px 0 16px; }

/* The two screens that only report a result have no form to align to, so the
   heading is centred over the text rather than left above it. */
.card-body.is-centred { text-align: center; }

.auth-foot {
  margin-top: 18px;
  display: flex; justify-content: center; align-items: center;
  gap: 6px; flex-wrap: wrap;
}
/* The icon and its words are one label. Without this the arrow on "Back to sign
   in" is welded to the B. */
.auth-foot a { display: inline-flex; align-items: center; gap: 6px; }

/* The way out of a refused password, at the end of the row it belongs to. */
.login-forgot { margin: -2px 0 0; text-align: right; }

/* The two screens that only report: a link is on its way, and the password has
   been changed. No form on either, so the icon carries the state and the text
   is centred under it rather than set as a paragraph. */
.reset-sent { text-align: center; }
.reset-sent p { margin-bottom: 8px; }
.reset-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  margin-bottom: 14px;
  border-radius: 50%;
  background: var(--brand-light);
  color: var(--brand);
  font-size: 1.3rem;
}
.reset-icon.is-done { background: var(--ok-bg); color: var(--ok); }

@media (max-width: 480px) {
  /* Top-aligned rather than centred, and a tighter gutter. Vertically centred
     on a short screen the card starts below the fold, so the first thing on
     screen is empty canvas. */
  .auth-shell { align-items: flex-start; padding: 24px 14px 40px; }
}

/* --- Password requirements ---------------------------------------------------
   Stated once under both password boxes rather than as help text on one of
   them, which made the two columns of the field row different heights - see
   core/_password_rules.html and QuietHelpMixin. */
.password-rules {
  /* Positive top margin, deliberately. A negative one tucks the block under the
     field row until a password is rejected: the validator messages render
     inside the field above, and the block would be pulled up over them. Nothing
     that sits below a field may use negative margin - the field's height is not
     fixed, it grows by however many errors it has. */
  margin: 10px 0 16px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--brand-tint);
}

.password-rules-title {
  display: block;
  font-size: 0.78rem;
  font-weight: 650;
  color: var(--ink-muted);
  margin-bottom: 7px;
}

/* Two columns where there is room: four stacked lines read as a wall of
   conditions, which is the thing being avoided. */
.password-rules ul {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 16px;
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (max-width: 520px) {
  .password-rules ul { grid-template-columns: 1fr; }
}

.password-rules li {
  display: flex;
  align-items: flex-start;
  gap: 7px;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--ink-muted);
}

.password-rules li .icon { color: var(--brand); margin-top: 0.22em; font-size: 0.85em; }

/* --- Card acceptance marks, and the callouts on the policy pages -------------
   In main.css rather than landing.css because the plan page carries them too,
   and that one is inside the signed-in shell. */
.card-marks { display: flex; flex-direction: column; gap: 8px; }
.card-mark-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.card-mark-list img { display: block; width: 42px; height: 28px; }
.card-marks-note { margin: 0; font-size: 0.78rem; color: var(--ink-faint); }

/* The one sentence in a section that answers the requirement the section exists
   for - the currency, the refund route, the fact that no card number arrives
   here. Set apart so a reviewer working down a checklist finds it without
   reading the paragraph around it. */
.legal-callout {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 14px 0;
  padding: 12px 14px;
  border-left: 3px solid var(--brand);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--brand-tint);
  color: var(--ink);
  line-height: 1.55;
}
.legal-callout .icon { flex: none; margin-top: 0.15em; color: var(--brand); }
/* --- Language switcher ------------------------------------------------------
   Three flag buttons. The flag is 20x14, not square like an .icon glyph, so it
   gets its own rule rather than borrowing that one. */
.lang-switch { display: inline-flex; align-items: center; }
.lang-options {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 2px; background: var(--canvas);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.lang-option {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 5px; border: none; border-radius: 5px;
  background: transparent; color: var(--ink-muted);
  font: inherit; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em;
  cursor: pointer;
}
.lang-option:hover { background: var(--brand-tint); color: var(--ink); }
.lang-option:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.lang-option.is-active { background: var(--surface); color: var(--brand); box-shadow: var(--shadow); }
/* Inside a form the picker is a control like any other, so it takes the whole
   width and the three options share it. `inline-flex` is right in the toolbar,
   where it sits beside other things, but in a column of full-width inputs it
   left a short box floating at one end. It also sat on the parent's baseline as
   an inline box, which put a sliver of line-box descender under it that the top
   did not have - the gap under the row read as tighter than the gap over it for
   that reason rather than any padding. `flex` is block-level and has no
   baseline to sit on. */
.field .lang-options { display: flex; width: 100%; }
.field .lang-option { flex: 1; justify-content: center; padding: 8px 7px; }

/* The same control built from radios, for the copy on the profile form - see
   core/_language_field.html. The input is clipped, so the label carries both
   the appearance and the focus ring. */
.lang-radio:checked + .lang-option { background: var(--surface); color: var(--brand); box-shadow: var(--shadow); }
.lang-radio:focus-visible + .lang-option { outline: 2px solid var(--brand); outline-offset: 1px; }
/* Room to spell the language out, since this one is not squeezed into a
   toolbar and "BS" is not a word anybody has to guess at. */
.lang-options .lang-name { font-weight: 500; letter-spacing: 0; text-transform: none; }
/* The compact form: flag and code, like the toolbar. For a picker that shares a
   row with another field, where three spelled-out names would wrap. The name is
   still in the markup for a screen reader, which has no flag to read. */
.lang-options.is-compact .lang-name {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

.lang-option .flag {
  width: 17px; height: 12px; display: block; flex: none;
  border-radius: 2px;
  /* The flags are flat colour to the edge, and several are pale at one corner;
     without this they dissolve into a light toolbar. */
  box-shadow: 0 0 0 1px rgba(16, 24, 40, 0.15);
}

/* Present for screen readers and for nothing else - the flag and the two-letter
   code are the visible label, and neither says "language" out loud. */
/* Off the screen but still in the accessibility tree. The second selector is
   not a duplicate: `.field input` sets `width: 100%` and outweighs a single
   class, so a clipped radio inside a form was 390px wide and pushed the whole
   page sideways on a phone - the language picker on the profile did exactly
   that. A utility whose one job is to take up no room has to win. */
.visually-hidden,
.field .visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* What the month's allowance has taken, under the Generate button. Amber the
   moment the next code would be refused, because the button above it is about
   to send somebody to the plans page and this is the only warning before it. */
.quota-line { color: var(--ink-muted); margin: 0; }
.quota-figure { font-weight: 650; font-variant-numeric: tabular-nums; color: var(--ink); }
.quota-line.is-full { color: var(--warn-ink); }
.quota-line.is-full .quota-figure { color: var(--warn-ink); }

/* --- The menu editor --------------------------------------------------------
   A tree of small forms: tabs, the headings inside them, the lines inside those.
   Indented rather than boxed, because four nested cards is a set of frames with
   a menu somewhere inside it. */
/* A tab, and everything under it. A ground a shade off the card rather than an
   outline on white: outlined, four tabs on one white card read as one long form
   with some rules drawn on it, and the eye has to follow a 1px line to work out
   where one tab ends. A filled block is spotted rather than traced. */
.tree-section {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--canvas);
  padding: 12px;
  margin-bottom: 14px;
}

/* The headings inside it go back to white, so the nesting reads as depth: the
   card is white, the tab is grey, the heading inside it is white again. Three
   levels, two colours, alternating - which is the one place alternation earns
   its keep, because it is saying "inside", not "next". */
.tree-category {
  margin: 10px 0 10px 14px; padding: 8px 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 2px solid var(--line-strong);
  border-radius: var(--radius-sm);
}
/* The one-line add forms sit on the tab's own ground, so they take the card
   colour to stay legible as inputs rather than as part of the block. */
.tree-section > .tree-add, .tree-category > .tree-add { background: var(--surface); }
.tree-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tree-rename { display: flex; align-items: center; gap: 6px; flex: 1 1 240px; margin: 0; }
.tree-rename input {
  flex: 1 1 auto; min-width: 0; padding: 6px 9px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink); font: inherit; font-size: 0.9rem;
}
.tree-section > .tree-head .tree-rename input { font-weight: 650; }
/* The three one-button forms sit in a row rather than stacking, which is what
   they would do as block-level forms. */
.tree-actions { display: flex; align-items: center; gap: 4px; }
.tree-actions form { margin: 0; }

.tree-items { list-style: none; margin: 8px 0; padding: 0; }
.tree-item {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0; border-top: 1px solid var(--line-soft);
}
.tree-item:first-child { border-top: none; }
/* Truncated rather than allowed to run on: `min-width: 0` lets the box shrink,
   but without this the text itself spills straight over the price beside it -
   "Ćevapi€9.50" on a phone, and on any name long enough at any width. */
.tree-item-name {
  flex: 1 1 auto; min-width: 0; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tree-item-price { font-variant-numeric: tabular-nums; color: var(--ink-muted); }
/* Sold out, in the editor as on the menu: still in its place, plainly off. */
.tree-item.is-out .tree-item-name { text-decoration: line-through; color: var(--ink-faint); }
/* An offer or event a guest cannot see is dimmed rather than struck through:
   it is not withdrawn, it is out of its window or switched off, and the reason
   is spelled out in the column beside it. */
.tree-item.is-out { opacity: 0.62; }
.tree-item.is-out .tree-item-price { font-style: italic; }

/* The glyph in front of a tab's name, saying what it holds. Not editable and
   not a control - the kind is fixed once the tab exists. */
.tree-kind {
  display: inline-flex; flex: none; color: var(--ink-faint);
  padding: 0 2px;
}
/* The venue's own chip - "1+1", "-30%" - beside an offer's name in the tree, so
   the list reads the way the public page does. */
.tree-chip {
  display: inline-block; margin-left: 7px; padding: 1px 7px;
  border-radius: 999px; background: var(--brand-light); color: var(--brand);
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.03em;
  text-transform: uppercase; vertical-align: 1px;
}
.tree-empty { padding: 6px 0; font-size: 0.85rem; }

/* An input group: the fields run together and the button is attached at the
   right-hand end of them, so the row reads as one control. Enter submits it,
   because it is a form with a single submit button - the button is there for
   the people who reach for a button. */
.tree-add {
  display: flex; align-items: stretch; margin: 8px 0 0;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); overflow: hidden;
}
.tree-add > input, .tree-add > select { border: none !important; border-radius: 0 !important; }
.tree-add > input + input, .tree-add > select + input { border-left: 1px solid var(--line) !important; }
.tree-add > .btn { border-radius: 0; border: none; border-left: 1px solid var(--line); white-space: nowrap; }
.tree-add > .btn:hover { background: var(--brand-light); }
.tree-add input, .tree-add select {
  padding: 6px 9px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink); font: inherit; font-size: 0.88rem;
}
.tree-add input[type="text"] { flex: 1 1 140px; min-width: 0; }
.tree-add input[type="number"] { width: 110px; }

/* A date field inside the group. `datepicker.js` wraps its input in a
   positioning div, and that wrapper - not the input - becomes the flex child,
   which breaks the group two ways and broke the events row both of them:

   - the `> input` rules above stop matching, so the field kept its own border
     and rounded corners and sat inside the group as a second box;
   - the calendar is drawn *under* the field, and `overflow: hidden` - which is
     what rounds the group's ends - cut all of it off. The picker opened on
     every click and was never visible, which reads as a control that does
     nothing.

   So the wrapper takes over the flex sizing and the seam. Lifting the clip is
   the other half, and it is the same rule the network dropdown already needed
   - see `.tree-add:has(.net-picker)` further down, where both are written
   once. */
.tree-add > .datepicker-wrap { display: flex; flex: 1 1 150px; min-width: 0; }
.tree-add > .datepicker-wrap > input {
  flex: 1 1 auto; min-width: 0; padding-right: 34px;
  border: none !important; border-radius: 0 !important;
}
.tree-add > input + .datepicker-wrap > input,
.tree-add > select + .datepicker-wrap > input { border-left: 1px solid var(--line) !important; }
.tree-add > .datepicker-wrap + input, .tree-add > .datepicker-wrap + select {
  border-left: 1px solid var(--line) !important;
}
.tree-add-section { margin-top: 4px; }

/* The five looks, drawn in their own colours: a swatch of the page it makes. */
/* Twenty looks. As a wrapping grid they were a wall six rows deep that pushed
   every other setting on the card off the screen, so they are two fixed rows
   that scroll sideways instead - the shape a picker of many similar things
   wants, and the one that keeps the card the same height whatever the count.

   `grid-auto-flow: column` fills down then across, so reading order matches the
   order in `styles.TEMPLATES`. The columns are a fixed width rather than a
   fraction: `1fr` in a scrolling track resolves against the *visible* width and
   would squeeze all twenty into it. */
.menu-templates {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(2, auto);
  grid-auto-columns: 92px;
  gap: 12px 10px;
  margin-bottom: 14px;
  overflow-x: auto;
  /* Room for the focus ring and the selected preset's outline, which would
     otherwise be clipped by the scroll container on the first and last column. */
  padding: 3px 3px 10px;
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
}
.menu-template { scroll-snap-align: start; }

/* A thin track rather than the platform's default, which on a desktop is a
   16px bar across a card that is only 190px tall. */
.menu-templates::-webkit-scrollbar { height: 8px; }
.menu-templates::-webkit-scrollbar-track { background: var(--canvas); border-radius: 999px; }
.menu-templates::-webkit-scrollbar-thumb {
  background: var(--line-strong); border-radius: 999px;
}
.menu-templates::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }
/* Firefox, which takes no pseudo-elements for this. */
.menu-templates { scrollbar-width: thin; scrollbar-color: var(--line-strong) var(--canvas); }
/* Each preset is a submit button now, not a label with a radio in it: the
   choice saves itself, so there is nothing for a radio to hold. */
.menu-template {
  display: block; position: relative; text-align: center; cursor: pointer;
  padding: 0; border: none; background: none; font: inherit; color: inherit;
}
.menu-presets { padding-bottom: 4px; }
/* A heading inside a card, for a card that answers two questions. Small and in
   the label's own colour, so it separates without competing with the card's
   own title above it. */
/* A menu the tier no longer carries: still listed, plainly inactive. */
tr.is-parked td { opacity: 0.6; }
.badge-warn { background: rgb(180 83 9 / 0.13); color: var(--warn-ink); }

/* A band across the whole card, not a line of small grey text.

   The settings card asks four separate questions - what the place is called,
   what its menu looks like, how to reach it, what it charges in - and as four
   faint captions between forty inputs, none of them registered: the card read
   as one long column of fields. Full-bleed, so the band is the width of the
   card and the eye reads it as a divider rather than as another label.

   The negative inline margin is the card body's own padding, given back as
   padding on the band. Any change to `.card-body` has to be made here too;
   there is no way to say "ignore my parent's padding" in CSS. */
.group-title {
  margin: 22px -18px 14px;
  padding: 9px 18px;
  font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--brand);
  /* Left to right, strong to nothing. A flat fill reads as a table header and
     would compete with the card's own head above it; a gradient that runs out
     reads as a rule with a label on it. */
  background: linear-gradient(
    90deg,
    var(--brand-light) 0%,
    var(--brand-tint) 42%,
    transparent 100%
  );
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  /* The mark at the start of the band, in the brand proper - the one saturated
     thing on it, so the row has somewhere to begin. */
  box-shadow: inset 3px 0 0 var(--brand);
}
/* The first band sits flush against the top of the body it opens, so it needs
   no rule above it and no gap - see `.pt-0`, which is on that body. */
.card-body > .group-title:first-child { margin-top: 0; border-top: none; }
.pt-0 { padding-top: 0 !important; }
.menu-swatch {
  display: flex; flex-direction: column; justify-content: flex-end; gap: 4px;
  height: 62px; padding: 8px; border-radius: var(--radius-sm);
  border: 2px solid var(--line); overflow: hidden;
}
.menu-swatch-bar { height: 8px; width: 46%; border-radius: 999px; }
.menu-swatch-line { height: 5px; width: 100%; border-radius: 999px; opacity: 0.85; }
.menu-swatch-line.short { width: 62%; }
/* `:has` rather than a class from the server: the swatch has to answer the
   click, not the next page load. The server class stays for the first render. */
.menu-template.is-selected .menu-swatch,
.menu-template:has(input:checked) .menu-swatch { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-light); }
.menu-template:focus-visible .menu-swatch { outline: 2px solid var(--brand); outline-offset: 2px; }
.menu-template:hover .menu-swatch { border-color: var(--brand); }
/* The radio was `pointer-events: none` and off in the corner of the page; kept
   inside its own label, so the whole tile is the target. */
.menu-template input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.menu-template-name { display: block; font-size: 0.76rem; color: var(--ink-muted); margin-top: 5px; }

/* A menu's header: name, address, whether it is live, and every action, on one
   line. Two blocks - a heading with buttons, then a card with the switch and
   more buttons - was two rows of chrome above a page whose subject is the menu
   underneath. It wraps rather than shrinking, so a narrow window gets two tidy
   rows instead of a cramped one. */
.menu-head { align-items: center; gap: 14px; flex-wrap: wrap; }
.menu-head-title { flex: 1 1 240px; min-width: 0; }
.menu-head-title h1 { margin: 0; }
.menu-head-url {
  display: inline-block; margin-top: 2px; font-size: 0.85rem;
  color: var(--ink-muted); word-break: break-all;
}
.menu-head-url:hover { color: var(--brand); }
.menu-head .page-actions { align-items: center; }
.menu-head .page-actions form { margin: 0; }

/* A switch that is a submit button: one bit, saved the instant it is flipped,
   with no checkbox and no Save beside it. The button carries the state, so it
   works with no JavaScript - which a checkbox that saves on change would not. */
.switch-form { margin: 0; }
.switch {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 4px; border: none; background: none; cursor: pointer;
  font: inherit; color: var(--ink-muted);
}
.switch-track {
  position: relative; display: block; width: 46px; height: 26px;
  border-radius: 999px; background: var(--line-strong);
  transition: background 0.18s ease;
}
.switch-knob {
  position: absolute; top: 3px; left: 3px; width: 20px; height: 20px;
  border-radius: 50%; background: var(--surface);
  box-shadow: 0 1px 3px rgb(16 24 40 / 0.3);
  transition: transform 0.18s ease;
}
.switch-label { font-size: 0.85rem; font-weight: 650; }
.switch.is-on .switch-track { background: var(--ok); }
.switch.is-on .switch-knob { transform: translateX(20px); }
.switch.is-on .switch-label { color: var(--ok); }
.switch:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 999px; }

/* The four colours of a menu, on one line: they are one palette. */
/* Two across, not four. Four columns leave each field 114px, and twelve quick
   picks need 213px at a size anybody can hit - so four columns means either a
   scroller or dots too small to aim at. Two columns give each palette its own
   line at full size, and the four boxes still read as one row of a palette
   rather than as four separate questions. */
.colour-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 16px; }
.colour-row .field { margin-bottom: 10px; }
/* Four colour boxes side by side leave a quarter of the width each, and the
   twelve quick picks under one of them do not fit at the size they are in a
   full-width field. They are shrunk to fit rather than made to scroll: a
   shortcut somebody has to drag sideways is not a shortcut, and all twelve have
   to be one click away. */
.colour-row .colour-dots { flex-wrap: nowrap; gap: 3px; }
.colour-row .colour-dot { flex: none; width: 16px; height: 16px; }
@media (max-width: 640px) { .colour-row { grid-template-columns: minmax(0, 1fr); } }

.rate-list { list-style: none; margin: 0 0 10px; padding: 0; }
.rate-list li { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--line-soft); }
.rate-list form { margin: 0; }
/* A card's footer: the row a form's own button sits in. It had no rules at all,
   so the button sat flush against the card's right and bottom edges. */
.card-foot {
  padding: 14px 18px; border-top: 1px solid var(--line);
  background: var(--canvas); border-radius: 0 0 var(--radius) var(--radius);
}
.card-foot-end { display: flex; justify-content: flex-end; }
.rate-actions { display: flex; align-items: center; gap: 6px; }
/* Where to find today's rate: a dropdown and a link into a search, in a row. */
.rate-lookup { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.rate-lookup label { font-size: 0.8rem; color: var(--ink-muted); }
.rate-lookup select {
  padding: 6px 9px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink); font: inherit; font-size: 0.88rem;
}
.rate-actions form { margin: 0; }

/* A date and time is not a headline number; it keeps the tile's shape without
   pretending to be a count. */
.stat-value.tiny-value { font-size: 1.05rem; }

/* --- Pager -------------------------------------------------------------------
   Under every list, and the same one under each: the size control on the left,
   what is on screen in the middle, the way through the pages on the right. It
   sits inside the card's own padding rather than under it, so the table and its
   controls read as one object. */
.pager {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 10px 16px;
  padding: 12px 16px; border-top: 1px solid var(--line);
  font-size: 0.85rem; color: var(--ink-muted);
}
.pager-size { display: flex; align-items: center; gap: 10px; }
.pager-size-label { font-size: 0.78rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-faint); }
/* A segmented control: one track, four numbers, the current one raised out of
   it. The set is small and fixed, so showing all of it costs a few pixels and
   saves opening anything - and the raised tile is the state, which a select
   only shows once it is closed again. */
.segmented {
  display: inline-flex; padding: 3px; gap: 2px;
  background: var(--canvas); border: 1px solid var(--line); border-radius: 999px;
}
.segmented-option {
  min-width: 34px; padding: 4px 10px; border-radius: 999px;
  color: var(--ink-muted); font-size: 0.82rem; font-weight: 600;
  font-variant-numeric: tabular-nums; text-align: center; text-decoration: none;
  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.segmented-option:hover { background: var(--brand-light); color: var(--brand); text-decoration: none; }
.segmented-option.is-active {
  background: var(--surface); color: var(--brand);
  box-shadow: 0 1px 2px rgb(16 24 40 / 0.12);
}
.segmented-option:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.pager-range { font-variant-numeric: tabular-nums; }
.pager-links { display: flex; align-items: center; gap: 4px; }
/* Round, because they are one glyph each and a rounded rectangle round a single
   chevron reads as a button with something missing from it. */
.pager-links .btn-icon {
  width: 30px; height: 30px; padding: 0; border-radius: 50%;
}
.pager-page {
  padding: 0 10px; font-size: 0.85rem; font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* The ends of the list keep their buttons in place rather than removing them,
   so the row does not shuffle sideways as somebody pages through it. */
.pager-links .is-disabled { opacity: 0.35; pointer-events: none; }

/* --- Toasts ----------------------------------------------------------------- */
/* In the flow, directly under the page's navigation - not fixed to a corner of
   the window. Three arrangements were tried: the top right corner is beside
   whatever somebody was looking at rather than in front of it, and on a wide
   screen it is the better part of a metre from the button that produced it;
   floated over the content it covered the first thing they were about to read.
   Taking up room pushes the page down instead, which is what makes a message
   impossible to miss and impossible to be in the way of.

   Each page includes the block itself, under its own header - see
   core/messages.html and the note in core/base.html. */
/* A public page's content is centred under a 1120px cap with 24px of padding,
   and this matches it, so a message shares the cards' own edges. */
.toasts {
  display: grid; gap: 8px;
  width: 100%; max-width: 1120px; margin: 16px auto 0; padding: 0 24px;
}
/* In the signed-in shell the content column is capped at 1200 and left aligned
   under it, with the same padding. */
.shell .toasts { max-width: 1200px; margin: 16px 0 0; }
/* The auth pages are a single narrow column that already has its own padding. */
.auth-shell .toasts { max-width: none; margin: 0 0 16px; padding: 0; }
/* The wash carries the same colour as the border and the glyph, fading out
   before it reaches the words - so the toast is tinted by its state without any
   text ever sitting on colour. */
.toast {
  display: flex; align-items: center; gap: 12px;
  /* The whole strip dismisses it, so it has to look like something to click. */
  cursor: pointer;
  border: 1px solid var(--line); border-left: 3px solid var(--brand);
  border-radius: var(--radius-sm); padding: 11px 14px;
  box-shadow: 0 4px 14px rgb(16 24 40 / 0.1);
  /* Colour underneath, wash on top of it - two properties rather than one
     gradient ending in `--surface`. The wash is translucent at its coloured
     end, and now that these sit over the page rather than in an empty corner,
     that let the heading underneath show through the left of the toast. */
  background-color: var(--surface);
  background-image: linear-gradient(90deg, var(--brand-light), transparent 52%);
}
.toast-success { border-left-color: var(--ok); background-image: linear-gradient(90deg, rgb(21 115 71 / 0.13), transparent 52%); }
.toast-warning { border-left-color: var(--warn); background-image: linear-gradient(90deg, rgb(180 83 9 / 0.13), transparent 52%); }
.toast-error { border-left-color: var(--danger); background-image: linear-gradient(90deg, rgb(180 35 24 / 0.13), transparent 52%); }
.toast-close {
  display: inline-flex; border: none; background: none; padding: 2px;
  font-size: 0.8rem; cursor: pointer; color: var(--ink-faint);
}
.toast-close:hover { color: var(--ink); }
/* Leaving, rather than blinking out: 200ms, which is what `shell.js` waits for
   before it takes the element out of the page. */
.toast { transition: opacity 0.2s ease, transform 0.2s ease; }
.toast.is-leaving { opacity: 0; transform: translateY(-6px); }
/* The glyph repeats the border colour, which is the point - the border
   alone is the kind of signal a red-green colour blindness erases. */
.toast-icon { flex: none; font-size: 1.05rem; color: var(--brand); }
.toast-success .toast-icon { color: var(--ok); }
.toast-warning .toast-icon { color: var(--warn); }
.toast-error .toast-icon { color: var(--danger); }
.toast span { flex: 1; }

/* --- Narrow screens ---------------------------------------------------------
   Every multi-column grid collapses to one. minmax(0, 1fr) throughout, for the
   reason given at .grid-1: fixed columns cannot shrink, .table-wrap's
   overflow-x never engages, and the whole page scrolls sideways instead of the
   table. */
@media (max-width: 980px) {
  .shell { grid-template-columns: minmax(0, 1fr); }
  .sidebar { border-right: none; border-bottom: 1px solid var(--line); }
  .grid-2, .grid-4, .grid-stats { grid-template-columns: minmax(0, 1fr); }
  .builder-panes { grid-template-columns: minmax(0, 1fr); }
  .builder-preview { position: static; }
  .field-row { grid-template-columns: minmax(0, 1fr); }
}

/* --- On a phone --------------------------------------------------------------
   Three things that only go wrong at this width, all of them a row of controls
   that has more in it than fits. */
@media (max-width: 640px) {
  /* A crowded action row drops its words and keeps its icons. Only crowded
     ones: a page whose header holds a single "New code" has room for the label,
     and a lone icon there would be a guess. Three or more controls is what does
     not fit, which is why this hangs off :has() rather than being blanket.
     The label stays in the markup - the button is still named for a screen
     reader, and `title` names it for a pointer. */
  .page-actions:has(> :nth-child(3)) .btn-text {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
  .page-actions:has(> :nth-child(3)) .btn { padding-left: 10px; padding-right: 10px; }
  .page-actions:has(> :nth-child(3)) .btn .icon { margin: 0; }

  /* The add-a-row forms stack instead of sharing one line. Four controls across
     390px left the two text boxes about eight pixels wide each - present, and
     impossible to type in. The group keeps its single border; the children lose
     the vertical rules that only make sense side by side. */
  .tree-add, .tree-rename { flex-wrap: wrap; }
  .tree-add > input, .tree-add > select, .tree-add > .net-picker,
  .tree-rename > input {
    flex: 1 1 100%; min-width: 0;
    border-left: none !important;
    border-bottom: 1px solid var(--line) !important;
  }
  .tree-add > .btn { flex: 1 1 100%; border-left: none; }
  /* The rename form's Save is a lone icon; full width it sits marooned in the
     middle of a row of its own. It keeps its size and goes to the end. */
  .tree-rename > .btn { flex: 0 0 auto; margin-left: auto; border-left: none; }
  .tree-add > :last-child, .tree-rename > :last-child { border-bottom: none !important; }

  /* A tap target is a finger, not a mouse pointer. The move, delete and edit
     buttons came out at 30px squares, the small buttons at 30px tall, and the
     colour swatches at 16px - all of them fine under a cursor and a guess under
     a thumb. */
  .btn-icon { min-width: 38px; min-height: 38px; }
  .btn-sm { min-height: 38px; }
  .lang-option { padding: 4px 5px; }
  .colour-dot { width: 24px; height: 24px; }
  /* The quick-pick row keeps all twelve on one line and shrinks them to 16px to
     do it - which is a cursor-sized target. Twelve 24px dots still fit across a
     390px card, so on a phone the row gets the bigger ones. */
  .colour-row .colour-dot { width: 24px; height: 24px; }
}

/* --- The type of a saved code ------------------------------------------------
   One presentation for the list, the dashboard and the detail header, so a code
   looks like the same kind of thing everywhere it is mentioned. */
/* A pill rather than an icon beside a word: the type is the one thing every row
   of every list is scanned for, and a chip is what makes it findable without
   reading. */
.type-tag {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px; border-radius: 999px; white-space: nowrap;
  background: var(--brand-tint); border: 1px solid rgb(47 96 148 / 0.16);
  color: var(--brand); font-size: 0.78rem; font-weight: 550;
}
.type-tag .icon { opacity: 0.8; }
.card-head .head-warn { color: var(--warn); margin-right: 8px; }

/* The same reading on a saved code, where nothing is live and it is rendered
   server-side rather than assembled from headers. */
.scan-static { margin-top: 14px; }

.landing-footer .brand-logo { height: 34px; }

/* --- Picking a network -------------------------------------------------------
   A dropdown that can carry a mark, which a native <select> cannot. The real
   <select> is still in the markup and is what submits; the script hides it and
   shows this instead, so the page works either way.

   Each mark is drawn in the network's own colour, pulled 30% toward the app's
   ink for the same reason the public menu does it: WhatsApp's green measures
   1.9:1 against a white card and is a logo you have to hunt for. */
/* The input group clips its children so its corners stay rounded, which also
   clips away anything that opens *below* the row - this dropdown to nothing,
   and the date calendar in the offers and events rows to nothing. Both are the
   same bug and take the same rule, so it is written once: a row carrying a
   popup stops clipping and rounds its two end children itself.

   `overflow-x: clip` is not a shortcut past this. It keeps the corners and
   still cuts the calendar off at the row's right-hand edge. */
.tree-add:has(.net-picker), .tree-add:has(.datepicker-wrap) { overflow: visible; }
.tree-add:has(.net-picker) > :first-child,
.tree-add:has(.datepicker-wrap) > :first-child {
  border-top-left-radius: var(--radius-sm); border-bottom-left-radius: var(--radius-sm);
}
.tree-add:has(.net-picker) > :last-child,
.tree-add:has(.datepicker-wrap) > :last-child {
  border-top-right-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm);
}

.net-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; flex: none;
  color: color-mix(in oklab, var(--net, currentColor) 70%, var(--ink));
}
.net-mark .icon { width: 15px; height: 15px; }
.net-row { display: inline-flex; align-items: center; gap: 8px; }

.net-picker { position: relative; display: flex; min-width: 168px; }
.net-picker select { flex: 1 1 auto; min-width: 0; }
.net-toggle {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 6px 9px; border: none; background: var(--surface);
  color: var(--ink); font: inherit; font-size: 0.88rem;
  text-align: left; cursor: pointer;
}
.net-toggle:hover { background: var(--brand-light); }
.net-name { flex: 1 1 auto; }
.net-caret { width: 12px; height: 12px; color: var(--ink-faint); }
.net-menu {
  position: absolute; z-index: 30; top: calc(100% + 4px); left: 0;
  min-width: 100%; margin: 0; padding: 4px; list-style: none;
  max-height: 280px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); box-shadow: 0 10px 24px rgb(16 24 40 / 0.14);
}
.net-menu li {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px; border-radius: var(--radius-sm);
  font-size: 0.88rem; cursor: pointer; white-space: nowrap;
}
.net-menu li:hover, .net-menu li.is-active { background: var(--brand-light); }
.net-menu li[aria-selected="true"] { font-weight: 600; }
