/* ---------------------------------------------------------------- drawn at 80% (Danny, 4 October 2026)

   The app was designed and signed off in Chrome at 80% zoom; at 100% the same pages scrolled.
   "If you want it to fit at normal size for everyone, I can shrink the whole app by about a fifth":
   "do this". So it is drawn at 80% everywhere, the way the browser's own zoom would draw it:
     phones and tablets: the viewport meta (base.html, initial-scale=0.8), which widens the layout
       viewport by a quarter exactly as zoom does, media queries and vh included;
     desktops: CSS zoom on html, below. Zoom does not move the media queries and does scale vh, so
       every breakpoint carries a desktop twin at 0.8 of its width, and every vh length is divided
       by --z. Scripts that read a size off the screen and write it into a style divide by the
       zoom too (app.js, zoom()).
   A desktop is a fine pointer with no touch screen: a touch device is already scaled by the meta. */
:root { --z: 1; }
@media (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) {
  :root { zoom: 0.8; --z: 0.8; }
}

/* SITE&SEOND app: LEDGER concept. Black, white, Inter, hairlines. Every component in one file.
   Tokens are BRIEF values only. Weight 300 is used at 20px and above only; everything smaller is 400
   (row counts included) so nothing goes grey on non-retina screens. Inputs are 16px so iOS does not zoom. */

:root {
  --bg: #000000;
  --ink: #ffffff;
  --ink-2: rgba(255, 255, 255, .6);
  --ink-tab: rgba(255, 255, 255, .5);
  --ink-off: rgba(255, 255, 255, .3);
  --rule: rgba(255, 255, 255, .18);
  --rule-2: rgba(255, 255, 255, .35);
  /* The faintest fill: a booked day that has gone, on the SALES calendar. */
  --fill-off: rgba(255, 255, 255, .07);

  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px;
  --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px; --s10: 128px;

  --font: Inter, -apple-system, "Helvetica Neue", Arial, sans-serif;
  --fs-body: 15px;
  --fs-small: 13px;
  --fs-label: 11px;
  --fs-h2: 20px;
  --fs-num: 28px;
  --fs-display: 64px;
  --fs-input: 16px;

  --header-h: 56px;
  --row-min: 52px;
  --col-max: 720px;
  --gutter: 20px;
  --fade: 200ms;
}

@media (min-width: 600px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (min-width: 480px) {
  :root {
    --fs-body: 16px;
    --fs-label: 12px;
    --fs-h2: 22px;
    --fs-num: 32px;
    --fs-display: 80px;
    --gutter: 24px;
  }
}

/* ---------------------------------------------------------------- reset and shell */

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

/* The page itself never moves (Danny, 4 October 2026, on the home-screen app: "it drags the whole app
   away from the top or bottom of the screen ... hard locked"). Everything that scrolls does it inside
   main.scroll; the document is exactly one screen, cannot scroll, and has no rubber band to pull.
   app.js adds the touch half of the lock in the installed app, where iOS bounces a drag that has
   nothing to scroll. */
html, body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  background: var(--bg);
  margin: 0;
}

html {
  color-scheme: dark;
  -webkit-text-size-adjust: 100%;
  font-synthesis: none;
}

body {
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.5;
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

#app {
  height: calc(100vh / var(--z));
  height: calc(100dvh / var(--z));
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Positioned and lifted so the site switcher's panel can hang below the header over the scrolling
   content, which comes after it in the flow and would otherwise paint on top. Under .skip (20). */
.hd {
  flex: 0 0 auto;
  height: var(--header-h);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 0 var(--gutter);
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
  position: relative;
  z-index: 12;
}

.hd-centre {
  min-width: 0;
  text-align: center;
  /* At 360 with a site switcher the centre column sizes to the select's widest option and squeezes
     the side columns, running the wordmark and the site name together. This padding is what holds
     them apart: 16px each side above 380, and 12px at 360, where the max-width:380 rule at the foot
     of this file takes it down to var(--s3) to hand the select the width back. The grid's own auto
     column plus min-width:0 here is what lets the select shrink rather than push. */
  padding: 0 var(--s4);
}

.hd-site {
  display: block;
  font-size: var(--fs-body);
  font-weight: 400;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.hd-right {
  display: flex;
  justify-content: flex-end;
  align-items: center;
}

.hd-account {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: -11px;
  color: var(--ink-2);
  text-decoration: none;
}

.hd-account[aria-current="page"], .hd-account:hover { color: var(--ink); }

.hd-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  text-decoration: none;
}

.hd-link:hover { color: var(--ink); }

/* The site switcher (Danny, 9 September 2026). A <details> disclosure, not a <select>: a native
   dropdown draws the browser's own menu, in the browser's own grey and its own typeface, over a
   black app, and on a phone it takes over the screen entirely. This panel is the app's: black,
   hairlines, Inter, filled dot on the site you are looking at. It needs no JavaScript (details and
   summary are native, each site is a link); app.js only closes it on Escape or a click outside. */
.switcher { min-width: 0; }

/* The button IS the site name in the centre of the header (BRIEF section 4), with a 1px chevron
   after it. 200px of name plus the chevron and its gap; the 100% keeps it inside the centre column
   at 360, where a long name takes the ellipsis. The air between the wordmark and the name is
   .hd-centre's padding. */
.switcher-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  height: 44px;
  max-width: min(224px, 100%);
  margin: 0 auto;
  padding: 0 var(--s2);
  color: var(--ink);
  font-size: var(--fs-input);
  cursor: pointer;
  /* Both lines, or the browser draws its own triangle beside ours: the standard property for
     Firefox and Chrome, the pseudo-element for Safari. */
  list-style: none;
}
.switcher-btn::-webkit-details-marker { display: none; }
.switcher-btn::marker { content: ""; }

.switcher-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.switcher-chev { flex: 0 0 auto; color: var(--ink-2); }
/* No transition on the flip: the motion rule is opacity fades only, so this is a state, not a move. */
.switcher[open] .switcher-chev { transform: rotate(180deg); color: var(--ink); }

/* The panel hangs off .hd (the positioned ancestor), full width of the header, so a long name and
   its domain both have room at 360 without touching the centre column. */
.switcher-panel {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  background: var(--bg);
  border-bottom: 1px solid var(--rule-2);
  max-height: calc(calc(100dvh / var(--z)) - var(--header-h) - 120px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  animation: switcher-in .2s ease-out;
}

@keyframes switcher-in { from { opacity: 0; } to { opacity: 1; } }

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

.switcher-opt {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: 56px;
  padding: var(--s3) var(--gutter);
  border-top: 1px solid var(--rule);
  color: var(--ink-2);
  text-align: left;
  text-decoration: none;
}
.switcher-list li:first-child .switcher-opt { border-top: 0; }
.switcher-opt .dot { margin-right: 0; flex: 0 0 auto; }
.switcher-opt[aria-current], .switcher-opt:hover { color: var(--ink); }
.switcher-opt-text { display: flex; flex-direction: column; min-width: 0; }
.switcher-opt-name { font-size: var(--fs-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.switcher-opt-sub {
  font-size: var(--fs-small);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Wordmark: the three letters at 500 against the rest, the whole system in miniature. 300 only at
   20px and above (the landing masthead, the manifesto); at the app's 15px and 13px it is 400, the
   same rule as every other size under 20px, so it never goes grey on a non-retina screen. */
.wm {
  font-size: 15px;
  font-weight: 400;
  letter-spacing: -.02em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  justify-self: start;
}

.wm b { font-weight: 500; }
.wm-sm { font-size: 13px; }
a.wm { display: inline-flex; align-items: center; min-height: 44px; gap: var(--s2); }

/* The logo mark in front of the wordmark, the landing site's masthead brought into the app
   (Danny, 9 September 2026). The file is static/icon.svg, byte for byte web/assets/logo.svg and
   the home-screen icon, so the two surfaces cannot drift apart. */
.wm-logo { width: 24px; height: 24px; display: block; flex: none; }

.scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* No rubber band at either end (Danny, 21 September 2026: "the we create your assets bounces up
     and down, like the page is a bit loose"). The epigraph on the socials tab is sticky, so it
     rides whatever the scroller does: overscrolling it made the one pinned thing on the page the
     one thing that moved.

     `-webkit-overflow-scrolling: touch` came out with it. It has done nothing since iOS 13, which
     gives momentum scrolling by default, and what it still does is put the scroller on its own
     compositing layer, which is a well known way to make a sticky child judder. */
  overscroll-behavior: none;
}

/* The inner wrapper carries the bottom spacing: WebKit drops a scroller's own padding-bottom.

   It is also a full height flex column (Danny, 10 September 2026), so the epigraph can be pinned to
   the foot of the page rather than following whatever happens to be above it. On a short tab it sat
   halfway up the screen; on a long one it sat at the end; and it moved every time a section appeared
   or folded away. .scroll is a flex item whose height the layout has already resolved, so the 100%
   here is a definite one, and every box is border-box, so the column's own padding is inside it and
   a short page still does not scroll. */
.col {
  max-width: var(--col-max);
  margin: 0 auto;
  padding: var(--s5) var(--gutter) var(--s6);
  overflow-wrap: anywhere;
  min-height: 100%;
  display: flex;
  flex-direction: column;
}

/* An absolute path or a long URL: .col's overflow-wrap: anywhere already breaks as a last resort;
   the templates add a <wbr> after each slash (the wbr filter) so a path breaks at slashes first. */
.path { overflow-wrap: anywhere; }

.tabs {
  flex: 0 0 auto;
  display: flex;
  background: var(--bg);
  border-top: 1px solid var(--rule);
  padding-bottom: calc(max(env(safe-area-inset-bottom), 20px) + 8px);
}

/* No desktop override: iPad landscape standalone still has a home indicator, and env() is 0 on a desktop, so the floor is 28px there and harmless. */

.tabs {
  justify-content: center;
}

.tab {
  position: relative;
  flex: 1 1 0;
  max-width: 240px;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: var(--s2) 0 var(--s1);
  color: var(--ink-tab);
  text-decoration: none;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
}

.tab svg { display: block; width: 22px; height: 22px; }

.tab[aria-current="page"] { color: var(--ink); }

.tab[aria-current="page"]::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 50%;
  width: 24px;
  height: 1px;
  margin-left: -12px;
  background: var(--ink);
}

/* ---------------------------------------------------------------- type */

h1, h2, h3, p { margin: 0; }

.h2, h2 {
  font-size: var(--fs-h2);
  font-weight: 300;
  line-height: 1.2;
  letter-spacing: 0;
}

h1.h2 { margin-bottom: var(--s4); }

.display {
  font-size: var(--fs-display);
  font-weight: 300;
  line-height: 1;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}

.label, h1.label, h2.label, h3.label {
  display: block;
  font-size: var(--fs-label);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}

h1.label, h2.label { margin-bottom: var(--s3); }

.num {
  font-size: var(--fs-num);
  font-weight: 300;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.muted { color: var(--ink-2); }
.small { font-size: var(--fs-small); }
/* "not yet checked" and friends are read, so they sit at --ink-2; only the decorative middle dot
   (.absent.mid) uses --ink-off, which is for disabled controls. */
.absent, .pending { color: var(--ink-2); }
.absent.mid { color: var(--ink-off); }
.tabular { font-variant-numeric: tabular-nums; }

p { max-width: 34em; }
p + p { margin-top: var(--s3); }
p + .actions, .list + .actions, .tiles + .actions { margin-top: var(--s4); }

a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--rule-2);
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}

a:hover { text-decoration-color: var(--ink); }

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

::selection { background: var(--ink); color: var(--bg); }

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

/* ---------------------------------------------------------------- sections */

.sec { margin-top: var(--s6); }
.sec-first { margin-top: 0; }
.sec > .label + .list, .sec > .label + .tiles, .sec > .label + .card { margin-top: 0; }

/* A section heading with a way through at the other end of the line (Danny, 9 September 2026:
   "put the ALL PAGES on the opposite side"). The heading keeps its own label rhythm; the link sits
   on the same baseline, and both wrap onto their own lines below 320px rather than colliding. */
.sec-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
}

.sec-head > .label { margin-bottom: var(--s3); }
.sec-head-link { flex: 0 0 auto; }
/* The list that follows a .sec-head sits tight to it, the same as one following a bare .label. */
.sec > .sec-head + .list, .sec > .sec-head + .tiles, .sec > .sec-head + .card { margin-top: 0; }

/* The History fold takes the same mark as every other section (Danny, 9 September 2026: "make sure
   the sections are nicely broken"). It cannot carry a <span>, because a details element has to open
   with its summary, so the hairline is drawn here instead. */
.history::before {
  content: "";
  display: block;
  width: 48px;
  height: 1px;
  background: var(--rule-2);
  margin-bottom: var(--s4);
}

/* The section mark: a 48px hairline at --rule-2 above a section's heading, so the page breathes. */
.sec-mark {
  display: block;
  width: 48px;
  height: 1px;
  background: var(--rule-2);
  margin-bottom: var(--s4);
}

.sec > p.muted { margin-top: var(--s2); }

/* ---------------------------------------------------------------- status dots */

.dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ink);
  vertical-align: middle;
  margin-right: var(--s2);
  position: relative;
  top: -1px;
}

.dot.hollow {
  background: transparent;
  border: 1px solid var(--ink);
}

.dot.off { background: var(--ink-off); border: 0; }

/* ---------------------------------------------------------------- notices and errors */

.notice {
  padding: var(--s3) 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--s4);
  max-width: none;
}

.err {
  margin-top: var(--s2);
  font-size: var(--fs-small);
  color: var(--ink);
}

.hint {
  margin-top: var(--s2);
  font-size: var(--fs-small);
  color: var(--ink-2);
}

/* ---------------------------------------------------------------- cards */

.card {
  border: 1px solid var(--rule);
  padding: var(--s4);
}

.card-title {
  font-size: var(--fs-h2);
  font-weight: 300;
  line-height: 1.2;
  margin-bottom: var(--s2);
  word-break: break-word;
}

.card .list { margin-top: var(--s2); }
.card .list-row:first-child { border-top: 0; }
.card .list-row:last-child { padding-bottom: 0; }

/* ---------------------------------------------------------------- stat tiles */

/* background-clip: padding-box so the hairline background does not double up under the border
   (kept in step with web/css/components.css). */
.tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background: var(--rule);
  background-clip: padding-box;
  border: 1px solid var(--rule);
}

/* An odd tile on a two-column phone grid spans the row, so no empty cell shows its background. */
.tiles.tiles-3 > .tile:nth-child(3),
.tiles.tiles-5 > .tile:nth-child(5) { grid-column: 1 / -1; }

@media (min-width: 600px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (min-width: 480px) {
  .tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .tiles.tiles-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tiles.tiles-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .tiles.tiles-3 > .tile:nth-child(3) { grid-column: auto; }
  /* Five numbers (views, calls, directions, website, bookings) want more room than four: at this
     width they go three and two, the last one spanning, rather than being squeezed to a width where
     DIRECTIONS does not fit on its own line. */
  .tiles.tiles-5 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .tiles.tiles-5 > .tile:nth-child(5) { grid-column: span 2; }
}

@media (min-width: 900px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (min-width: 720px) {
  .tiles.tiles-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .tiles.tiles-5 > .tile:nth-child(5) { grid-column: auto; }
}

/* A label and a number are never broken across lines (Danny, 10 September 2026: "DIRECTIONS" came
   back as "DIRECTION" over "S", and 1,399 as "1,39" over "9"). .col sets overflow-wrap: anywhere for
   the long paths and URLs elsewhere in the app, and it was reaching in here, where a word that does
   not fit should make the tile wider rather than being cut in half. */
.tile > .label, .tile > .num, .tile > .num-money {
  overflow-wrap: normal;
  word-break: keep-all;
  white-space: nowrap;
}

/* Money tiles (the Bookings block): "$12,500.00" must fit a 160px tile on a phone. */
.tile .num-money { font-size: var(--fs-h2); overflow-wrap: anywhere; }

.tile {
  background: var(--bg);
  padding: var(--s4);
  min-height: 96px;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}

/* The number sits under its label and the note takes the bottom slot, so a note that wraps to two
   lines never lifts its number: label, number and note line up across the row at any width. */
.tile .label { margin: 0; }
.tile .num { margin-top: 0; }

.tile .note {
  margin-top: auto;
  font-size: var(--fs-small);
  color: var(--ink-2);
  line-height: 1.4;
}

/* ---------------------------------------------------------------- hairline lists */

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

.list-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s1) var(--s3);
  min-height: var(--row-min);
  padding: var(--s3) 0;
  border-top: 1px solid var(--rule);
}

.list-row:last-child { border-bottom: 1px solid var(--rule); }
.list.list-open .list-row:last-child { border-bottom: 0; }

.row-name {
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 400;
  overflow-wrap: anywhere;
}

/* margin-left: auto so a value pushed onto its own line by a long name still hugs the right edge. */
.row-val, .row-count {
  flex: 0 0 auto;
  margin-left: auto;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.row-val .dot, .row-name .dot { margin-right: var(--s2); }
.row-sub {
  flex: 1 1 100%;
  font-size: var(--fs-small);
  color: var(--ink-2);
}

.row-meta {
  flex: 0 0 auto;
  font-size: var(--fs-small);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* Thin bars: an SVG track with the width set as a rect attribute, never an inline style.
   <svg class="bar" viewBox="0 0 100 1" preserveAspectRatio="none" aria-hidden="true"><rect width="63" height="1"/></svg> */
.bar {
  display: block;
  flex: 1 1 100%;
  width: 100%;
  height: 1px;
  margin-top: var(--s1);
  background: var(--rule);
  overflow: visible;
}

.bar rect { fill: var(--ink); }

/* ---------------------------------------------------------------- activity feed */

.feed .list-row { align-items: baseline; }
.feed time, .feed .row-meta { order: 2; }

/* ---------------------------------------------------------------- range switcher */

.range {
  display: flex;
  gap: var(--s5);
  margin: var(--s4) 0;
}

.range button, .range a {
  min-height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink-2);
  font-family: var(--font);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  border-radius: 0;
  cursor: pointer;
}

.range [aria-current="true"], .range [aria-pressed="true"] {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

/* The dates under the switcher ("29 Aug to 4 Sep 2026, up to the latest day in the log"). */

/* ---------------------------------------------------------------- chart
   The SVG holds lines only (non-scaling strokes, so 1px at any width); labels are HTML around it.
   Points are zero-length paths with round caps, so the dot stays 6px at every scale. */

.chart { position: relative; margin: var(--s4) 0 0; }

.chart-plot { position: relative; padding-right: 40px; }

/* ---------------------------------------------------------------- reading the chart
   Danny, 22 September 2026: "what about when you hover over the chart, I feel it could be way more
   dynamic and sophisticated."

   Three pieces, all built by app.js and none of them in the page without it: a hairline down the
   day being read, that day's point filled, and a chip with the date and the number. The chart
   underneath is untouched, which is the point: this is a way of reading it, not a second chart.

   MOTION: the guide, the dot and the chip are moved with `transform`, which is the third documented
   exception to the opacity-only rule in this file, after dragging and the segmented marker. A guide
   that jumped between days instead of travelling to them would read as a flicker rather than as a
   thing being followed. Everything appearing and disappearing is still opacity, and under
   prefers-reduced-motion the travel is dropped and only the fade is left. */
.chart-guide, .chart-hit, .chart-read {
  position: absolute;
  left: 0;
  top: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--fade), transform 90ms linear;
}
.is-reading > .chart-guide, .is-reading > .chart-hit, .is-reading > .chart-read { opacity: 1; }

/* The hairline. Its height and top are set by app.js from the plot it is in, so it is exactly as
   tall as the chart whatever the page has given it. */
.chart-guide {
  width: 1px;
  margin-left: -0.5px;
  background: var(--rule-2);
}

/* The day being read, filled, over the hollow or solid point already drawn there. */
.chart-hit {
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 50%;
  background: var(--ink);
}

/* The chip. Solid, so the line never reads through it, and a hairline so it belongs to the same
   drawing as everything else. It sits above the plot rather than over the numbers. */
.chart-read {
  /* `top` is set by app.js, which puts the chip in whichever half of the chart the point is not in.
     Only the sideways half-step is here. */
  translate: -50% 0;
  white-space: nowrap;
  padding: 5px var(--s2);
  border: 1px solid var(--rule-2);
  background: var(--bg);
  color: var(--ink);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .06em;
  line-height: 1.2;
}

/* A chart being read is a control: it says so when it has the keyboard, and the arrow keys walk
   the days. */
.chart-plot:focus-visible { outline: 1px solid var(--rule-2); outline-offset: 4px; }
.chart-plot { touch-action: pan-y; }

@media (prefers-reduced-motion: reduce) {
  .chart-guide, .chart-hit, .chart-read { transition: opacity var(--fade); }
}

.chart svg {
  display: block;
  width: 100%;
  height: 140px;
  overflow: visible;
}

@media (min-width: 600px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (min-width: 480px) {
  .chart svg { height: 200px; }
}

.chart .grid { stroke: var(--rule); stroke-width: 1; vector-effect: non-scaling-stroke; }
.chart .base { stroke: var(--rule-2); stroke-width: 1; vector-effect: non-scaling-stroke; }
.chart .line { stroke: var(--ink); stroke-width: 1; fill: none; vector-effect: non-scaling-stroke; stroke-linejoin: round; }
.chart .line.synthetic { stroke-dasharray: 2 3; }
.chart .pt { stroke: var(--ink); stroke-width: 6; stroke-linecap: round; fill: none; vector-effect: non-scaling-stroke; }
.chart .pt-hollow { stroke: var(--ink); stroke-width: 6; stroke-linecap: round; fill: none; vector-effect: non-scaling-stroke; }
.chart .pt-hollow-in { stroke: var(--bg); stroke-width: 4; stroke-linecap: round; fill: none; vector-effect: non-scaling-stroke; }

.chart-y {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 36px;
  pointer-events: none;
  font-size: var(--fs-label);
  font-weight: 500;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.chart-y span { position: absolute; right: 0; transform: translateY(-50%); }
.chart-y .y0 { top: 0; }
.chart-y .y1 { top: 33.333%; }
.chart-y .y2 { top: 66.667%; }
.chart-y .y3 { top: 100%; }

.chart-x {
  display: flex;
  justify-content: space-between;
  padding-right: 40px;
  margin-top: var(--s2);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}


.chart.is-swapping { opacity: 0; transition: opacity 150ms; }
.chart { transition: opacity var(--fade); }

/* ---------------------------------------------------------------- buttons */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 20px;
  border: 1px solid var(--ink);
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  line-height: 1;
  cursor: pointer;
  transition: opacity var(--fade);
  -webkit-appearance: none;
  appearance: none;
}

.btn:active { opacity: .6; }

.btn-primary { background: var(--ink); color: var(--bg); }

.btn:disabled, .btn[aria-disabled="true"] {
  border-color: var(--ink-off);
  color: var(--ink-off);
  cursor: default;
}

.btn-primary:disabled { background: var(--ink-off); border-color: transparent; color: var(--bg); }

.btn-block { display: flex; width: 100%; }

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-top: var(--s5);
}

.actions-inline { margin-top: var(--s3); }

/* The SITE tab's Update request button, first thing under the tab name (Danny, 9 September 2026).
   The h1 label already carries 12px under it, so this sits closer than a mid-page action row and
   keeps its own air above the site card. */
.actions-top { margin-top: var(--s2); margin-bottom: var(--s5); }

/* The SITE tab's two actions were briefly built into a band here, to match the switch on the SEO
   and Socials tabs. They are a real switch now (Danny, 21 September 2026: "so the two tabs at the
   top are WEBSITE and TRAFFIC"), so the band is gone and .seg does the work on all three tabs. */

/* A tile with more behind it (Danny, 9 September 2026: Visitors and Page views open their own page).
   It is the same box, and it says it is a way in by lifting on hover, not by looking different. */
a.tile-link { text-decoration: none; color: inherit; transition: opacity var(--fade); }
a.tile-link:hover { opacity: .82; }
a.tile-link .num { color: var(--ink); }

/* ---------------------------------------------------------------- history
   Danny, 9 September 2026: deployments, the update history and the activity fold away behind one
   line, closed until it is asked for, so the tab is the site, the update and the numbers and the
   record is there when it is wanted. A details element, so it opens with no JavaScript. */

/* No border-top: the fold takes the 48px section mark like every other section (::before, above),
   and the two together drew a doubled band one pixel apart. */
.history { margin-top: var(--s7); }

.history-btn {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  min-height: 44px;
  padding: var(--s3) 0 0;
  cursor: pointer;
  list-style: none;
}
.history-btn::-webkit-details-marker { display: none; }
.history-btn::marker { content: ""; }
.history-btn .label { margin: 0; }
.history-btn:hover .label, .history[open] .history-btn .label { color: var(--ink); }

.history-note {
  font-size: var(--fs-small);
  color: var(--ink-2);
  margin-right: auto;
}
.history-chev { flex: 0 0 auto; align-self: center; color: var(--ink-2); }
.history[open] .history-chev { transform: rotate(180deg); color: var(--ink); }

/* Inside it the sections keep their own rhythm, the first one tight under the line it opened. */
.history-body { animation: switcher-in .2s ease-out; }
.history-body > .sec:first-child { margin-top: var(--s5); }

/* The record, set small (Danny, 22 September 2026: "make all the history text much smaller, its
   quite big all of these elements"). It was at body size, which gave a list nobody opens twice the
   same weight as the thing the page is for. Small for the event, smaller again for the date and the
   kind, and the rows close up to match: forty of them at 44px each is a scroll, and every one of
   them is one line of text. */
.history-body .list-row { min-height: 0; padding: var(--s2) 0; }
.history-body .row-name { font-size: var(--fs-small); line-height: 1.45; }
.history-body .row-meta, .history-body .row-sub { font-size: var(--fs-label); letter-spacing: .04em; }
.history-body .label { font-size: var(--fs-label); }
/* A row that holds buttons keeps the 44px a finger needs; only the reading rows give ground. */
.history-body .list-row:has(.actions-inline) { min-height: 44px; }

/* The buttons on an update in the history: text only, no outline. It has been dealt with, so its
   controls stop competing with the ones above that still want an answer. */
.list-row > .actions-inline .btn-text { color: var(--ink-2); }
.list-row > .actions-inline .btn-text:hover { color: var(--ink); }

/* ---------------------------------------------------------------- the update request sheet
   As many requests in one submit as the client likes (Danny, 9 September 2026). Each is a fieldset
   with a hairline down its left edge, so a long sheet reads as a stack of separate asks rather than
   one wall of fields. */

.req {
  border: 0;
  border-left: 1px solid var(--rule);
  margin: var(--s5) 0 0;
  padding: 0 0 0 var(--s4);
}
.req > legend {
  padding: 0;
  margin-bottom: var(--s3);
  color: var(--ink-2);
}
.req .field:first-of-type { margin-top: 0; }

/* "Which page" at one end and "Add new page" at the other, on one line at every width: the label
   keeps its baseline and the chip sits hard right. */
.field-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
}
.field-head label { margin-bottom: 0; }

/* Global and Add new page: a tick box drawn as a small outline control, so it reads as something to
   click and still is a checkbox with JavaScript off.
   The selector is doubled on purpose: these are labels inside a .field, and `.field label` further
   down the file sets display: block. `label.chip` alone weighs the same and loses on source order,
   which stretched the control to the column width and let its text run out of its own border, so
   `.field label.chip` carries the box and the bare one covers a chip used anywhere else. */
label.chip, .field label.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 44px;
  padding: 0 var(--s3);
  border: 1px solid var(--rule);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  cursor: pointer;
}
label.chip:hover, .field label.chip:hover { color: var(--ink); border-color: var(--rule-2); }
label.chip:has(input:checked), .field label.chip:has(input:checked) { color: var(--ink); border-color: var(--ink); }
.field > label.chip { margin-top: var(--s2); }
/* Never squeezed by the label beside it, and never stretched by the column: the chip is its words. */
label.chip, .field label.chip { flex: 0 0 auto; align-self: flex-start; white-space: nowrap; margin-bottom: 0; }
label.chip > span, .field label.chip > span { display: inline; }

/* The page picker on the update request sheet (Danny, 9 September 2026). The same disclosure the
   header switcher uses, for the same reason: a native dropdown draws the browser's own grey menu
   over a black app. A radio per page, so it works with JavaScript off; app.js only keeps the
   summary and the View page link in step with the choice. */
.picker-row { display: flex; align-items: flex-start; gap: var(--s3); flex-wrap: wrap; }
.picker { position: relative; flex: 1 1 12rem; min-width: 0; }

.picker-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  min-height: 44px;
  padding: 0 var(--s3);
  border: 1px solid var(--rule);
  color: var(--ink);
  font-size: var(--fs-input);
  cursor: pointer;
  list-style: none;
}
.picker-btn::-webkit-details-marker { display: none; }
.picker-btn::marker { content: ""; }
.picker-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-chev { flex: 0 0 auto; color: var(--ink-2); }
.picker[open] .picker-chev { transform: rotate(180deg); color: var(--ink); }
.picker[open] .picker-btn { border-color: var(--ink); }

.picker-panel {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  z-index: 8;
  background: var(--bg);
  border: 1px solid var(--rule-2);
  border-top: 0;
  max-height: calc(46vh / var(--z));
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  animation: switcher-in .2s ease-out;
}

/* Doubled selectors again: these rows are labels inside a .field, and `.field label` sets block,
   uppercase and the small label size, which stacked the dot above the name and shouted every page
   title. `.field label.picker-opt` is what makes a row a row. */
label.picker-opt, .field label.picker-opt {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: 52px;
  padding: var(--s2) var(--s3);
  border-top: 1px solid var(--rule);
  color: var(--ink-2);
  cursor: pointer;
  font-size: var(--fs-body);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  margin: 0;
}
.picker-panel > label.picker-opt:first-child { border-top: 0; }
label.picker-opt:hover, label.picker-opt:has(input:checked) { color: var(--ink); }
/* The radio itself is opacity:0, so a focus ring drawn on it is invisible: the row wears it. */
label.picker-opt:has(input:focus-visible) { color: var(--ink); outline: 1px solid var(--ink); outline-offset: -1px; }
label.picker-opt input { position: absolute; opacity: 0; pointer-events: none; }
label.picker-opt .dot { margin-right: 0; flex: 0 0 auto; }
.picker-opt-text { display: flex; flex-direction: column; min-width: 0; }
.picker-opt-name { font-size: var(--fs-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-opt-sub { font-size: var(--fs-small); color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The out to the real page, beside the picker, so they can check they mean this one. It is hidden
   until a page is chosen, and .btn's own display: inline-flex would otherwise beat the hidden
   attribute's UA rule, so the button says it here. */
.view-page { flex: 0 0 auto; }
.btn[hidden], .view-page[hidden] { display: none; }
.newpage-name { margin-top: var(--s3); }

/* The plus button that adds another request. Icon and label in one control, 44px tall like every
   other button, and a plain submit so the sheet grows with no JavaScript at all. */
.btn-icon { gap: var(--s2); padding: 0 var(--s4); }
.btn-icon svg { flex: 0 0 auto; }
.actions-add { margin-top: var(--s5); }

/* Text-only action, still 44px tall. */
.btn-text {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink-2);
  font-family: var(--font);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: underline;
  text-decoration-color: var(--rule-2);
  text-underline-offset: 4px;
  cursor: pointer;
}

.btn-text:hover { color: var(--ink); }

/* ---------------------------------------------------------------- forms
   Bottom-border inputs at --rule-2 (so the line survives sunlight), 44px tall, 16px text. */

.form { margin-top: var(--s4); }

.field { margin-top: var(--s4); }
.field:first-child { margin-top: 0; }

.field label, .field legend {
  display: block;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin-bottom: var(--s1);
  padding: 0;
}

.field label .muted { text-transform: none; letter-spacing: 0; }

.field:focus-within > label, .field:focus-within > legend { color: var(--ink); }

input[type="text"], input[type="email"], input[type="password"], input[type="url"], input[type="number"],
input[type="search"], input[type="date"], input[type="datetime-local"], input[type="file"], textarea, select {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: var(--s2) 0;
  border: 0;
  border-bottom: 1px solid var(--rule-2);
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-input);
  font-weight: 400;
  line-height: 1.4;
  -webkit-appearance: none;
  appearance: none;
}

textarea { min-height: 96px; resize: vertical; }

input:focus, textarea:focus, select:focus { border-bottom-color: var(--ink); outline: 0; }
input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 1px solid var(--ink); outline-offset: 2px; }

input::placeholder, textarea::placeholder { color: var(--ink-2); opacity: 1; }

input:disabled, textarea:disabled, select:disabled { color: var(--ink-off); border-bottom-color: var(--rule); }

.has-err input, .has-err textarea, .has-err select { border-bottom-color: var(--ink); }

select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='1'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0 center;
  background-size: 16px 16px;
  padding-right: 24px;
}

fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }

.choices .choice {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: var(--row-min);
  padding: var(--s2) 0;
  border-top: 1px solid var(--rule);
  cursor: pointer;
  font-size: var(--fs-body);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
  margin: 0;
}

.choices .choice:last-of-type { border-bottom: 1px solid var(--rule); }
.choice-name { flex: 1 1 auto; }
/* The price column stays one line (the note under the plan name wraps instead), so the three
   prices in a group line up. */
.choice-price { color: var(--ink-2); font-variant-numeric: tabular-nums; white-space: nowrap; flex: 0 0 auto; }
/* What the plan's bar carries (BRIEF 5.7), one small line under the plan name. */
.choice-note { display: block; font-size: var(--fs-small); color: var(--ink-2); }

input[type="radio"], input[type="checkbox"] {
  -webkit-appearance: none;
  appearance: none;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin: 0;
  border: 1px solid var(--ink);
  border-radius: 0;
  background: transparent;
  display: inline-grid;
  place-content: center;
  cursor: pointer;
}

input[type="radio"]::before, input[type="checkbox"]::before {
  content: "";
  width: 8px;
  height: 8px;
  background: var(--ink);
  opacity: 0;
  transition: opacity var(--fade);
}

input[type="radio"]:checked::before, input[type="checkbox"]:checked::before { opacity: 1; }

/* A single choice reads as one: radios are hollow circles with a round fill (the app's own status
   vocabulary); checkboxes stay square. */
input[type="radio"], input[type="radio"]::before { border-radius: 50%; }

/* Doubled selector for the same reason as .chip and .picker-opt above: a tick inside a .field is a
   label, and `.field label` shouts it in uppercase with letter spacing. A sentence a client is
   agreeing to is a sentence, not a field title. */
.check, .field label.check {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 44px;
  font-size: var(--fs-body);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink);
}

/* Two fields side by side from 600px, stacked below. The row carries the 16px rhythm itself (and
   its grid gap is 16px), so field-to-field spacing is the same at every width and whether or not
   a field is the first in its row. */
.form-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s4) var(--s5);
  margin-top: var(--s4);
}

.form-row:first-child { margin-top: 0; }
.form-row .field, .form-row .field:first-child { margin-top: 0; }

@media (min-width: 600px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (min-width: 480px) {
  .form-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------------------------------------------------------------- empty states */

.empty { padding-top: var(--s2); }
.empty .h2 { margin-bottom: var(--s2); }
.empty .actions { margin-top: var(--s4); }

/* ---------------------------------------------------------------- tables */

.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0 calc(var(--gutter) * -1);
  padding: 0 var(--gutter);
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-body);
}

.table th {
  text-align: left;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  padding: 0 var(--s3) var(--s2) 0;
  border-bottom: 1px solid var(--rule-2);
  white-space: nowrap;
}

/* Cells break between words only: .col's overflow-wrap: anywhere would otherwise split "Completed"
   mid-letter when a four-column table is squeezed at 360 (.cell-sub keeps its own anywhere for a
   long ref). The status cell never wraps, so a narrow table scrolls inside .table-wrap instead. */
.table td {
  padding: var(--s3) var(--s3) var(--s3) 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
  min-height: var(--row-min);
  overflow-wrap: normal;
}

.table th:last-child, .table td:last-child { padding-right: 0; }
.table .num-cell, .table td.num-cell, .table th.num-cell { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.table .status-cell { white-space: nowrap; }
.table .when-cell { white-space: nowrap; }
.table .actions-cell { white-space: nowrap; }
.table td .dot { margin-right: var(--s2); }

.table-admin td { font-size: var(--fs-small); }
.table-admin .btn, .table .btn { min-height: 44px; padding: 0 var(--s3); }

/* ---------------------------------------------------------------- socials cards */

.cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s3);
}

@media (min-width: 600px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (min-width: 480px) {
  .cards { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s4); }
}

/* ---------------------------------------------------------------- the schedule canvas
   Danny, 20 September 2026: "make the schedule tab look better, it should simply look like an
   Instagram canvas, and when you click on it it should take you into that post". So: squares, three
   across, hairline gutters, no words on them. Everything a card used to say is on the post page. */
/* A form that fills its half (Danny, 20 September 2026: "it should fill the page, and the what is it
   should be a description and should be a box, and then the library should be a full sized button
   below that box"). The description box takes whatever room is left, so the half ends at the foot of
   the screen instead of stopping a third of the way down it. */
.fit .view > .sec-fill { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.fit .sec-fill > .form { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.fit .sec-fill .field-grow { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
/* An actual box, not the hairline every other field is: he asked for a box and a description is the
   one field here you write more than four words into, so it should look like there is room for them. */
.fit .sec-fill .field-grow textarea {
  flex: 1 1 auto;
  min-height: 92px;
  resize: none;
  border: 1px solid var(--rule);
  padding: var(--s2);
}
.fit .sec-fill .field-grow textarea:focus { border-color: var(--ink); }
.fit .sec-fill .actions { margin-top: var(--s2); }

/* A button that owns its line rather than sitting in a row of them. */
.btn-wide { width: 100%; justify-content: center; }
.actions-stack { flex-direction: column; align-items: stretch; gap: var(--s2); }
.actions-stack .hint { text-align: center; }

/* ---------------------------------------------------------------- the two pages behind a tile
   (Danny, 21 September 2026: "please tidy up those pages globally, they should all fit on one page
   without having to scroll, including the edit page")

   Both are laid out from fixed heights rather than from their content, because content is the one
   thing that varies: a portrait photograph, a two line caption, a four line one. Fixed heights are
   what make every post page in the app identical and what keep the buttons on the screen. */

/* Words that cannot push the page over, and that always end on a whole line.
   Two of them, with the ellipsis the clamp draws, and the whole caption one tap away on Edit where
   it is a box you can read and change rather than a paragraph you cannot. A flexible height was
   tried first and thrown away: it fits just as well, but it cuts the last line through the middle
   of the letters, which looks like a rendering fault rather than a decision. */
.clamp-2 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  /* Always exactly two lines tall, a one word caption included. Danny, 21 September 2026: "set the
     page up so no matter what the sizing of the image is, the stuff below it, going out, where it
     goes, is always in the same place." A clamp alone only stops long text pushing down; a height
     is what stops short text pulling up, and between the two the rows under the picture sit on the
     same line on every post in the app. */
  /* Two of THIS element's own lines. `lh` is the unit for exactly that, and it is the only way to
     get it right here: .profile-text sets its own 20px type and 27px line height, so any height
     worked out from the body font clipped the second line through the letters. */
  height: calc(2 * 1.35em);
  height: 2lh;
}

/* Everything on the post page is a fixed height except the words, which take whatever is left.
   That is what makes the page fit whatever it is carrying: a notice at the top, a caption of two
   hundred characters, a line saying an account is not connected yet. The picture never changes
   size, and the buttons never leave the screen. */
/* The post page fills the screen it is on (Danny, 21 September 2026: "could you spread everything
   out a bit so it fits the page, it's all sat at the top of the page and there is so much dead
   space at the bottom and it looks a bit naff").

   Height, not min-height, so the column is exactly the screen and the one flexible thing in it has
   something to grow into. Everything except the picture keeps a fixed size: the picture takes
   whatever is left, which means a phone gets a modest band and a desktop gets a large one, and on
   any one screen every post in the app is laid out identically, which is the thing he asked for
   the time before this. */
/* A real gutter under the last button (Danny, 21 September 2026: "bring the elements below the
   photo up a bit so the button Remove doesn't sit quite so low to the bottom"). It comes out of the
   picture, which is the flexible thing on the page and has it to spare.

   min-height, not height. A hard height cannot be exceeded, so on the one screen where everything
   at its smallest still does not fit (a short phone carrying a notice as well) the words were
   pushed out of their own box and printed over the row beneath them. A floor lets the picture grow
   into the screen exactly the same way, and lets the page scroll in the one case where it must,
   which is a far better failure than text on top of text. */
.col.fit-post { min-height: 100%; padding-top: var(--s2); padding-bottom: var(--s5); }
.fit .sec-shot { margin-top: var(--s3); }
/* No `min-height: 0` here on purpose. A flex item's automatic floor is its own content, and that
   floor is what stops this section being squeezed to less than the picture's minimum plus the two
   lines of words: without it the words were pushed out of the box and printed over the row under
   them. The picture inside is the only thing allowed to give. */
.fit-post .sec-shot { flex: 1 1 0; display: flex; flex-direction: column; }
/* `flex: 1 1 0`, never `auto`. With an auto basis the box starts at the photograph's own height and
   only the leftover space is shared, so a tall picture ended up with a taller box than a wide one
   and the rows under it moved from post to post. A zero basis makes every box start from nothing
   and take the same share, so the band is the same height on every post on a given screen. */
.fit-post .sec-shot > .shot { flex: 1 1 0; height: auto; min-height: 120px; }
.fit .sec-shot .profile-text { margin-top: var(--s3); flex: 0 0 auto; }

/* Three buttons stacked and full width, not a row of small ones. They are the only things on the
   page to do, and at the foot of a tall screen a row of narrow boxes reads as an afterthought. */
.fit-post .sec-foot .actions { flex-direction: column; align-items: stretch; gap: var(--s2); }
.fit-post .sec-foot .actions > form { display: flex; }
.fit-post .sec-foot .actions .btn { width: 100%; min-height: 52px; justify-content: center; }
/* No epigraph on the two pages that are about one piece. "We create your assets" is a line for a
   page somebody is reading; here it is forty pixels between the buttons and the foot of a screen
   that has to hold everything. */
.col.fit-post > .folio, .col.fit-edit > .folio, .col.fit-crop > .folio { display: none; }
/* The buttons sit directly under "Where it goes" (Danny, 21 September 2026), not pushed to the foot
   of the screen: they belong to what is above them, and a thumb reaching for Edit should not have to
   travel past a band of nothing to find it. Doubled class, because `.fit .sec` sets a margin-top of
   its own further down this file and would otherwise win on document order. */
.fit .sec.sec-foot { margin-top: var(--s4); }

/* The edit page: a head, three controls and a calendar, all in one screen, with room for a six
   week month. Every number here was measured at 375 by 812 rather than guessed. */
/* The edit page fills its screen the same way the post page does (Danny, 21 September 2026: "do
   the same to the edit page, the image on it can be bigger next to the save button, the calendar
   lower down, same treatment").

   min-height, not height: a floor gives the flexible parts something to grow into and still lets
   the one impossible screen scroll rather than print one thing over another. Same lesson as the
   post page, learned the same way.

   The sizes below are all `clamp`, so they scale with the screen without a media query for each
   one: small on a phone, generous on a desktop, and never past a size that would look silly. */
.col.fit-edit { min-height: 100%; padding-top: var(--s2); padding-bottom: var(--s4); }
.col.fit-edit .edit-form { flex: 1 1 auto; min-height: 0; }
.fit-edit .edit-top { display: flex; align-items: center; gap: var(--s3); }
.fit-edit .edit-top .pagetop { flex: 1 1 auto; min-width: 0; margin: 0; }
.fit-edit .edit-top .btn { flex: 0 0 auto; min-height: clamp(44px, calc(6vh / var(--z)), 60px); }
/* Bigger beside Save where there is room for it: on a desktop a 44px square next to a 60px button
   reads as a stray icon rather than as the picture being worked on. */
.fit-edit .shot-thumb { width: clamp(44px, calc(7vh / var(--z)), 88px); height: clamp(44px, calc(7vh / var(--z)), 88px); }
.fit-edit .form > .field { margin-top: var(--s2); }
.fit-edit .field > label, .fit-edit fieldset.field > legend { margin-bottom: var(--s1); }
.fit-edit #caption { min-height: 0; height: clamp(56px, calc(12vh / var(--z)), 132px); resize: none; }
/* A piece still in the library has no day and no destinations, so the form is one box. It takes
   the whole screen rather than leaving five hundred pixels of black under a single field. */
.col.fit-edit .form-library { flex: 1 1 auto; min-height: 0; }
.col.fit-edit .form-library > .field { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.col.fit-edit .form-library #caption { flex: 1 1 auto; height: auto; min-height: 96px; }
.col.fit-edit .form-library .hint { flex: 0 0 auto; }

/* Square days, which is what a month looks like. Sizing the rows as `1fr` of the leftover space
   was tried first and thrown away: on a tall screen it gave 145px tall cells in a 103px wide
   column, and a grid of portrait boxes does not read as a calendar. A square scales by itself,
   53px on a phone and 103px on a desktop, and clears the 44px tap target at both.

   The description takes the slack instead, which is the better home for it: it is the one thing on
   this page somebody is actually writing into. */
.col.fit-edit .cal { grid-template-rows: auto; grid-auto-rows: auto; }
.col.fit-edit .cal-day, .col.fit-edit .cal-pad { aspect-ratio: 1 / 1; }
.col.fit-edit .cal-box { height: 100%; }
/* Nothing on this page is stretched to soak up the leftover room. Giving it all to the description
   was tried and looks worse than the gap it was meant to fix: a 365px empty box on a page whose
   longest caption is three lines. The slack is spread BETWEEN the blocks instead, which is what
   "spread everything out" actually means, and every control keeps a size that suits it. */
.col.fit-edit #calendar { flex: 0 0 auto; }
.col.fit-edit .edit-form { justify-content: space-between; }
.fit-edit .cal-head { margin-bottom: var(--s1); }
.fit-edit .cal-wd { padding-bottom: 0; }
.fit-edit .cal-none { margin-top: var(--s1); }
/* No epigraph on the three pages that are about one piece: it is a line for a page somebody is
   reading, and here it stands between the work and the foot of the screen. Declared with .col so it
   beats `.col > .folio` above, which is more specific than a bare class. */

/* The set up form on the connect page. Two blocks, one per registration, and each needs air above
   its title or the second reads as a stray letter under the first block's hint. */
.form > .label { margin-top: var(--s6); margin-bottom: var(--s2); }
.form > .label:first-of-type { margin-top: var(--s4); }
.form > .label + .hint { margin-top: 0; margin-bottom: var(--s4); }

/* A short screen eases the generous sizing rather than breaking the page. A phone carrying a
   notice as well is the one case where 52px buttons and a 24px gutter do not both fit, and the
   thing to give up there is a few pixels of button, never a legible layout. 46 is still well over
   the 44 the whole app is built to. */
@media (hover: none) and (max-height: 860px), (pointer: coarse) and (max-height: 860px), (any-pointer: coarse) and (max-height: 860px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (max-height: 688px) {
  .col.fit-post { padding-bottom: var(--s4); }
  .fit-post .sec-foot .actions .btn { min-height: 46px; }

  /* And the edit page goes back to the sizes that were measured to fit a six week month on a
     phone. Square days are 53px at this width, nine more per row than the 44 the page was built
     around, which is fifty four pixels a month grid cannot spare here. Square is a nicety for a
     screen with the room; the month fitting at all is not. */
  .col.fit-edit { padding-bottom: var(--s1); }
  .col.fit-edit .cal-day, .col.fit-edit .cal-pad { aspect-ratio: auto; }
  .col.fit-edit .cal { grid-auto-rows: 44px; }
  .fit-edit #caption { height: 56px; }
  .fit-edit .shot-thumb { width: 44px; height: 44px; }
  .fit-edit .edit-top .btn { min-height: 44px; }
}

/* ---------------------------------------------------------------- the lock, and moving the grid
   (Danny, 21 September 2026: "a little toggle, like a lock toggle... the user should be able to
   hold down on the image and drag them, reorganising the grid, and with it changing the release
   schedule of each image")

   The toggle sits at the right of the Connect line, which puts it over the first tile and under
   the switch, exactly where he asked for it. */
.lock {
  margin-left: auto;
  flex: 0 0 auto;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 44px;
  padding: 0 var(--s3);
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  cursor: pointer;
  text-decoration: none;
  transition: color var(--fade), border-color var(--fade);
}
/* display: inline-flex above out specifies the user agent's `[hidden] { display: none }`, which is
   the same trap `.btn[hidden]` already exists for further up this file. Without this line the lock
   would still be drawn on the upload half after the switch hid it. */
.lock[hidden] { display: none; }
.lock:hover { color: var(--ink); border-color: var(--rule-2); }
.lock.is-open { color: var(--ink); border-color: var(--ink); }
.arrange-hint { margin-top: var(--s2); margin-bottom: var(--s3); }

/* Unlocking changes nothing about the shape of the grid (Danny, 21 September 2026: "the dates
   should be on the images not below them, it should not change the shape of the grid, the grid
   should stay as it is, just have the dates appear on the images").

   So the tile is the same square it always was and everything sits on the photograph. A first
   attempt put the day in a solid black band flush with the top edge, which read as a caption for
   the tile ABOVE it; the fix is the convention every photo app already uses, which is a scrim.
   The day is dropped onto the foot of the picture over a gradient that fades into it, so it is
   unmistakably part of that square and the photograph still reads through. */
.tile-day {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: var(--s1) 2px calc(var(--s1) + 1px);
  background: linear-gradient(to top, rgba(0, 0, 0, .88) 40%, transparent);
  color: var(--ink);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .06em;
  text-align: center;
  white-space: nowrap;
  pointer-events: none;
  z-index: 1;
}

.canvas-tile { position: relative; cursor: grab; touch-action: none; }
.canvas-tile.is-held { cursor: default; opacity: .45; }

/* The two arrows. They are the whole feature without JavaScript, and the keyboard's only way in,
   so they are real 44px targets and are always there while the lock is open. */
/* The arrows sit on the picture as well, at the top so the day keeps the foot to itself. Still
   real 44px targets: they are the whole feature with JavaScript off and the only way in from a
   keyboard, so they are not allowed to shrink to suit a layout. */
.tile-move { position: absolute; left: 0; right: 0; top: 0; display: flex; z-index: 1; }
.tile-arrow {
  flex: 1 1 0;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: linear-gradient(to bottom, rgba(0, 0, 0, .8) 40%, transparent);
  color: var(--ink);
  cursor: pointer;
  transition: opacity var(--fade);
  opacity: .75;
}
.tile-arrow:hover { opacity: 1; }
.tile-arrow:focus-visible { outline: 1px solid var(--ink); outline-offset: -2px; }

/* Dragging. Two exceptions to the opacity-only rule, and both are the same exception: a picture
   following a finger is not decoration, it is the thing being done. The tile under the finger is
   lifted out of the flow with a transform and everything else simply re-flows behind it, which is
   what makes the grid look like it is rearranging itself. */
.canvas-tile.is-dragging {
  position: relative;
  z-index: 3;
  cursor: grabbing;
  opacity: .85;
  outline: 1px solid var(--ink);
  outline-offset: -1px;
}
.canvas-tile.is-target { outline: 1px dashed var(--rule-2); outline-offset: -1px; }
.js .canvas-move { user-select: none; }

/* ---------------------------------------------------------------- cropping by hand
   (Danny, 21 September 2026: "allow a crop button for the user to manually crop any images that
   seem to upload with this edge")

   The thumbnail on the edit page IS the button. Pressing the picture to work on the picture needs
   no label, and the head has no room for one beside Save. */
a.shot-thumb { position: relative; display: block; }
.shot-thumb .thumb-mark {
  position: absolute;
  right: 0;
  bottom: 0;
  display: inline-flex;
  padding: 2px;
  background: var(--bg);
  color: var(--ink);
}
a.shot-thumb:hover img { opacity: .75; }

.col.fit-crop { min-height: 100%; padding-top: var(--s2); padding-bottom: var(--s4); }
.crop-form { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }

/* The picture, and the part of it being kept. Without JavaScript this is simply the photograph:
   the shade and the grips are drawn by app.js and have no size until it sets one. */
.crop-stage {
  position: relative;
  flex: 1 1 0;
  min-height: 140px;
  margin-top: var(--s3);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  touch-action: none;
}
.crop-stage img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; }

/* Four shadows rather than one box: the part being cut away is dimmed, so what is left is the
   picture at full strength and the edge between them is the crop. */
.crop-shade {
  position: absolute;
  pointer-events: none;
  outline: 1px solid var(--ink);
  box-sizing: border-box;
}
.js .crop-shade { display: none; }
.js .crop-stage.is-ready .crop-shade { display: block; }

/* A handle on each edge. 44px of grab, drawn as a hairline so it reads as the edge itself. */
.crop-grip { position: absolute; display: none; }
.js .crop-stage.is-ready .crop-grip { display: block; }
.crop-grip-left, .crop-grip-right { width: 44px; cursor: ew-resize; }
.crop-grip-top, .crop-grip-bottom { height: 44px; cursor: ns-resize; }
.crop-grip::after {
  content: "";
  position: absolute;
  background: var(--ink);
}
.crop-grip-left::after, .crop-grip-right::after { top: 0; bottom: 0; left: 50%; width: 2px; }
.crop-grip-top::after, .crop-grip-bottom::after { left: 0; right: 0; top: 50%; height: 2px; }

.crop-size { margin-top: var(--s3); }
.crop-fields { display: flex; gap: var(--s2); }
.crop-field { flex: 1 1 0; display: flex; flex-direction: column; gap: var(--s1); }
.crop-field > span {
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.crop-field input { width: 100%; min-height: 44px; }
.crop-actions { margin-top: var(--s4); flex-direction: column; align-items: stretch; gap: var(--s2); }
.crop-actions .btn { width: 100%; justify-content: center; min-height: 48px; }

/* The epigraph on this tab is sticky, so anything that scrolls under it has to be able to scroll
   CLEAR of it (Danny, 21 September 2026: "when this opens up, it's a bit close to the bottom").
   The canvas already has a margin for this; the fold on the upload half did not, so its last row
   came to rest underneath the pinned line. The room is only needed once it is open. */
.fit.pin-folio .more[open] { padding-bottom: var(--s8); }

/* The Connect line under the two halves (Danny, 21 September 2026). Small on purpose: it is set up
   once and then never touched, so it must not read as a third half of the tab. */
/* The negative top margin here used to cancel the switch's own margin-bottom entirely, which was
   fine while this line was two words of text and wrong the moment it carried a 44px button: the
   lock sat flush against the UPLOAD tab (Danny, 21 September 2026: "the rearrange button is
   touching the upload button, separate it, it looks naff where it is"). */
.connect-line { display: flex; align-items: center; gap: var(--s3); margin-top: var(--s4); margin-bottom: var(--s4); }
/* Neither of these may be broken across lines by the column's overflow-wrap: "CONNE / CT" is not a
   word. The hint is the one that gives, and it wraps at spaces like prose. */
.connect-line > .btn-text { flex: 0 0 auto; white-space: nowrap; }
/* margin: 0 is the whole fix for the alignment (Danny, 21 September 2026: "the no accounts bit
   runs lower than CONNECT"). `.hint` carries an 8px top margin for its usual job of sitting under a
   field, and `align-items: center` centres a box INSIDE its margins, so that 8px pushed the words
   down by exactly half of it. Measured: the row's centre and CONNECT's were both at 173 and the
   hint's at 177. */
.connect-line > .hint { flex: 1 1 auto; min-width: 0; overflow-wrap: normal; margin: 0; }

/* ---------------------------------------------------------------- the edit page and its calendar
   (Danny, 21 September 2026: "when they click the date it should open a calendar that shows what
   date other posts are going out on too so they can see and move them around in there")

   A month of days, no JavaScript in any of it: each day is a radio inside its own label, so picking
   one is a form control and the whole thing works with scripting off.

   Save is FIRST in the document and last on the screen. A form's default button is the first submit
   button in tree order, and the month arrows are submit buttons: with them above Save, a client who
   tabs to a day and presses Enter would page the calendar instead of saving. `order` moves it back
   down without changing that. */
.edit-form { display: flex; flex-direction: column; }

/* A smaller look at the picture: on this page it is a reminder of which piece is being edited, not
   the subject of the page. */
/* The reminder of which piece is being edited, beside the heading rather than above the form: on
   the edit page the picture is not the subject, and a full band of it would push the calendar off
   the screen. */
.shot-thumb { flex: 0 0 auto; width: 44px; height: 44px; overflow: hidden; background: var(--bg); }
.shot-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.shot-thumb .asset-ph { height: 100%; font-size: 9px; }

/* The new page name box, hidden until "A new page" is the answer in the picker (Danny, 22 September
   2026). Only with scripting on: `.js` is set on <html> by app.js, so with it off the box is always
   on the page and the sheet works exactly as it did. */
.js .newpage-name[data-idle] { display: none; }
/* No `.js` prefix (hardening pass, 22 September 2026): with scripting off and nothing chosen yet
   the server renders data-idle, and the old rule left an empty bordered box under the picker. With
   scripting on nothing changes: app.js sets and clears data-idle exactly as before. */
.view-page[data-idle] { display: none; }

/* The Copy button's hit area is 44px whatever its drawn size (hardening pass, 22 September 2026).
   The drawn box stays 28px, as designed; the overlay is invisible and only catches the tap, on the
   one control whose miss costs a mistyped IP address. */
.copy { position: relative; }
.copy::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, 44px);
  height: max(100%, 44px);
  transform: translate(-50%, -50%);
}

/* The copy button on a value (the DNS records on the migration page). Built by app.js, so it is
   never on a page that cannot use it. Quiet: it sits beside the thing it copies and says what it
   did, which is the only feedback a copy can give. */
.copy {
  flex: 0 0 auto;
  margin-left: var(--s2);
  min-height: 28px;
  padding: 0 var(--s2);
  border: 1px solid var(--rule-2);
  background: transparent;
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--fade), border-color var(--fade);
}
.copy:hover { color: var(--ink); border-color: var(--ink); }
.copy.is-done { color: var(--ink); border-color: var(--ink); }

/* The chosen page's address, in a box under the picker. A link, not a line of text: it opens in a
   new tab so the page can be read while the request is being written. It belongs to the picker
   above it, so it sits close under it rather than a field's distance away. */
.fit-request .req-body > .view-page { margin-top: var(--s2); }
.card-view { padding: var(--s2) var(--s3); }
.view-link {
  display: block;
  font-size: var(--fs-small);
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------- the update request sheet, filled
   Danny, 22 September 2026: "I think we could fill the page more", then, at a bigger window,
   "you only made it bigger by about half a centimetre, there's loads of room."

   He was right and the first two attempts were not. The box was sized in `vh`, which is a share of
   the SCREEN; what it needed was everything the sheet does not want, which is a different number on
   every window. At 375 by 812 the two happen to agree and the page looked full, so the bug only
   showed on a taller one: the fixed parts stayed fixed, the box took a third of the extra height,
   and the rest became a band of black under the button.

   So it is flex, from the column all the way down to the textarea, and every link in that chain has
   `min-height: 0` because a flex item will not shrink below its content without it. The one thing
   that is not a flex container is the fieldset: laid out as one it put the free space in the wrong
   places and dropped the Files row on top of the box, which is what `.req-body` is for. */
/* The column may not grow past the screen. `.col` is `min-height: 100%` with no ceiling, so when
   the address box appeared the column simply got taller, the page scrolled, and not one of the
   flex items below was ever asked to give ground: the writing box sat at its five rows while the
   sheet ran off the bottom. With a ceiling the shortage is real and travels down the chain to the
   one thing that can absorb it. If it ever cannot, the page scrolls exactly as it did before. */
.col.fit-request { max-height: 100%; }
.fit-request .sec-first { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.fit-request .form { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
/* `display: contents`, so the fieldset is a grouping for a screen reader and nothing at all to the
   layout: `.req-body` becomes a direct child of the form and the height runs straight through. Laid
   out as a flex container instead, a fieldset keeps its own anonymous content box and the height
   stops there, which is why the box collapsed and the leftover pooled above the button. */
.fit-request .req { display: contents; }
.fit-request .req-body { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
/* Everything except the words is its own size. `:not(.field-words)` matters and is not tidiness:
   `.req-body > .field` is (0,3,0) and `.field-words` is (0,2,0), so without it the pinning rule
   beat the growing one whatever the order in the file, and the box stayed at five rows while the
   leftover pooled above the button. Third time this evening a more specific selector has quietly
   won an argument it should not have been in. */
.fit-request .req-body > .field:not(.field-words) { flex: 0 0 auto; }
/* A little air under it rather than every last pixel (Danny, 22 September 2026: "pull it back by
   about 10% on the sizing"). In vh so it stays a tenth-ish of the box at any window: 32px of a 254
   box on a phone, 44px of a 542 one on a tablet. The box is still what grows; this is the slice it
   hands back. */
.fit-request .field-words { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; margin-bottom: calc(4vh / var(--z)); }
.fit-request .card-words { flex: 1 1 auto; display: flex; min-height: 96px; padding: var(--s3); }
.fit-request .card-words textarea {
  /* `1 1 0`, not `auto`: with an auto basis the box starts at the five rows the markup asks for and
     will not go below them, so on a short screen with the address box showing the sheet overflowed
     instead of the one thing on it that can give. A zero basis leaves the floor to the card. */
  flex: 1 1 0;
  width: 100%;
  height: auto;
  min-height: 0;
  border: 0;
  padding: 0;
  resize: none;
}

/* Files and Send lifted off the foot (Danny, 22 September 2026: "the files and send the request
   button, it's all too close to the bottom"). They were hard against the epigraph, because the box
   above them was taking everything and leaving them pressed into the last inch of the screen. The
   room comes out of the box, which is the thing on this page that can spare it. */
/* Files sits up under the box (Danny, 22 September 2026: "I would move the files section up so it
   sits under the box a fair bit more"). The only gap between them now is the air the box itself
   hands back. */
.fit-request .req-body > .field:last-child { margin-top: 0; }
/* And a clear break before the button, so Files belongs to the form above it and Send is on its
   own ("and then have a bit of separation between send the request").

   9vh under it, not a fixed number: on a phone that is 73px and on a tablet 99, so the button sits
   the same distance off the foot whatever the window rather than being generous on one and mean on
   the other. Right aligned, which is where the thing that ends a form goes. */
/* `.col` on the front is not decoration: `.fit .actions { margin-top: var(--s3) }` lives further
   down this file at the same specificity, so it was winning on document order and the separation
   asked for here was coming out as twelve pixels. */
.col.fit-request .actions-lead {
  margin-top: var(--s7);
  margin-bottom: calc(9vh / var(--z));
  justify-content: flex-end;
}

.ticks { display: flex; flex-wrap: wrap; gap: var(--s2); }

.cal-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); margin-bottom: var(--s3); }
.cal-month { font-size: var(--fs-label); font-weight: 500; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2); }
.cal-step {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  color: var(--ink-2);
  cursor: pointer;
  transition: color var(--fade);
}
.cal-step:hover { color: var(--ink); }

/* Edge to edge, the same break out the canvas uses, and for the same reason: seven cells inside the
   column's gutter come to 44.9px at the 360px baseline, which clears the tap target by under a
   pixel and fails the moment anything gains padding. Cancelling the gutter gives 50.6px at 360 and
   53.4px at 375, with room to spare. */
.cal {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 1px;
  margin-left: calc(var(--gutter) * -1);
  margin-right: calc(var(--gutter) * -1);
}

.cal-wd { font-size: var(--fs-label); letter-spacing: .08em; color: var(--ink-2); text-align: center; padding-bottom: var(--s1); }
.cal-pad { min-height: 44px; }

/* Doubled selector, the fourth time this file has needed it: `.field label` renders any label
   inside a field at 11px, uppercase and letterspaced, which would turn the day numbers into tiny
   grey initials on a ragged grid. */
.cal-day, label.cal-day, .field label.cal-day {
  position: relative;
  display: block;
  margin: 0;
  font-size: var(--fs-body);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
  cursor: pointer;
}

/* Hidden the house way. `[hidden]` would not do it: input[type=radio] is display:inline-grid a few
   hundred lines up, which out specifies the user agent rule. */
.cal-day input { position: absolute; opacity: 0; pointer-events: none; }

.cal-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 44px;
  border: 1px solid var(--rule);
  transition: opacity var(--fade), border-color var(--fade);
}
.cal-box .dot { margin-right: 0; top: 0; }
.cal-day:hover .cal-box { border-color: var(--rule-2); }
.cal-day.is-today .cal-box { border-color: var(--rule-2); }

.cal-day input:checked + .cal-box { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.cal-day input:checked + .cal-box .dot { background: var(--bg); }
.cal-day input:checked + .cal-box .dot.hollow { background: transparent; border-color: var(--bg); }

/* Inset, because at a 1px gap the standard 2px offset paints over the neighbouring day. */
.cal-day input:focus-visible + .cal-box { outline: 1px solid var(--ink); outline-offset: -2px; }

/* A day that has gone. Not a control, and --ink-off is the token this file reserves for exactly
   that. The marks stay, so the month still reads as a whole. */
.cal-day.is-past, .field .cal-day.is-past { cursor: default; color: var(--ink-off); }
.cal-day.is-past .dot { background: var(--ink-off); }
.cal-day.is-past .dot.hollow { background: transparent; border-color: var(--ink-off); }

/* ---------------------------------------------------------------- SALES (Danny, 24 September 2026)

   Both halves are one list in the look the COMPLETED table had: the label head of `.table th` over
   hairline rows with `.table td`'s padding, on three columns. Each row is one link to the booking's
   own page, which a table row cannot be without JavaScript. The columns are sized off the body
   size rather than in em, so the 11px head and the rows under it line up. The dates never wrap; a
   long name wraps inside its own column. */
.bk-head, .bk-row {
  display: grid;
  grid-template-columns: calc(var(--fs-body) * 8.5) minmax(0, 1fr) auto;
  gap: 0 var(--s3);
  align-items: baseline;
}
.bk-head {
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--rule-2);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.bk-rows { margin: 0; padding: 0; list-style: none; }
.bk-row {
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--rule);
  color: var(--ink);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
.bk-row:hover .bk-who { text-decoration: underline; text-decoration-color: var(--rule-2); text-underline-offset: 3px; }
.bk-when, .bk-num { white-space: nowrap; }
.bk-num { text-align: right; }
.pagetop + .bk-dates { margin-top: var(--s4); }

/* One booking's page, closer set (Danny, 24 September 2026: "make all of the boxes smaller so
   there is room at the bottom for a delete booking button"). The lines at the small size with 5px
   over and under rather than 52px rows, the sections 16px apart rather than 32, so the fullest
   booking there is (fourteen lines, a note among them) and Delete booking all sit above the
   epigraph at 505 by 928; at 6px and 24px the button still sat 20px under it. The email and phone
   keep their 44px taps from the house pseudo-element, which draws nothing. */
.col.bk-page .sec:not(.sec-first) { margin-top: var(--s4); }
.col.bk-page .list-row { min-height: 0; padding: 5px 0; font-size: var(--fs-small); }
.col.bk-page .row-sub { font-size: var(--fs-small); }
.col.bk-page .bk-delete .btn { width: 100%; justify-content: center; }

/* The line under the switch after a delete: what went, and Undo at its end. */
.notice.bk-undo { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); margin: var(--s3) 0 0; padding: var(--s2) 0; font-size: var(--fs-small); }

/* The bottom half of the page (Danny, 24 September 2026: "could that be on the bottom half of the
   page please with a little heading that says Bookings above it", then SALES LIST on COMPLETED).
   The half on show takes the height between the switch and the epigraph; `.sales-top` is the
   first half (UPCOMING's calendar, empty on COMPLETED) and the section fills the second, its
   heading over the box and the box taking the rest. Two flex halves, so it is the bottom half at
   any window height; each keeps a floor, so a short window scrolls the page rather than crushing
   either. The selectors carry `.sec` to outrank `.pin-head .view > .sec:first-child`, which comes
   later in this file. */
.col.sales > .view:not([hidden]) {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
}
/* The same box on both halves, so the lists sit at the same height whichever is showing. Nothing
   on the box itself adds to its share: the calendar's own margins are inside it. Its floor is the
   calendar's, six rows the height of a line of small type. */
.col.sales > .view > .sales-top {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  min-height: 300px;
}
.col.sales > .view > .sec.sales-box {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  min-height: 224px;
  /* Clear of the epigraph: flush against it, the pinned band sat on the box's bottom edge and hid
     it (measured at 505: box and band both at 789.5). Then higher off the foot, twice (Danny, 24
     September 2026: "ever so slightly higher from the bottom of the page, take it up a bit more",
     then "even more"): 64px under the box. The second 32 is a lift, not more padding: the margin
     takes back what the padding adds, so the halves split the page exactly as before, the box keeps
     its height, and heading and box move up together. */
  margin-top: calc(var(--s6) * -1);
  padding-bottom: var(--s8);
}

/* Export CSV on SALES LIST's line at the label's own height, so that line is as tall as BOOKINGS
   and the box does not jump when the switch moves (measured at 505: .btn-text's 44px made it 44
   against 27, and the box sat 17px lower on COMPLETED). The 44px it needs to be tapped comes from
   a transparent pseudo-element, the house way, rather than from a height of its own. */
.sales-box .sec-head .btn-text { display: inline; min-height: 0; line-height: 1.4; position: relative; }
.sales-box .sec-head .btn-text::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, 44px);
  height: max(100%, 44px);
  transform: translate(-50%, -50%);
}

/* The list in its box (Danny, 24 September 2026: "contain those scrollable tables within half the
   screen"): it fills what the half leaves under the heading. The SITE tab's card is the frame.

   Only the rows move (Danny, the same day: "the box bounces around a lot. lock the total and the
   month booking revenue tab at the top so it's not all bouncing around on scroll"). The head, and
   a total's foot, are not in the scroller at all, so there is nothing for them to ride; they had
   been sticky inside a scrolling card, which carried them on its elastic overscroll. The rows'
   scroller has no overscroll either (none, the house rule for .scroll since 21 September), so a
   flick that reaches the end stops dead rather than springing back.

   The scroller runs the card's full width and every row, the head and the foot sit 12px in from
   its sides, so the scrollbar passes down that margin instead of over the amounts. The head and the
   foot reserve the same scrollbar gutter the rows do (scrollbar-gutter on a box that never scrolls
   reserves it all the same), so a scrollbar that takes room, a mouse user's, moves every column by
   the same amount and nothing falls out of line. .card.bk-list outranks .card-tight, further down. */
.card.bk-list {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;
}
.bk-list > .bk-head, .bk-list > .bk-foot {
  flex: 0 0 auto;
  margin: 0 var(--s3);
  overflow: hidden;
  scrollbar-gutter: stable;
}
.bk-list > .bk-head { padding-top: var(--s3); }
.bk-list > .bk-rows {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: none;
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: var(--rule-2) transparent;
}
.bk-list > .bk-rows:last-child { padding-bottom: var(--s3); }
.bk-list > .bk-rows > li { margin: 0 var(--s3); }
.bk-list .bk-rows > li:last-child > .bk-row { border-bottom: 0; }

/* The rows a picked day is for. The scroller starts under the head, so a row scrolled to its top
   is in full view with no margin to leave. */
.bk-row.is-picked { background: var(--rule); }

/* ---------------------------------------------------------------- SALES calendar (Danny, 24 September 2026)

   UPCOMING's top half: "the top space now add the calendar. but dont have it touching the sides it
   should be as wide as the bookings box". The BOOKINGS card, the same width, with 8px under the
   switch and 24px over the BOOKINGS heading (the 56 below is that plus the 32 the section is
   lifted by). Its own component rather than the SOCIALS picker's `.cal`, which runs edge to edge.
   Six rows that share whatever height the half leaves, so every month is the same size. A booked
   night is a filled cell and a free day a number alone (the day rules below say why). The days are
   shorter than the 44px the house gives a tap: a whole month has to fit the half. */
.sales-top > .scal {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin: var(--s2) 0 calc(var(--s6) + var(--s5));
}
/* COMPLETED's top half: this year's totals in the house tiles, two by two at every width (the
   tiles go four across from 600px elsewhere), with the calendar's margins, so the tiles and the
   month take the same box and the lists below sit level. The rows share the height the half
   leaves; the 96px floor a tile has elsewhere gives way to that. */
.sales-top > .sales-tiles {
  flex: 1 1 auto;
  min-height: 0;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr));
  margin: var(--s2) 0 calc(var(--s6) + var(--s5));
}
.sales-tiles > .tile { min-height: 0; }

/* Each tile opens its own page (Danny, 24 September 2026: "those four boxes should be clickable so
   you can go in and see details"). It keeps a tile's look rather than a link's: no underline, its
   own colours, a small chevron in the top corner to say it opens, and a shade lighter under the
   pointer. The shade is laid over black rather than being see-through, or the grid's hairline
   ground would show through the whole tile. */
a.tile { position: relative; color: var(--ink); text-decoration: none; transition: background-color var(--fade); }
a.tile:hover { background: linear-gradient(var(--fill-off), var(--fill-off)), var(--bg); }
a.tile:focus-visible { outline: 1px solid var(--ink); outline-offset: -2px; }
.tile-go { position: absolute; top: var(--s4); right: var(--s3); color: var(--ink-off); transition: color var(--fade); }
a.tile:hover .tile-go { color: var(--ink); }

/* A total's own page (Danny, 24 September 2026: "the same container boxes as the booking table so
   you can see every single month, and toggle between years too"). The booking table's box with the
   row's name on the left and the two figures right-aligned; every month fits, so the box is as tall
   as its rows. The Total row is the year: set in the heavier weight rather than under another rule,
   which would double the hairline the December row already draws. The year steps on the title's
   line, pushed to its end, with the calendar's arrows. */
/* The two figure columns are a fixed width, not auto (Danny, 24 September 2026: "look at the
   bookings numbers, they are all over the place"). Each row is a grid of its own, so an auto column
   is as wide as that row's figure: $12,660 pushed its row's 7 left of the next row's 5, and a month
   with nothing in it pushed its marks right. Six body widths holds $117,780 and the widest head. */
.bk-list.is-totals .bk-head, .bk-list.is-totals .bk-row {
  grid-template-columns: minmax(0, 1fr) calc(var(--fs-body) * 6) calc(var(--fs-body) * 6);
  gap: 0 var(--s3);
}
.bk-row.is-total { font-weight: 500; }

/* By channel on the page, not under the epigraph (Danny, 24 September 2026: "the bottom area by
   channel is falling off the bottom of the page. bring it higher please, by making the month by
   month a bit smaller"). The column gets a definite height, so the months' section can give way:
   it is as tall as its twelve months when the window has the room, and shorter when it has not,
   scrolling inside its box like the SALES lists, down to a floor where the page scrolls instead.
   The Total row is the box's foot, outside the scroller, so the year stays in sight and still,
   under the stronger rule. */
.col.totals { height: 100%; }
.col.totals > .sec.totals-months { flex: 0 1 auto; min-height: 224px; display: flex; flex-direction: column; }
.col.totals > .sec.totals-months > .card.bk-list { flex: 0 1 auto; min-height: 0; }
/* Clear of the epigraph, as on the SALES tab: flush against it, the pinned band sat on the last
   box's bottom edge (measured at 505: both at 790). The last section is By channel, or the months
   in a year with nothing in it. */
.col.totals > section:last-of-type { padding-bottom: var(--s4); }
.bk-list > .bk-foot { border-top: 1px solid var(--rule-2); }
.bk-foot > .bk-row.is-total { border-bottom: 0; padding-bottom: var(--s5); }
.yr { margin-left: auto; display: flex; align-items: center; gap: var(--s1); }
.yr-label { min-width: 4ch; text-align: center; font-size: var(--fs-label); font-weight: 500; letter-spacing: .12em; color: var(--ink-2); font-variant-numeric: tabular-nums; }

.scal-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; height: 28px; }
.scal-month { margin: 0; font-size: var(--fs-label); font-weight: 500; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2); }
.scal-step {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  color: var(--ink-2);
  text-decoration: none;
  transition: color var(--fade);
}
a.scal-step:hover { color: var(--ink); }
a.scal-step::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.scal-grid {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  grid-template-rows: auto repeat(6, minmax(0, 1fr));
  /* Rows 2px apart; days in a row touching, so a stay can run across them as one block. The 2px
     between two blocks side by side is the fill's own 1px inset on each (below). */
  gap: 2px 0;
  margin-top: var(--s2);
}
.scal-wd { text-align: center; font-size: var(--fs-label); letter-spacing: .08em; color: var(--ink-2); padding-bottom: var(--s1); }
/* Booked or free at a glance (Danny, 24 September 2026: "make it a bit more clearer visually of
   which days are booked and which days are not"), and one booking one block ("if someone has booked
   4 nights, those should be connected as one grey area"). A booked night is filled, its number at
   full strength; a free day is its number alone, set back at --ink-2. The fill is a layer under the
   number, 1px in from each side, and it closes that gap on a side where the night beside it is the
   same booking (is-join-prev, is-join-next: the server knows which, see calendar_view). So a stay is
   one grey run, and two stays that meet on a changeover stay two. A night gone is the faintest
   fill; today carries the brighter outline over it; the picked day is solid white. */
.scal-day {
  position: relative;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  color: var(--ink-2);
  font-size: var(--fs-small);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}
.scal-day::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 1px;
  right: 1px;
  z-index: -1;
  transition: background-color var(--fade);
}
.scal-day.is-join-prev::before { left: 0; }
.scal-day.is-join-next::before { right: 0; }
.scal-day.is-booked { color: var(--ink); }
.scal-day.is-booked::before { background: var(--rule); }
.scal-day.is-past, .scal-day.is-out { color: var(--ink-off); }
.scal-day.is-past.is-booked::before { background: var(--fill-off); }
.scal-day.is-today::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 1px;
  right: 1px;
  border: 1px solid var(--ink-2);
  pointer-events: none;
}
a.scal-day:hover::before { background: var(--rule-2); }
a.scal-day[aria-current="true"] { color: var(--bg); }
a.scal-day[aria-current="true"]::before { background: var(--ink); }
a.scal-day:focus-visible { outline: 1px solid var(--ink); outline-offset: -2px; }

.cal-none { margin-top: var(--s4); }

/* The epigraph pinned over a scrolling canvas (Danny, 20 September 2026: "keep 'We create your
   assets' locked where it is, against black, so the grid scroll falls nicely behind it"). Sticky
   rather than fixed, so at the foot of the page it simply stops where it would have sat anyway, and
   nothing is ever left permanently underneath it. The solid fill is what makes the grid pass behind
   it rather than through it, and the fade above softens the edge the photographs cross. */
/* The head is pinned as well as the foot (Danny, 21 September 2026: "the schedule upload and
   rearrange area should also be locked so the grid scrolls up underneath it, think that will feel
   much stronger").

   So the canvas runs between two black bands: the switch and the lock at the top, the epigraph at
   the bottom, and the photographs travel behind both. Same three pieces as the folio, for the same
   reasons: a solid fill so the grid passes behind it rather than through it, the gutter broken on
   both sides so the tiles cannot slip past either edge of the band, and a fade on the inner side
   so the edge the photographs cross is soft rather than a cut. */
/* SEO has the same switch bar at the top of it, so it gets the same pinned head, and so does SITE
   (Danny, 21 September 2026: "make these buttons on site match seo and socials tabs aswell", then
   "these dont match where they are on seo and socials, make sure they perfectly align with both of
   those"). SITE's column is not a .fit one, because the tab is a long page that scrolls rather
   than a screenful that does not, which is why the head rule below is written to work either way.
   SALES has had the same switch since 24 September 2026 and takes the same pinned head. CREATE
   does not get it: its first section is a heading with the tab's content under it, and pinning
   that would pin a block of the page rather than a bar. Its epigraph is pinned all the same. */
.col.fit.pin-folio, .col.pin-head { padding-top: 0; }

/* The first thing under a pinned head sits where it sits on the SEO tab (Danny, 22 September 2026:
   "move the whole page up so the top of the website area box aligns with that of the one on the SEO
   tab"). SEO's column is a .fit one, where every section takes an 8px gap; an ordinary column takes
   32, so the SITE tab's card started 24px lower than SEO's first block. Only the FIRST gap is
   closed: everything below it keeps the ordinary column's air, which is the point of not being a
   .fit column at all. Measured at 375px: both at 150. */
.pin-head .view > .sec:first-child { margin-top: var(--s2); }

/* `.fit .sec` sets a margin-top further down this file and ties with `.pin-head .sec-first` on
   specificity, so document order gave it the win and the SEO switch sat eight pixels lower than
   the socials one. Measured: 28 against 20. At (0,3,0) both selectors here beat it outright, on
   a .fit column and a plain one alike. */
.col.pin-folio.fit .sec-first, .col.pin-head .sec-first {
  position: sticky;
  top: 0;
  z-index: 2;
  margin-top: 0;
  padding-top: var(--s3);
  padding-bottom: var(--s2);
  background: var(--bg);
  margin-left: calc(var(--gutter) * -1);
  margin-right: calc(var(--gutter) * -1);
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

.col.pin-folio.fit .sec-first::after, .col.pin-head .sec-first::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: var(--s4);
  background: linear-gradient(to bottom, var(--bg), transparent);
  pointer-events: none;
}

/* Nothing below the pinned line, or it comes unstuck at the foot of the scroll and rides up by
   whatever that was (Danny, 21 September 2026: "the we create your assets bounces up and down,
   like the page is a bit loose"). Measured: it sat at 674 all the way down and at 657 at the very
   end, and seventeen is this column's own padding-bottom plus a pixel. `bottom: 0` means "stick
   until you reach where you would have been", so the fix is to make where it would have been the
   bottom. Its own padding is the gutter now. */
/* And it is pinned on EVERY tab (Danny, 21 September 2026: "we should do this same thing for the
   site tab as well, all tabs in fact, so it's all locked and they never move"). It began as one
   rule for the socials canvas; there was never a reason for the line to hold still on one tab and
   drift on the others. The folio lives in base.html, so this is one rule rather than five, and
   `:has` scopes it to the columns that carry one: the signed in tabs. The public pages have their
   own footer and the admin has none.

   padding-bottom matches the 16px every other tab already left under it, which is the eight pixels
   he asked for back ("we create your assets needs to come slightly higher to match we deploy your
   SEO strategy"). */
.col:has(> .folio) { padding-bottom: 0; }

.col > .folio {
  position: sticky;
  bottom: 0;
  z-index: 2;
  background: var(--bg);
  padding-top: var(--s4);
  padding-bottom: var(--s4);
  margin-left: calc(var(--gutter) * -1);
  margin-right: calc(var(--gutter) * -1);
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

/* The same distance off the bottom on all five tabs. `.fit` already zeroed this and the other
   three did not, which is why the epigraph sat 41px up on Site, Bookings and Create and 17px up on
   SEO and Socials: a margin under the words, inside the band. */
.col > .folio .epi { margin-bottom: 0; }

/* The fade is only for a page where something genuinely scrolls under the line, which is the
   canvas. On a page built to fit, nothing passes behind it and the gradient is pure loss: it laid
   twenty four pixels of darkening over the last row of the SEO tab and took the bottom off its
   border (Danny, 21 September 2026: "you can't see the bottom of the button border"). */
.pin-folio > .folio::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: var(--s5);
  background: linear-gradient(to top, var(--bg), transparent);
  pointer-events: none;
}

/* Edge to edge, the way a profile grid is (Danny, 20 September 2026). The column's gutter is
   cancelled on both sides, so on a phone the squares reach the glass; on a wide screen they reach
   the column's own edges, because a grid the width of a desktop is a grid of nothing.

   The pinned line has to break out with it, or the tiles would scroll past either side of its black
   band through the gutter. Same negative margins, and the padding put back so the words stay on the
   same left edge as everything else on the page. */
.fit.pin-folio .canvas {
  margin-left: calc(var(--gutter) * -1);
  margin-right: calc(var(--gutter) * -1);
  margin-bottom: var(--s3);
}

/* The canvas carries no section mark: the switch above it is the half's only heading, and the grid
   reads as one object rather than as a block under a rule. */
.sec-canvas { margin-top: var(--s4); }
.fit .sec-canvas { margin-top: var(--s2); }

.canvas {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2px;
}

/* Not element qualified. A locked tile is a link and an unlocked one is a div you can pick up, and
   both are the same tile. 3:4, portrait, the shape of an Instagram profile grid since January 2025
   (Danny, 25 September 2026: "instagram isnt that anymore... could we match instagrams aspect ratio
   please for the grid"). The picture is the whole file, cropped to the tile by `cover` below. */
.canvas-item {
  position: relative;
  display: block;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--bg);
}

/* Only a tile with nothing to show carries an edge. A photograph is its own edge, and a hairline
   between every square is what stops it reading as one canvas. */
.canvas-item:has(.asset-ph) { border: 1px solid var(--rule); }

/* In colour. The grayscale everywhere else is the app's own look; these are the client's pictures
   and the whole point of them is what they look like. */
.canvas-item img { display: block; width: 100%; height: 100%; object-fit: cover; }
a.canvas-item:hover img { opacity: .8; }

/* A piece nothing has placed yet, marked on the tile rather than written out. */
.canvas-item .dot { position: absolute; top: var(--s2); right: var(--s2); }

/* The post itself: the picture at full width, in colour, and nothing over it. */
/* No border (Danny, 20 September 2026: "so many images that look like this, can't imagine they
   actually look like this"). He was right, and it was not the files: a 1px hairline around a
   photograph reads as a white rim on the picture itself. A photograph is its own edge. */
/* One picture, always the same size, and always the whole picture (Danny, 21 September 2026: "the
   images should always be sized correctly, at the moment some of them are bigger and some are
   smaller", then "I think I prefer seeing the full size picture how we had it before").

   A fixed height rather than an aspect ratio. A ratio is a function of the column width, so the
   same post would be 251px tall on a phone and 540px on a desktop and the page would stop fitting
   the moment the window changed. A height is a height: every post page in the app is laid out
   identically, and the budget for what sits under it never moves.

   `contain`, not `cover`. Cover fills the band by cropping, and cropping a client's photograph to
   tidy up a layout is not ours to do: they chose the frame. Contain fits the whole thing inside the
   band, so a portrait comes out narrower rather than cut, and against a black page the space either
   side of it is not visible as anything at all.

   236 is the tallest band the worst case will carry: a notice at the top, the longest caption in
   the client's schedule, and a line saying an account is not connected. Measured, not chosen. */
.shot { background: var(--bg); overflow: hidden; height: 236px; }
.shot img { display: block; width: 100%; height: 100%; object-fit: contain; }
.shot .asset-ph { height: 100%; aspect-ratio: auto; }

.asset {
  border: 1px solid var(--rule);
  padding: var(--s3);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}

.asset-thumb {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px solid var(--rule);
  background: var(--bg);
}

.asset-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1);
}

.asset-ph {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s1);
  height: 100%;
  text-align: center;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.asset-caption {
  font-size: var(--fs-small);
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.asset-status {
  margin-top: auto;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.asset .actions { margin-top: var(--s2); gap: var(--s2); }
.asset .btn { padding: 0 var(--s3); flex: 1 1 100%; }

.is-fading { opacity: .6; transition: opacity var(--fade); }

/* ---------------------------------------------------------------- health score (SEO) */

.score {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  margin: var(--s2) 0 var(--s4);
}

.score .display { font-size: var(--fs-display); }
.score .of { color: var(--ink-2); font-size: var(--fs-body); }

/* ---------------------------------------------------------------- a tab that fits the phone

   Danny, 10 September 2026: "both what we do and what you do could fit nicely on the page on mobile
   without needing to scroll at all. Cut down the worth knowing size and just fit both on the pages."

   Measured on the 375x812 phone the app is built for: the scroller is 674px between the header and
   the tab bar, and the epigraph and the segmented control take the first and last of it. That leaves
   about 500px for a half, which is why the ways in lose their sub line here and nowhere else. Nothing
   is lost by it: every one of those sentences is the first line of the page the row opens, so it is
   read at the moment it is wanted rather than seven at once on the way past.

   Scoped to `.fit` on the column, so the taller, explaining version of `.ways` stays the default
   everywhere else (the profile pages, where a row is the whole subject of the page it opens). */
/* The column's own air, which is generous everywhere else and is most of what was over. */
.fit { padding-top: var(--s3); padding-bottom: var(--s4); }
.fit .sec { margin-top: var(--s2); }
.fit .label { margin-bottom: var(--s2); }
.fit .label + .muted { margin-top: var(--s1); }
/* A notice is transient and sits above everything: it takes the least room it can, so pressing a
   button never turns a page that fits into one that does not. */
.fit .notice { font-size: var(--fs-small); margin-top: var(--s2); }
/* A second heading inside a section needs the air a new section would have given it. */
.label-gap { margin-top: var(--s4); }
.fit .folio { padding-top: var(--s4); }
.fit .epi { margin-bottom: 0; }
.fit .ways { gap: 4px; }
/* The switch is the tab's title here, so it does not need a heading's worth of air around it.
   `.pin-head` as well as `.fit`, or the SITE tab's switch sits eight pixels lower than the other
   two: its column is pinned but not a .fit one, so it was taking `.seg`'s own margin instead of
   this. Measured at 375px: 84 against 76. All three read 76 now. */
.fit .seg, .pin-head .seg { margin-top: var(--s2); margin-bottom: var(--s3); }
/* The score is the headline of the tab, not a poster. 64px of numeral was costing a way in. */
.fit .score .display { font-size: var(--fs-num); }
.fit .rating-num { font-size: var(--fs-num); }
.fit .score .of { font-size: var(--fs-small); }
/* What's next is a paragraph from us, and it reads as one at small. It is also the only thing on
   the half whose length we choose, which is why the admin field says to keep it to a few lines. */
.fit .pre-line { font-size: var(--fs-small); }

/* One line each: the name, what there is of it, and the way through. 52px, so still over the 44px
   tap target with room for a name that wraps on a narrow phone. */
.ways-tight a.way { min-height: 44px; padding: var(--s3) var(--s4); gap: var(--s3); }

/* The score and the button that changes it, on one line rather than stacked. */
.stand { display: flex; align-items: center; justify-content: space-between; gap: var(--s4); }
.stand .score { margin: 0; }
.stand .actions { margin: 0; flex: 0 0 auto; }

/* The half FILLS the screen rather than sitting at the top of it (Danny, 10 September 2026: "I want
   the items to fill the page, perfectly. Now it's all pushed to the top"). Getting under 674px left
   the leftover as dead air above the epigraph. The lists take it instead, so every row on a half is
   the same height and the last one ends flush above the epigraph.

   `:not([hidden])` rather than a bare `.view`, because `display: flex` here would otherwise out
   specify `.view[hidden] { display: none }` and both halves would paint at once. The same trap as
   `.btn[hidden]` before it. */
.fit .view:not([hidden]) { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.fit .view > .sec-grow { display: flex; flex-direction: column; min-height: 0; }
/* On the pages under the tab there is no half in between: the growing section is a child of the
   column itself, and what it holds stretches to the foot the same way. */
.fit > .sec-grow { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
/* On its own page the chart gets the room the page has, rather than the 140px it takes when it is
   one block among many. The svg carries the height, not the wrapper: the wrapper holds the axis. */
.fit .sec-grow > .chart { flex: 0 0 auto; }
.fit .sec-grow > .chart svg { height: 240px; }
.fit .sec-grow > .ways { flex: 1 1 auto; }
/* The SITE tab's website card was briefly grown to fill a screenful here, and its rows and frame
   tightened to make that fit. All of it is gone (Danny, 22 September 2026: "a tad more spaced out
   and breathing room"): the tab is an ordinary column again and the card is the ordinary card, one
   step smaller. See .card-tight. */
/* Grow, never shrink: when the plan is opened the page is taller than the screen, and a row that
   gave ground there would drop under the 44px it has to keep. */
.fit .sec-grow > .ways > li { flex: 1 0 auto; display: flex; }
.fit .sec-grow > .ways > li > a.way { flex: 1 0 auto; }

/* Each list takes a share of the leftover in proportion to how many rows it holds, so a three row
   list and a four row list on the same half end up with rows of the same height. `data-rows` is the
   count the template already knows; CSS cannot do the arithmetic, so the cases are written out. */
.fit > .sec-grow[data-rows] { flex-grow: 1; }
.fit .view > .sec-grow[data-rows="1"] { flex-grow: 1; }
.fit .view > .sec-grow[data-rows="2"] { flex-grow: 2; }
.fit .view > .sec-grow[data-rows="3"] { flex-grow: 3; }
.fit .view > .sec-grow[data-rows="4"] { flex-grow: 4; }
.fit .view > .sec-grow[data-rows="5"] { flex-grow: 5; }
.fit .view > .sec-grow[data-rows="6"] { flex-grow: 6; }
.fit .view > .sec-grow[data-rows="7"] { flex-grow: 7; }
.fit .view > .sec-grow[data-rows="8"] { flex-grow: 8; }

/* The way back, at the top where it belongs, on every page under the SEO tab. 44px like every other
   control, and it replaced a whole section holding one button at the foot of each page. */
.back {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 44px;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  text-decoration: none;
}
.back:hover { color: var(--ink); }
.back svg { flex: 0 0 auto; }

/* The way back sits on the title's own line, the way it does in every phone app, rather than taking
   a line of its own above it. Worth 44px on every page under this tab. */
.pagetop { display: flex; align-items: center; gap: var(--s2); }
.pagetop > .back { margin-left: calc(var(--s3) * -1); padding: 0 var(--s2); }
.pagetop > .h2 { margin: 0; min-width: 0; }

/* Rows carry the same information in less height here: the air between them was set for a page that
   was allowed to run on. */
.fit .sec-mark { margin-bottom: var(--s3); }
.fit .list-row { padding: var(--s2) 0; }
/* The sentence under a section heading is one line of explanation, not a paragraph break. */
.fit .sec > p.muted { margin-top: var(--s1); font-size: var(--fs-small); }
.fit .sec-head > .label { margin-bottom: var(--s2); }
.fit .entries { gap: var(--s2); }
.fit .actions { margin-top: var(--s3); }
/* A thumbnail is a thumbnail: the strip does not need to be a third of the screen to show what we
   hold, and it scrolls sideways for the rest. */
.fit .reel-item { height: 120px; }
.fit .reel-item img { height: 100%; width: auto; }
/* The verb on an entry sits with the line saying where it came from, not on a row of its own. */
.fit .entry-body > .actions { margin-top: var(--s1); }
.fit .entry-body > .hint + .actions { margin-top: 0; }
.fit .entry { padding: var(--s3) 0; }

/* On a page that has to fit, the tiles run along one line and scroll sideways rather than stacking
   into three rows of a grid. Same tiles, same numbers; five of them cost 130px here instead of 400.
   The chart keeps its shape at a shorter height: a trend does not need half a screen to read. */
.fit .tiles { display: flex; overflow-x: auto; gap: var(--s2); scroll-snap-type: x proximity; }
.fit .tiles > .tile { flex: 0 0 46%; scroll-snap-align: start; }
.fit .chart { height: 120px; }
.fit .chart svg { height: 100%; }
/* The chart on a page of its own (What it did, under the Business Profile and Search Console) is
   the one chart in a .fit column with only the epigraph under it, and the rules above disagreed
   about it: the wrapper was held at 120px while the plot inside kept its 240px, so the plot and its
   dates hung out of the box and the page never knew how tall they were. On a phone (393 by 797,
   24 September 2026) the bottom line and the dates went under the epigraph, on a page that then
   would not scroll to them. The wrapper is as tall as what it holds now: at 505 by 928 nothing
   moves, and a shorter screen scrolls the few pixels it needs (43 there) instead of hiding them. */
.fit > .sec-grow > .chart { height: auto; }

/* The line that opens the rest of a list (templates/_capped.html). One row, the same 44px as
   everything else, the same marker treatment as the History fold it is modelled on. */
.more { margin-top: var(--s2); }
.more-btn {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 44px;
  cursor: pointer;
  list-style: none;
  color: var(--ink-2);
}
.more-btn::-webkit-details-marker { display: none; }
.more-btn::marker { content: ""; }
.more-btn .label { margin: 0; }
.more-chev { flex: 0 0 auto; margin-left: auto; }
.more-btn:hover, .more[open] .more-btn { color: var(--ink); }
.more[open] .more-chev { transform: rotate(180deg); }
.more > .list, .more > .entries, .more > .table-wrap { margin-top: var(--s2); }

/* The Account page on one screen (Danny, 25 September 2026: "can we make it all fit on one page
   please"). A column of full-width buttons; each form opens in place under its own button, which
   fills while it is open so it is plain which one that is. A summary drawn as a .btn loses the
   browser's disclosure triangle. Close account is a text button, the quietest thing on the page. */
.acct-actions { display: flex; flex-direction: column; gap: var(--s2); }
.acct-fold > summary { list-style: none; }
.acct-fold > summary::-webkit-details-marker { display: none; }
.acct-fold > summary::marker { content: ""; }
.acct-fold[open] > summary.btn { background: var(--ink); color: var(--bg); }
.acct-fold > .muted { margin-top: var(--s4); }
.acct-fold > .form { margin-top: var(--s4); margin-bottom: var(--s5); }

/* ---------------------------------------------------------------- folio and epigraph
   Each tab closes with the manifesto line it keeps, at h2 300, above the final hairline. */

/* Always at the foot. `auto` takes whatever space is left on a short page and collapses to nothing
   on a long one, where the padding is what holds it clear of the content above. */
.folio { margin-top: auto; padding-top: var(--s8); }

.epi {
  font-size: var(--fs-h2);
  font-weight: 300;
  line-height: 1.2;
  color: var(--ink-2);
  margin-bottom: var(--s5);
  max-width: none;
}

.folio-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--rule-2);
}

.folio-line .label { margin: 0; }
.folio-line a.label { text-decoration: none; display: inline-flex; align-items: center; min-height: 44px; }

/* The landing site's footer set (Privacy, Terms, FAQ, Leave, Affiliate) under the folio line on
   every app footer: label links, 44px tall, wrapping on a narrow phone. */
.folio-links { display: flex; flex-wrap: wrap; gap: 0 var(--s4); }
.folio-links a.label { text-decoration: none; display: inline-flex; align-items: center; min-height: 44px; margin: 0; }
.folio-links a.label:hover { color: var(--ink); }

/* The company block on the public pages (login, signup): verbatim, line breaks kept. */
.co {
  margin-top: var(--s4);
  white-space: pre-line;
  font-size: var(--fs-small);
  line-height: 1.6;
  color: var(--ink-2);
  max-width: none;
}
.folio-line a.label:hover { color: var(--ink); }

/* ---------------------------------------------------------------- tap targets (brief: 44px)
   The inline links (a plan note, a hint, the sign-in line, a row's site name, a card title's link),
   the footer set, the admin crumbs, the editor sub-nav and each row's Edit summary are drawn at
   their text size. A transparent pseudo-element extends every one to at least 44px each way without
   moving anything, the same rule as the landing site (web/css/style.css). Nothing goes in this list
   that sits beside another control: the box is centred, so it grows over its neighbour (that is why
   .switcher-btn and .switcher-opt carry a real height of their own instead). */
.row-sub a, .row-val a, .hint a, .muted a, .row-name a, .card-title a, .kv dd a, .folio-links a.label, .folio-line a.label, .crumbs a, .subnav a, details.row-edit > summary, .files .check { position: relative; }
.row-sub a::after, .row-val a::after, .hint a::after, .muted a::after, .row-name a::after, .card-title a::after, .kv dd a::after, .folio-links a.label::after, .folio-line a.label::after, .crumbs a::after, .subnav a::after, details.row-edit > summary::after, .files .check::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, 44px);
  height: max(100%, 44px);
  transform: translate(-50%, -50%);
}

/* Skip link: first in the tab order on every page, off screen until it is focused (the landing
   site's rule in px). #app clips its overflow but is not positioned, so the link is not clipped. */
.skip {
  display: block;
  position: absolute;
  top: -200px;
  left: var(--gutter);
  z-index: 20;
  padding: 12px 16px;
  background: var(--bg);
  border: 1px solid var(--ink);
  color: var(--ink);
  font-size: var(--fs-label);
  line-height: 1.3;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
}

.skip:focus { top: 8px; }

/* ---------------------------------------------------------------- motion */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
}

/* ---------------------------------------------------------------- admin (appended by the admin agent)
   Rows again: a label-link sub-nav under the site name, inline row forms whose controls keep the
   44px height (every admin editor is phone-first too), and a <details> per row for the edit form so
   every editor works with JS off. Black, white, hairlines. */

/* Crumbs are links, so each is a 44px target; the row's own height carries the rhythm, no margin. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--s2);
  margin: calc(var(--s3) * -1) 0 0;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  max-width: none;
}

.crumbs a { display: inline-flex; align-items: center; min-height: 44px; color: var(--ink-2); text-decoration: none; }
.crumbs a:hover { color: var(--ink); }
.crumbs span[aria-hidden] { color: var(--ink-off); }

/* The per-site editors: eight label links that wrap onto as many rows as the width needs (at 360
   that is three), never a hidden horizontal scroll with labels cut mid-word. */
.subnav {
  display: flex;
  flex-wrap: wrap;
  /* 1px of row gap, not 0: each link pulls its underline down onto the container rule with
     margin-bottom: -1px, so at 0 a wrapped row overlapped the row above it by that pixel and the
     lower link took the tap in that band. The column gap is what separates the labels. */
  gap: 1px var(--s5);
  margin: var(--s3) 0 0;
  border-bottom: 1px solid var(--rule);
}

.subnav a {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--ink-2);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  margin-bottom: -1px;
}

.subnav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--ink); }

.row-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s3);
  flex: 1 1 100%;
  margin-top: var(--s2);
}

.row-form .field { margin: 0; flex: 1 1 140px; }
.row-form .field label { margin-bottom: 0; }
.row-form .field.grow { flex: 1 1 100%; }
.row-form input, .row-form select, .row-form textarea { min-height: 44px; padding: var(--s1) 0; }
.row-form textarea { min-height: 72px; }
.row-form .btn { padding: 0 var(--s3); flex: 0 0 auto; }
.row-form input[type="checkbox"] { min-height: 0; }

.list-row .row-name .label { display: inline; margin: 0 var(--s2) 0 0; }
.list-row .row-actions { display: flex; flex-wrap: wrap; gap: var(--s2); flex: 0 0 auto; }
.list-row .row-actions .btn { padding: 0 var(--s3); }
.list-row .row-actions form { display: inline-flex; gap: var(--s2); align-items: center; }

details.row-edit { flex: 1 1 100%; }
details.row-edit > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  text-decoration: underline;
  text-decoration-color: var(--rule-2);
  text-underline-offset: 4px;
}
details.row-edit > summary::-webkit-details-marker { display: none; }
details.row-edit > summary:hover, details.row-edit[open] > summary { color: var(--ink); }
details.row-edit > .row-form { margin-top: var(--s2); padding-bottom: var(--s2); }

textarea.json {
  min-height: 240px;
  white-space: pre;
  overflow: auto;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}

.problems { margin: var(--s3) 0 0; padding: 0; list-style: none; }
.problems li { padding: var(--s2) 0; border-top: 1px solid var(--rule); font-size: var(--fs-small); }
.problems li:last-child { border-bottom: 1px solid var(--rule); }

.kv { display: grid; grid-template-columns: max-content 1fr; gap: var(--s1) var(--s4); font-size: var(--fs-small); }
.kv dt { color: var(--ink-2); margin: 0; }
.kv dd { margin: 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

.thumb-list { display: flex; flex-wrap: wrap; gap: var(--s2); }
.thumb-list li { list-style: none; width: 64px; }
.thumb-list img { display: block; width: 64px; height: 64px; object-fit: cover; border: 1px solid var(--rule); filter: grayscale(1); }
.thumb-list span { display: block; font-size: var(--fs-label); color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sec .table-wrap + .form, .sec .list + .form { margin-top: var(--s5); }
.pw { font-variant-numeric: tabular-nums; letter-spacing: .04em; }

/* ---------------------------------------------------------------- SEO tab (appended by the SEO tab agent)
   Only additions: the sentence under the score, the group labels between the page lists, a sub-line
   inside a table cell, and the admin's "What's next" paragraph keeping its line breaks. */

.sec > .check-meta { margin-top: 0; margin-bottom: var(--s4); }

.group-label { margin: var(--s5) 0 var(--s3); }
.group-label:first-of-type { margin-top: 0; }

.cell-sub {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-small);
  color: var(--ink-2);
  overflow-wrap: anywhere;
}

.pre-line { white-space: pre-line; }

/* ---------------------------------------------------------------- SITE tab (tabs_site.py, templates/site/)
   Appended by the site tab: the feed's kind label sits under the message on its own line, the setup
   steps keep their dot beside the word, a logged request keeps its line breaks. Nothing above is changed. */

.feed .row-sub { order: 3; }
.feed .row-name { flex: 1 1 60%; }

.steps .row-name .dot { margin-right: var(--s3); }

.request-text { white-space: pre-line; }

.site-card .row-val { max-width: 60%; }

/* ---------------------------------------------------------------- socials tab (appended by the socials tab agent)
   An asset awaiting approval takes the stronger hairline so the eye finds it in the grid; the card's
   meta line is the plain label with no margin; the change page reuses .cards so the preview stays a tile. */

.asset > .label { margin: 0; }
.asset > .small { margin: 0; }
.range a[aria-current="true"] { cursor: default; }

/* House rule (Danny, 5 Sep 2026): text on our websites is not highlightable. Form fields stay selectable. */
html{-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}
input,textarea,select{-webkit-user-select:text;user-select:text}
/* Two admin exceptions, both customer-typed text on the jobs pages, neither on a client surface:
   a URL we refuse to link (admin/_links.html), which would otherwise be a string Danny can neither
   click nor copy, and .verbatim, the customer's own words (the brief, the round notes, a creation
   request), which he has to be able to lift into the build. */
.selectable,.verbatim{-webkit-user-select:text;user-select:text}

/* File picker in the house style: the native grey button becomes a white outline button. */
input[type="file"]{color:rgba(255,255,255,.6);font:inherit;font-size:14px;padding:6px 0}
input[type="file"]::file-selector-button{font:inherit;font-size:11px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:#fff;background:#000;border:1px solid #fff;padding:10px 16px;margin-right:12px;cursor:pointer;border-radius:0}
input[type="file"]::file-selector-button:hover{background:#fff;color:#000}

/* ---------------------------------------------------------------- onboarding (onboarding.py, templates/onboarding/)
   The four-step sheet and the holding page. Only additions: the "1 of 4" step label, a section head
   with its Edit link, the Advanced disclosure, the direct-reference rows, the attached-file rows,
   the process sentence, and the holding page's single sentence. Black, white, hairlines. */

.step { margin-bottom: var(--s2); }

.sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3); }
.sec-head .label { margin-bottom: var(--s3); }
.sec-head a.edit { position: relative; text-decoration: underline; text-decoration-color: var(--rule-2); text-underline-offset: 4px; }
.sec-head a.edit:hover { color: var(--ink); }
.sec-head a.edit::after { content: ""; position: absolute; left: 50%; top: 50%; width: max(100%, 44px); height: max(100%, 44px); transform: translate(-50%, -50%); }

details.advanced { margin-top: var(--s5); border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
details.advanced > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 44px;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
details.advanced > summary::-webkit-details-marker { display: none; }
details.advanced > summary::after { content: "+"; font-weight: 300; font-size: var(--fs-h2); line-height: 1; }
details.advanced[open] > summary::after { content: "\2212"; }
details.advanced > summary:hover, details.advanced[open] > summary { color: var(--ink); }
details.advanced > .hint { margin-top: 0; }
details.advanced > .actions-inline { margin-bottom: var(--s4); }

.ref-row { margin-top: var(--s4); padding-top: var(--s3); border-top: 1px solid var(--rule); }
.ref-row > .label { margin-bottom: 0; }
.ref-row .field { margin-top: var(--s3); }

.files { margin-top: var(--s3); }
.files .list-row { min-height: 44px; padding: var(--s2) 0; }
/* The Remove label is drawn small; the tap-target rule above extends it to 44px each way. */
.files .check { min-height: 0; font-size: var(--fs-small); color: var(--ink-2); }

.process { margin-top: var(--s5); padding-top: var(--s4); border-top: 1px solid var(--rule); max-width: none; }
/* After the Advanced disclosure its bottom hairline is the rule: one line, never two. */
details.advanced + .process { border-top: 0; padding-top: 0; }

/* A page that is one sentence sits in the middle of what is left between the header and the tab
   bar, not at the top of it (Danny, 9 September 2026). .scroll is a flex item with a height the
   flex layout has already resolved, so the 100% here is a definite one, and every box is
   border-box, so the column's own padding is inside it and nothing overflows into a scroll. */
.col-fill {
  min-height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* The column's own padding is 24 over 32, and the difference is the block sitting 4px above the
     middle. Nothing scrolls on a page this short, so the bottom one comes back to match. */
  padding-bottom: var(--s5);
}

/* CREATE's placeholder (Danny, 24 September 2026: "Create Engine coming soon"): the holding
   page's block, centred in the room between the header and the tab bar. There is no epigraph at
   the foot (Danny, 25 September 2026: "it should not have one at all"), so the column's padding is
   24 above and 24 below and the block's own two auto margins centre it exactly: measured at 505 by
   928, 366 above and 366 below. The half-padding lift it carried was for the epigraph, and without
   one it put the block 24px off. */
.col-fill.create-soon > .sec.holding { margin-top: auto; margin-bottom: auto; }

/* The holding page: the sentence, the hairline above it and the whole block centred on the page. */
.holding { text-align: center; }
.holding .sec-mark { margin-left: auto; margin-right: auto; }
.holding .h2 { max-width: 22em; margin-left: auto; margin-right: auto; }

/* ---------------------------------------------------------------- Admin, Jobs (appended by the jobs agent)
   The customer's words verbatim: line breaks kept, long links wrapped; a round's JSON as a small block. */
.verbatim { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; font-family: inherit; }
pre.verbatim { font-size: var(--fs-small); line-height: 1.5; color: var(--ink-2); }
.sec .kv + .form, .sec .form + .form { margin-top: var(--s5); padding-top: var(--s4); border-top: 1px solid var(--rule); }

/* ---------------------------------------------------------------- tabs by plan (BRIEF 5.7, tabs_bookings.py, tabs_create.py)
   The bar carries data-tabs="3", "4" or "5". Five labels at 360px are 72px each: the tracking eases
   a touch under 420px so BOOKINGS and SOCIALS stay on one line at 11px. Nothing above is changed. */

.tab { min-width: 0; white-space: nowrap; padding-left: 2px; padding-right: 2px; }

@media (hover: none) and (max-width: 419px), (pointer: coarse) and (max-width: 419px), (any-pointer: coarse) and (max-width: 419px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (max-width: 335px) {
  .tabs[data-tabs="5"] .tab, .tabs[data-tabs="4"] .tab { letter-spacing: .08em; }
}

/* The narrowest phones: the header carries the wordmark, the site name and the account glyph, and
   the site name is what gives way. 12px each side still holds the wordmark and the name apart
   (running them together was the whole point of the padding) and hands the name the 8px back. The
   name itself takes the ellipsis; the whole of it is in the button's title and in the panel. */
@media (hover: none) and (max-width: 380px), (pointer: coarse) and (max-width: 380px), (any-pointer: coarse) and (max-width: 380px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (max-width: 304px) {
  .hd-centre { padding: 0 var(--s3); }
  .switcher-btn { max-width: min(200px, 100%); padding: 0 var(--s1); }
}

/* Prose blocks (the "How bookings work on your site" note): paragraphs breathe, nothing else. */
.prose p + p { margin-top: var(--s3); }

/* ---------------------------------------------------------------- the Update area
   One card per update the client still has a say in (Danny, 9 September 2026), each carrying its
   own buttons: Confirm and publish, Leave notes, Delete, and Undo publish while it is confirmed.
   The buttons sit inside the card, so a stack of updates never runs its actions together. */
/* The UPDATE section, given room (Danny, 21 September 2026: "spread the update section a bit to
   fill that page so its not all squashed together"). Three things sit in it now that Update
   request has come down off the top of the tab: the way to ask for a change, then what is already
   in hand. They were stacked on twelve pixels apiece and read as one block of text with a button
   in it. Every gap below is one step up the scale from what it was. */
.sec-update > .actions { margin-top: var(--s4); margin-bottom: var(--s6); }
/* The empty state: a line of its own above and below, because it is the answer to "what is
   waiting for me" and not a caption on the button. */
.sec-update > .h2 { margin-bottom: var(--s3); }
.sec-update > .muted { margin-bottom: var(--s5); }

/* The same card, one step shorter (Danny, 22 September 2026: "make the website box with private
   link etc in ever so slightly smaller, I preferred it when it was a bit smaller than it is now").
   44px in the clear rather than 52, which is the floor in the house rules and not a pixel under it,
   and 12px of frame rather than 16. The rows hold links, and it is the link that gets tapped rather
   than the row. */
.card-tight { padding: var(--s3); }
.card-tight .list-row { min-height: 44px; padding: var(--s2) 0; }

/* The one thing to do in a section, and every one of them the same size (Danny, 22 September 2026:
   "make the update request button the same size as the confirm migration button"). A button sizes
   from its own label, so Update request came out 158px against Confirm migration's 181 and the two
   read as different kinds of control on the same tab. One floor under both, set above the longer of
   the two labels, and the words centre in whatever is left over. */
.actions-lead > .btn { min-width: 12rem; justify-content: center; }
/* And the same rhythm around it (Danny, 22 September 2026: "the confirm migration button should be
   a tad higher so its the same distance apart as update and update request button is, and then the
   paragraph below it should have a bit more of a gap"). Measured before: the Update section ran
   16px under its heading and 32px above what followed; Migration ran 24 and 8. Both are 16 and 32
   now, so the two sections are the same shape as well as the same size. */
.actions-lead { margin-top: var(--s4); margin-bottom: var(--s6); }

/* The BACKEND half's five ways in, set apart from the chart above them (Danny, 22 September 2026:
   "give this part a title, maybe Analytics, and maybe give it some breathing space from the chart
   so the page is nicer laid out"). A section's ordinary 32px is the gap between two blocks of the
   same kind; this is the gap between the numbers and the things behind them, so it takes the next
   step up. */
.sec.sec-analytics { margin-top: var(--s7); }

/* The Visitors and Page views pages, held to one screen (Danny, 22 September 2026: "in visitors tab
   now, make this page fit on one page without needing to scroll"). Measured at 375 by 812: 1,331px
   before, and a 674px scroller.

   The chart takes a fixed share of the screen rather than its standalone 240px, and the list of
   numbers under it grows into whatever is left, so a tall screen spreads the rows out and a short
   one closes them up. Nothing here scrolls at either end. */
.fit-metric .sec-chart { margin-top: var(--s3); flex: 0 0 auto; }
/* `.fit .chart` fixes the wrapper at 120px and `.fit .chart svg` fills it, which leaves the date
   labels under the plot no room inside the box: they overflowed onto the first row of numbers.
   Undone both ways here. The wrapper takes its height from what is in it, and the plot is what is
   sized, so the dates always have their own line. */
.fit-metric .sec-chart .chart { height: auto; }
.fit-metric .sec-chart .chart svg { height: clamp(76px, calc(10vh / var(--z)), 112px); }
.fit-metric > .sec-grow > .list { flex: 1 1 auto; display: flex; flex-direction: column; margin: 0; }
/* Grow, never shrink, and never under the 44px a finger needs. */
.fit-metric > .sec-grow > .list > .list-row { flex: 1 1 auto; min-height: 44px; padding: var(--s2) 0; }
/* Everything except the numbers sits at its own size. `:not(.sec-grow)` matters: `.sec-grow` also
   carries `.sec`, this rule is later in the file than `.fit > .sec-grow`, and at equal specificity
   later wins, so without it the growing section was pinned to its content and the page ended in a
   band of nothing. */
.fit-metric > .sec:not(.sec-grow), .fit-metric > .history { flex: 0 0 auto; }

/* The update line and the box under it, landing on the same lines as the SITE tab's label and its
   card (Danny, 22 September 2026: "on the seo tab bring the top line of what we checked up to align
   with the website box, so they are the same place on the screen", then "they arent exactly aligned,
   SEO's is a millimetre or two higher").

   Written as the label's own measurements rather than as a pixel gap, and that is the whole point:
   the label is `--fs-label` at a line-height of 1.4 with `--s3` under it, and `--fs-label` changes
   from 11px to 12px at the wide breakpoint. A fixed gap tuned at 375px drifts the moment the type
   scale moves, which is exactly what it did. This one cannot: the line box and the gap are the same
   expressions on both tabs, so the two boxes are on the same line at every width.

   Specificity: `.fit .sec > p.muted` is (0,3,1) and set a margin of its own, so this has to be more
   than that rather than merely later in the file. */
.col .sec.sec-checked > p.check-meta {
  line-height: calc(var(--fs-label) * 1.4);
  margin-top: var(--s1);
  margin-bottom: var(--s3);
}

.update-card { margin-top: var(--s4); }
.update-card:first-of-type { margin-top: var(--s3); }
/* With the button above them, the first card needs the same clear line the others have. */
.sec-update .update-card:first-of-type { margin-top: 0; }
.sec-update .update-card + .update-card { margin-top: var(--s5); }
.update-card .actions { margin-top: var(--s4); }
.update-card .actions form, .list-row .actions form { display: contents; }
/* An update in the history keeps its buttons on the row, wrapping under the words at 360. */
.list-row > .actions-inline { display: flex; flex-wrap: wrap; gap: var(--s3); width: 100%; }
.update-card > .muted.small { margin-top: var(--s3); }

/* ---------------------------------------------------------------- the migration helper (Danny, 9 September 2026)

   A chat in a panel that comes down over the tab, from under the header to the top of the tab bar.
   It is its own room: the page behind it does not scroll, and the only ways out are Close and
   Escape. --tabs-h is a floor that app.js replaces with the bar's measured height on open, because
   the bar grows with the home indicator on a phone.

   The slide is a deliberate exception to the opacity-only rule, asked for by name. It is one
   transform on one element, it respects prefers-reduced-motion, and nothing else moves. */
:root { --tabs-h: 73px; }

.assist {
  position: fixed;
  left: 0;
  right: 0;
  top: var(--header-h);
  bottom: var(--tabs-h);
  /* Under .hd (12), so it slides out from beneath the header rather than across the wordmark, and
     over .picker-panel (8). */
  z-index: 11;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
  transform: translateY(-100%);
  transition: transform 260ms ease, opacity var(--fade);
  opacity: 0;
}

.assist.open { transform: translateY(0); opacity: 1; }
/* [hidden] has to beat display: flex, the same trap the View page button hit. */
.assist[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .assist { transition: opacity var(--fade); transform: none; }
}

.assist-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  min-height: 44px;
  padding: 0 var(--gutter);
  border-bottom: 1px solid var(--rule);
}

.assist-head .label { margin: 0; }

.assist-close {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: grid;
  place-content: center;
  background: none;
  border: 0;
  color: var(--ink-2);
  cursor: pointer;
}

.assist-close:hover { color: var(--ink); }

/* The scroller. The composer sits at the bottom of the panel, not the bottom of the conversation,
   so it is in the same place whether there are two messages or forty. */
.assist-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding: var(--s4) var(--gutter) var(--s4);
}

.chat {
  flex: 1 1 auto;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  max-width: var(--col-max);
  width: 100%;
  margin-inline: auto;
}

.chat-row { display: flex; flex-direction: column; gap: 4px; max-width: 46ch; }
.chat-user { align-self: flex-end; text-align: right; }
.chat-who {
  font-size: var(--fs-label);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.chat-body {
  margin: 0;
  font-size: var(--fs-body);
  line-height: 1.5;
  padding: var(--s3);
  border: 1px solid var(--rule);
}
.chat-user .chat-body { border-color: var(--rule-2); }
.chat-when { font-size: var(--fs-small); color: var(--ink-2); }
.chat-shot { display: block; border: 1px solid var(--rule); }
.chat-shot img { display: block; width: 100%; height: auto; }

/* The composer. Hairlines and a white button, the same vocabulary as every other form here. */
.chat-form {
  flex: 0 0 auto;
  width: 100%;
  max-width: var(--col-max);
  margin: var(--s4) auto 0;
  padding-top: var(--s3);
  border-top: 1px solid var(--rule);
}

.chat-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.chat-form textarea { width: 100%; }
.chat-tools { display: flex; align-items: center; gap: var(--s3); margin-top: var(--s2); }
.chat-send { margin-left: auto; }
.chat-form input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; }

label.chat-clip, .field label.chat-clip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 44px;
  font-size: var(--fs-small);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-2);
  cursor: pointer;
  margin: 0;
}

label.chat-clip:hover { color: var(--ink); }
/* Same again: the file input is hidden, so the label it belongs to shows the focus. */
.chat-form:has(#chat-image:focus-visible) label.chat-clip { color: var(--ink); outline: 1px solid var(--ink); outline-offset: 2px; }
label.chat-clip svg { flex: 0 0 auto; }

/* ---------------------------------------------------------------- the two halves (Danny, 10 September 2026)

   The SEO tab splits under the header: what we do, and what you do. Two halves of one control, with
   a marker that slides between them, because that is what the control is: a switch, not a pair of
   links. Asked for by name, and the second deliberate exception to the opacity-only rule after the
   migration helper's panel. One transform on one element, and it holds still under
   prefers-reduced-motion.

   Both halves are always in the page, one hidden. With JavaScript off the links reload with ?view=
   and the server hides the other one; with it on, app.js swaps the attribute and nothing reloads. */
.seg {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  margin: var(--s4) 0 var(--s5);
  border: 1px solid var(--rule);
}

.seg-opt {
  position: relative;
  z-index: 1;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--s3);
  color: var(--ink-2);
  text-decoration: none;
  text-align: center;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  transition: color var(--fade);
}

.seg-opt:hover { color: var(--ink); }
.seg-opt[aria-current="page"] { color: var(--bg); }

/* The marker: half the control, filled, sitting behind the words. */
.seg-mark {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 50%;
  background: var(--ink);
  transition: transform 220ms ease;
}

/* Which half the marker sits under, read off which option is current rather than off the name of
   the half (Danny, 20 September 2026: the SOCIALS tab got the same switch and the rule here said
   `[data-on="you"]`, so the marker stayed left, the current label went black on black and the other
   went grey on white. Two invisible words). app.js keeps aria-current and data-on in step, so this
   is true whichever tab the switch is on and whatever the halves are called. */
.seg[data-seg-at="1"] .seg-mark { transform: translateX(100%); }
/* The `:has` version is kept underneath it as a floor for a page rendered before app.js runs, and
   because it is the one that is true by construction. It is not enough on its own: measured on the
   socials switch, aria-current moved to the second option and the marker did not move with it, so
   the white block stayed under the word that was no longer current. An attribute the switch itself
   carries cannot fail to invalidate. */
.seg:has(> .seg-opt:nth-of-type(2)[aria-current="page"]) .seg-mark { transform: translateX(100%); }

@media (prefers-reduced-motion: reduce) {
  .seg-mark { transition: none; }
}

/* The half that is not showing. [hidden] has to beat the block display, the same trap as .assist. */
.view[hidden] { display: none; }

/* ---------------------------------------------------------------- ways in (Danny, 10 September 2026)

   The "What you do" half is a list of things to do, each opening its own page. A way is the whole
   row: a name in the label voice so it reads as a control, one line of why underneath, and a
   chevron. Bordered rather than hairline-separated, because unlike a list row this is a button. */
.ways { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s3); }

a.way {
  display: flex;
  align-items: center;
  gap: var(--s4);
  min-height: 72px;
  padding: var(--s4);
  border: 1px solid var(--rule);
  color: var(--ink-2);
  text-decoration: none;
  transition: color var(--fade), border-color var(--fade);
}

a.way:hover { color: var(--ink); border-color: var(--rule-2); }

.way-text { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; flex: 1 1 auto; }

.way-name {
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink);
}

.way-sub { font-size: var(--fs-small); color: var(--ink-2); }
.way-chev { flex: 0 0 auto; color: var(--ink-2); }
a.way:hover .way-chev { color: var(--ink); }



/* The count on the right of a way in: how many reviews are waiting, how many questions are
   unanswered. It is what keeps a page of ways in readable as a dashboard. */
.way-count {
  flex: 0 0 auto;
  font-size: var(--fs-small);
  color: var(--ink-2);
  white-space: nowrap;
}

a.way:hover .way-count { color: var(--ink); }

/* ---------------------------------------------------------------- a profile reads as a profile

   Danny, 10 September 2026: the listing's own words were laid out as a to-do list, hollow dots and
   all, when they are the thing itself. A description is a sentence, a category is a name, and
   photographs are photographs. */
.profile-text {
  font-size: var(--fs-h2);
  font-weight: 300;
  line-height: 1.35;
  color: var(--ink);
  margin: 0;
  max-width: 34em;
}

/* What a listing carries as a set: the extra categories, the things they offer. */
.tags { list-style: none; margin: var(--s3) 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s2); }

.tag {
  padding: 6px var(--s3);
  border: 1px solid var(--rule);
  font-size: var(--fs-small);
  color: var(--ink-2);
}

/* The photographs, side by side, scrolled with a finger or a trackpad. No JavaScript: it is a list
   that scrolls, which is what a carousel is once the arrows are taken off it. Snap so a photo comes
   to rest against the edge rather than half way. */
.reel {
  list-style: none;
  margin: var(--s3) 0 0;
  padding: 0 0 var(--s2);
  display: flex;
  gap: var(--s3);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}

.reel-item {
  flex: 0 0 auto;
  width: 260px;
  scroll-snap-align: start;
  border: 1px solid var(--rule);
}

.reel-item img { display: block; width: 100%; height: 176px; object-fit: cover; }

@media (hover: none) and (max-width: 480px), (pointer: coarse) and (max-width: 480px), (any-pointer: coarse) and (max-width: 480px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (max-width: 384px) {
  .reel-item { width: 78%; }
  .reel-item img { height: 200px; }
}

/* ---------------------------------------------------------------- reviews, questions, posts

   Danny, 10 September 2026: three pages that were row lists, which made other people's words look
   like a table of figures. One grammar for all three instead: the marker down the left (the stars,
   the state, the number), the words beside it, and anything that answers them set in underneath
   behind a hairline. */
.rating { display: flex; align-items: baseline; gap: var(--s4); }
.rating-num { font-size: var(--fs-display); font-weight: 300; line-height: 1; color: var(--ink); }
.rating-of { display: flex; flex-direction: column; gap: var(--s1); }
.rating-count { font-size: var(--fs-small); color: var(--ink-2); }

/* Five stars, filled to the rating. Drawn here rather than fetched: no external resources, and a
   glyph would bring a second typeface with it. */
.stars { display: inline-flex; gap: 2px; color: var(--ink); }
.stars svg { display: block; }

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

.entry {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);
  gap: var(--s4);
  padding: var(--s4) 0;
  border-top: 1px solid var(--rule);
}

.entries > .entry:first-child { border-top: 0; padding-top: 0; }
.entry-mark { display: flex; flex-direction: column; gap: var(--s2); }
.entry-when { font-size: var(--fs-small); color: var(--ink-2); }
.entry-num { font-size: var(--fs-h2); font-weight: 300; line-height: 1; color: var(--ink); }
.entry-body { min-width: 0; }
.entry-who { margin: 0 0 var(--s2); font-size: var(--fs-body); color: var(--ink); }
.entry-text { margin: 0; font-size: var(--fs-body); line-height: 1.5; color: var(--ink-2); }
.entry-said { color: var(--ink); }

/* An answer belongs to its question, and a reply to its review, so it is set in behind a hairline
   rather than floating underneath as another row. */
.entry-reply {
  margin-top: var(--s3);
  padding-left: var(--s3);
  border-left: 1px solid var(--rule-2);
}

.entry-reply .label { margin-bottom: var(--s1); }
.entry-reply p { margin: 0; font-size: var(--fs-small); line-height: 1.5; color: var(--ink-2); }

.entry-waiting {
  margin: var(--s3) 0 0;
  font-size: var(--fs-small);
  color: var(--ink-2);
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/* Below 480 the marker sits above the words rather than squeezing them into half a column. */
@media (hover: none) and (max-width: 480px), (pointer: coarse) and (max-width: 480px), (any-pointer: coarse) and (max-width: 480px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (max-width: 384px) {
  .entry { grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
  .entry-mark { flex-direction: row; align-items: baseline; gap: var(--s3); }
}

/* ---------------------------------------------------------------- the migration page, at the Site tab's sizes
   Danny, 22 September 2026, with the page set plain: "all the text and elements could be a bit
   smaller, some of the text is massive compared to the frontend page", and then, pointing at the
   Frontend page's "No updates to action right now" block: "all text should be sized like this".

   Measured against that page at 505x928, two things were bigger and nothing else was. The
   paragraphs here were 15px where its muted paragraph is 13px; the list rows here were 52px where
   its card rows are 44px. Labels (11px), row text (15px) and buttons (11px) were already the same.
   So this block is those two things, and it is not the shrink-everything of this morning: the
   heading, the labels, the rows' own type and the 32px between sections are all left exactly as
   the Site tab has them.

   `.sec > p.muted`, not `p`: the row notes (.row-sub) are 13px already, and a bare `p` would also
   catch any error line the watching state prints, which should read at the size errors read at. */
.migrate-page .sec > p.muted { font-size: var(--fs-small); line-height: 1.5; }
/* The same pair `.card-tight .list-row` sets for the Site tab's card. Both are needed: the app's
   rows have a 52px floor (--row-min), so padding alone changed nothing and the rows stayed at 52.
   The floor comes down to the 44px a finger needs and the padding to s2, and that is the Site
   tab's row, here on a hairline list instead of in a card. */
.migrate-page .list-row { min-height: 44px; padding: var(--s2) 0; }
/* The name in a box of its own on the signing page (Danny, 22 September 2026: "put the your name
   signature bit in its own box too"), the way the request sheet boxes the message. The input gives
   up its own underline, because the box is the edge now, and the box takes the focus that the
   underline used to show: it darkens to ink while the name is being typed. */
.migrate-page .card-sign { padding: var(--s2) var(--s3); }
.migrate-page .card-sign input { border-bottom: 0; }
.migrate-page .card-sign:focus-within { border-color: var(--ink); }

/* The signing page, a touch more open than the page before it (Danny, 22 September 2026: "it's
   nice, but it could be spread out a tiny bit more than that"). Three small steps, not an engine:
   rows 44 to 48, the gap between the fields 16 to 24, the gap above the button 24 to 32. About 48px
   across the page at 505x928, which still fits with room. Only this page: the description page he
   had just called nice keeps .migrate-page's sizes.

   `.field + .field`, not `.field`: the bare class would also beat `.field:first-child { margin-top: 0 }`
   and open a gap under the heading that nobody asked for. */
.sign-page .list-row { min-height: 48px; }
.sign-page .form > .field + .field { margin-top: var(--s5); }
.sign-page .actions { margin-top: var(--s6); }

/* ---------------------------------------------------------------- the SEO tab, a little short of the foot
   Danny, 22 September 2026: "on the SEO tabs, both halves, the buttons are too big and touch the
   bottom area a bit too much. Make them all equally ever so slightly smaller, so it has the same
   effect as SITE does, where it's not touching the bottom header so closely."

   The tiles on both halves grow to fill the column (`.sec-grow` and `.way`, above), so their size
   is whatever is left after everything fixed has been laid out, and the way to make every one of
   them a touch smaller by the same amount is to leave a little less for them to fill. This takes
   it from the foot: the epigraph's top padding goes from 16px to 80px on this tab only, so the last
   tile stops 80px above the epigraph instead of 16, and the 64px comes off the tiles in equal
   shares. Measured at 505x928: What we do 146px to 130px a tile, What you do 76px to about 67.
   (48 was the first answer; "still a bit too low to the bottom".)

   `.col.seo-tab > .folio` outranks `.fit .folio` and `.col > .folio`, which both set s4. */
.col.seo-tab > .folio { padding-top: calc(var(--s8) + var(--s4)); }
/* And a real break between the two headed sections of What you do (Danny, 22 September 2026:
   "the gap between Set these up and Keep these going: Keep these going is too close to the one
   above. Make those boxes all equally smaller to buy some room"). `.fit .sec` puts 8px between
   sections, which is right where a section runs straight on from the one before and wrong where
   the second one opens with a hairline and a heading: the heading sat 21px under the last tile.
   48 here, so it sits 61px under it. The seven tiles pay for the 40px in equal shares, as with
   the foot above; What we do has one headed section and is not touched. */
.col.seo-tab .sec-grow + .sec-grow { margin-top: var(--s7); }

/* ---------------------------------------------------------------- the way in (auth.enter)
   Danny, 3 October 2026: the landing site's Sign in and Sign up buttons land here first. Two cards,
   side by side from 900px: Sign in (opens the app in a new tab) and Sign up (the onboarding sheet). */

.enter {
  display: grid;
  gap: var(--s5);
  margin-top: var(--s5);
}

@media (min-width: 900px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (min-width: 720px) {
  .enter { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
}

.enter-card { padding: var(--s5); }
.enter-card .actions { margin-top: var(--s4); }
.enter-out { margin-top: var(--s3); }

/* ---------------------------------------------------------------- boxed fields (the way in)
   Danny, 3 October 2026, on the sign up sheet: "make these areas in boxes". The pages before the app
   (the way in, sign in, the four onboarding steps) set col_class "boxed": every text field, list and
   note sits in a hairline box like the cards; the app's own forms keep their underline. */

.boxed input[type="text"], .boxed input[type="email"], .boxed input[type="password"], .boxed input[type="url"],
.boxed input[type="number"], .boxed input[type="search"], .boxed input[type="date"], .boxed input[type="datetime-local"],
.boxed textarea, .boxed select {
  border: 1px solid var(--rule-2);
  padding: var(--s2) var(--s3);
}

.boxed input:focus, .boxed textarea:focus, .boxed select:focus { border-color: var(--ink); }
.boxed input:disabled, .boxed textarea:disabled, .boxed select:disabled { border-color: var(--rule); }
.boxed .has-err input, .boxed .has-err textarea, .boxed .has-err select { border-color: var(--ink); }
.boxed select { background-position: right var(--s3) center; padding-right: 40px; }

/* ---------------------------------------------------------------- the CREATE tab as the landing shows it
   create/preview.html (Danny, 3 October 2026), rendered only by scripts/shoot_app.py for Jane's Bakery:
   the ask (a boxed brief, what to make, Create) over a grid of what the engine made. The designs are
   photographs with type laid over them, and two drawn in CSS (the logo, the menu card). */

.cr-ask { margin-top: var(--s5); }
.cr-ask textarea { min-height: 88px; }
.cr-kinds-label, .cr-made-label { margin-top: var(--s5); }
.cr-kinds { list-style: none; margin: var(--s2) 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s2); }
.cr-kind {
  display: inline-flex; align-items: center; min-height: 36px; padding: 0 var(--s3);
  border: 1px solid var(--rule-2); font-size: 11px; font-weight: 500; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-2);
}
.cr-kind.on { background: var(--ink); border-color: var(--ink); color: #000; }
.cr-ask .actions { margin-top: var(--s4); }

.cr-grid { list-style: none; margin: var(--s3) 0 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4) var(--s3); }
@media (min-width: 600px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (min-width: 480px) { .cr-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.cr-tile {
  position: relative; aspect-ratio: 4 / 5; overflow: hidden; display: flex; flex-direction: column;
  justify-content: flex-end; padding: var(--s3); border: 1px solid var(--rule); color: #fff;
}
.cr-tile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.cr-tile::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,.72), rgba(0,0,0,0) 62%); pointer-events: none; }
.cr-over { position: relative; z-index: 1; display: grid; gap: 4px; }
.cr-small { font-size: 9px; font-weight: 500; letter-spacing: .16em; text-transform: uppercase; opacity: .85; }
.cr-head { font-family: Georgia, "Times New Roman", serif; font-size: 19px; line-height: 1.12; }
.cr-story .cr-head { font-size: 22px; }

.cr-logo { background: #f3ece1; color: #3b2a1e; align-items: center; justify-content: center; text-align: center; gap: 6px; border-color: #f3ece1; }
.cr-logo::after, .cr-menu::after { display: none; }
.cr-logo .cr-mark { width: 44px; height: 44px; }
.cr-logo .cr-word { font-family: Georgia, "Times New Roman", serif; font-size: 30px; line-height: 1; font-style: italic; }
.cr-logo .cr-small { opacity: 1; }

.cr-menu { background: #1d1612; border-color: #3a2c22; justify-content: flex-start; gap: 6px; color: #f3ece1; }
.cr-menu-h { font-family: Georgia, "Times New Roman", serif; font-size: 18px; margin-bottom: 4px; }
.cr-row { display: flex; justify-content: space-between; gap: var(--s2); font-size: 11px; padding: 5px 0; border-top: 1px solid rgba(243,236,225,.18); }
.cr-row span:last-child { font-variant-numeric: tabular-nums; }

.cr-play { position: absolute; z-index: 1; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 44px; height: 44px; border-radius: 50%; border: 1px solid #fff; display: grid; place-items: center; background: rgba(0,0,0,.25); }
.cr-time { position: absolute; z-index: 1; top: var(--s2); right: var(--s2); font-size: 10px; font-variant-numeric: tabular-nums; background: rgba(0,0,0,.55); padding: 2px 6px; }
.cr-dots { position: relative; z-index: 1; display: flex; gap: 4px; margin-top: var(--s2); }
.cr-dots i { width: 5px; height: 5px; border-radius: 50%; background: rgba(255,255,255,.45); }
.cr-dots i.on { background: #fff; }

.cr-cap { display: flex; justify-content: space-between; gap: var(--s2); margin-top: var(--s2); font-size: 12px; color: var(--ink-2); }
.cr-state { font-size: 10px; letter-spacing: .12em; text-transform: uppercase; }
.cr-state.ready { color: var(--ink); }

/* ---------------------------------------------------------------- the SEO tab on the landing's phone

   tabs/seo_preview.html, which only scripts/shoot_app.py renders. Two tiles a row at every width, so
   the four figures read as a block above the chart. */
.sp-score { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3); margin-top: var(--s5); }
.sp-score .score { margin: 0; }
.sp-up { font-size: var(--fs-small); color: var(--ink); letter-spacing: .04em; white-space: nowrap; }
/* `.fit .tiles` runs tiles along one line that scrolls sideways, and `.fit .chart` holds the chart to
   120px: this page has the room, so the tiles go two by two and the chart takes its full height. */
.fit .tiles.sp-tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; overflow: visible; }
.fit .tiles.sp-tiles > .tile { flex: none; }
.fit .chart.sp-chart { height: auto; }
.fit .chart.sp-chart svg { height: 190px; }

/* ---------------------------------------------------------------- add the app to your phone

   _install.html, the first time someone signs in (Danny, 4 October 2026). A small window in the middle
   of the screen over a dimmed app, the SEO tab's switch (.seg) for iPhone and Android, four numbered
   steps and "Got it". Inside the installed app it never shows: they have already done it. */
.install {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s4);
  background: rgba(0, 0, 0, .72);
}

.install[hidden] { display: none; }

.install-box {
  width: 100%;
  max-width: 340px;
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: none;
  padding: var(--s5) var(--s4) var(--s4);
  background: var(--bg);
  border: 1px solid var(--rule-2);
}

.install-title { margin: 0; font-size: var(--fs-h2); font-weight: 400; line-height: 1.25; color: var(--ink); }
.install-lead { margin: var(--s2) 0 0; font-size: var(--fs-small); line-height: 1.5; color: var(--ink-2); }

/* The switch is buttons here, not links: nothing reloads and there is no ?view= to keep. */
.install-seg { margin: var(--s4) 0; }
.install-seg .seg-opt { background: none; border: 0; font-family: inherit; cursor: pointer; }

.install-steps { margin: 0; padding: 0; list-style: none; counter-reset: step; }
/* The number hangs in the margin rather than taking a grid column: a step is running text with the
   address inside it, and a grid made "Open", the address and "in Safari" three cells. */
.install-steps li {
  counter-increment: step;
  position: relative;
  padding: var(--s2) 0 var(--s2) 36px;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink);
}

.install-steps li:first-child { border-top: 0; padding-top: 0; }
.install-steps li::before {
  content: "0" counter(step);
  position: absolute;
  left: 0;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: .12em;
  color: var(--ink-2);
  padding-top: 2px;
}

.install-url { overflow-wrap: anywhere; text-decoration: underline; text-underline-offset: 3px; }
.install-done { margin-top: var(--s4); }

@media (display-mode: standalone) {
  .install { display: none; }
}

/* ---------------------------------------------------------------- the SITE tab's Frontend half on a phone

   Danny, 4 October 2026: "make it so that page you dont have to scroll. even if things needs to be a tad
   smaller". The card lost two rows (the startup pack is on Documents, the account ref on Account), and
   on a phone the half packs a little tighter: less air between sections, a shorter card row, and the
   "No updates" line at body size rather than a heading. Desktop keeps its spacing. The query carries
   the 80% desktop twin like every other breakpoint here (see the top of this file). */
@media (hover: none) and (max-width: 599.98px), (pointer: coarse) and (max-width: 599.98px), (any-pointer: coarse) and (max-width: 599.98px), (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) and (max-width: 479.98px) {
  .view-front .sec { margin-top: var(--s4); }
  .view-front .card-tight .list-row { padding: 6px 0; }
  .view-front .sec-update > .actions { margin-top: var(--s3); margin-bottom: var(--s4); }
  .view-front .sec-update > .h2 { font-size: var(--fs-body); margin-bottom: var(--s1); }
  .view-front .sec-update > .muted { margin-bottom: var(--s4); }
  .view-front .actions-lead { margin-top: var(--s3); margin-bottom: var(--s3); }
}

/* One document inside the app (site/document_view.html, static/pdfview.js; Danny, 7 October 2026:
   "it needs a back button or a way of getting back"). Each page keeps its shape before it is drawn
   so the page does not jump as the pages arrive; the links in the file sit over it as real links. */
.doc-view .pdf { display: flex; flex-direction: column; gap: var(--s3); }
.pdf-page { position: relative; width: 100%; background: var(--fill-off); border: 1px solid var(--rule); overflow: hidden; }
.pdf-page canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
.pdf-link { position: absolute; z-index: 1; display: block; }
.pdf-failed { display: flex; flex-direction: column; gap: var(--s4); }
.pdf-failed[hidden] { display: none; }
.doc-img { display: block; width: 100%; height: auto; border: 1px solid var(--rule); }
