/* Plain CSS, mobile-first for the field app. The palette and type are the marketing site's
   (marketing/src/styles/global.css): warm paper, brown ink, olive and ochre, Fraunces over
   Archivo. A customer who follows "See my price" from getdoodiligence.com should land somewhere
   that looks like the same business. None of the marketing site's scenery comes across (the
   grain, the torn edges, the luggage tag); this is a working tool and stays plain.

   Every text pair below was measured: 4.5:1 or better for text, 3:1 for borders and controls.
   Change a colour and measure it again, in both themes.

   The fonts are the marketing site's own files, copied from its node_modules (@fontsource,
   SIL Open Font License), Latin subset only. */

@font-face {
  font-family: "Fraunces Variable";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("/assets/fraunces-latin-wght-normal-0f433db6.woff2") format("woff2-variations");
}
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url("/assets/archivo-latin-400-normal-a45e3aff.woff2") format("woff2");
}
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url("/assets/archivo-latin-500-normal-d570699f.woff2") format("woff2");
}
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url("/assets/archivo-latin-600-normal-cafc4a45.woff2") format("woff2");
}

:root {
  --display: "Fraunces Variable", Georgia, serif;
  --body: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

  --paper: #F2E6D0;
  --card: #FBF6EC;           /* a shade lighter than the paper, so a card lifts off it */
  --field: #FFFCF6;
  --ink: #4A3B2A;            /* 10:1 on --card */
  --muted: #6E5B45;          /* 5.24:1 on --paper, 6.01:1 on --card */
  --olive: #3F5A2E;          /* links; 7.19:1 on --card */
  --olive-bright: #5A7A3F;
  --ochre: #C0663F;          /* display only: the wordmark, 3.3:1, large text */
  --cta: #9A4419;            /* the marketing site's "See my price" */
  --cta-ink: #F7EEDC;        /* 5.67:1 on --cta */
  --line: rgb(74 59 42 / 16%);
  --line-strong: rgb(74 59 42 / 28%);
  --field-line: #8C7A63;     /* 4.04:1 on --field; a control's edge needs 3:1 */

  /* Status tints. Each ink is measured on its own tint. */
  --ok-bg: #DFE6CF;      --ok-ink: #3F5A2E;     /* 6.03:1 */
  --warn-bg: #F5DCC4;    --warn-ink: #8A3F16;   /* 5.68:1 */
  --bad-bg: #F6D5CB;     --bad-ink: #9B2A1C;    /* 5.58:1 */
  --neutral-bg: #E6D8C2; --neutral-ink: #4A3B2A; /* 7.67:1 */
  --danger: #9B2A1C;     --danger-ink: #F7EEDC; /* 6.64:1 */
}

/* Dark follows the device until someone pulls the header's light switch, the way the marketing
   site behaves. A pull sets data-theme on <html> (layouts/_theme_head, theme_controller.js), and
   the choice has to win both ways: :not() keeps the device's dark away from a page switched to
   light, and the attribute block below carries the same dark values for a light device switched
   to dark. The two blocks are the same list; change one, change both. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #2A2318;
    --card: #342B1E;
    --field: #231D14;
    --ink: #E8DCC8;          /* 10.26:1 on --card */
    --muted: #B5A08A;        /* 5.54:1 on --card */
    --olive: #A8C87A;        /* 7.42:1 on --card */
    --olive-bright: #C0D99A;
    --ochre: #E8A870;
    --cta: #E8A870;
    --cta-ink: #2A2318;      /* 7.59:1 */
    --line: rgb(232 220 200 / 16%);
    --line-strong: rgb(232 220 200 / 32%);
    --field-line: #8A7862;   /* 3.93:1 on --field */

    --ok-bg: #3A4128;      --ok-ink: #C0D99A;     /* 6.93:1 */
    --warn-bg: #4A3321;    --warn-ink: #EDB485;   /* 6.42:1 */
    --bad-bg: #4D2A22;     --bad-ink: #F2A596;    /* 6.35:1 */
    --neutral-bg: #463825; --neutral-ink: #E8DCC8; /* 8.37:1 */
    --danger: #F2A596;     --danger-ink: #2A2318; /* 7.84:1 */
  }
}

:root[data-theme="dark"] {
  --paper: #2A2318;
  --card: #342B1E;
  --field: #231D14;
  --ink: #E8DCC8;          /* 10.26:1 on --card */
  --muted: #B5A08A;        /* 5.54:1 on --card */
  --olive: #A8C87A;        /* 7.42:1 on --card */
  --olive-bright: #C0D99A;
  --ochre: #E8A870;
  --cta: #E8A870;
  --cta-ink: #2A2318;      /* 7.59:1 */
  --line: rgb(232 220 200 / 16%);
  --line-strong: rgb(232 220 200 / 32%);
  --field-line: #8A7862;   /* 3.93:1 on --field */

  --ok-bg: #3A4128;      --ok-ink: #C0D99A;     /* 6.93:1 */
  --warn-bg: #4A3321;    --warn-ink: #EDB485;   /* 6.42:1 */
  --bad-bg: #4D2A22;     --bad-ink: #F2A596;    /* 6.35:1 */
  --neutral-bg: #463825; --neutral-ink: #E8DCC8; /* 8.37:1 */
  --danger: #F2A596;     --danger-ink: #2A2318; /* 7.84:1 */
}

:root { color-scheme: light dark; }
/* Native controls (the date picker, select menus, scrollbars) follow a pulled switch too. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--body);
  color: var(--ink);
  background: var(--paper);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--olive); text-underline-offset: 0.2em; text-decoration-thickness: 1px; }
a:hover { color: var(--ink); }

:where(a, button, input, select, textarea, summary):focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
}

/* Read aloud, never shown: a column header over a row of buttons, say. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* First in the tab order and off screen until a keyboard lands on it. */
.skip-link {
  position: absolute; left: 1rem; top: -3rem; z-index: 10;
  padding: 0.5rem 0.9rem; border-radius: 4px;
  background: var(--card); color: var(--ink); border: 1px solid var(--line-strong);
}
.skip-link:focus { top: 0.5rem; }

.wrap { max-width: 720px; margin: 0 auto; padding: 1.25rem 1rem 4rem; }

/* The marketing site's masthead bar, without the picture above it: the wordmark on the paper
   and a rule underneath. The bar runs edge to edge; the padding lines its contents up with
   the column below, so the wordmark sits over the left edge of the page, not the screen's. */
.topnav {
  --col: 720px;
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: 0.5rem 1rem;
  padding: 0.9rem max(1rem, calc(50% - var(--col) / 2 + 1rem));
  border-bottom: 1px solid var(--line-strong);
}
.topnav.narrow { --col: 520px; }
.topnav .brand {
  font-family: var(--display); font-weight: 700; font-size: 1.25rem; letter-spacing: -0.02em;
  color: var(--ink); text-decoration: none;
}
.topnav .brand span { color: var(--ochre); }
.topnav .links { display: flex; align-items: center; gap: 1rem; margin-left: auto; }
.topnav .links a, .topnav .links > span {
  color: var(--muted); text-decoration: none;
  font-size: 0.8rem; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase;
}
.topnav .links a:hover { color: var(--ink); }
/* The section you are in. An underline as well as the ink, so it does not rest on colour alone. */
.topnav .links a[aria-current] {
  color: var(--ink);
  text-decoration: underline; text-decoration-color: var(--ochre);
  text-decoration-thickness: 2px; text-underline-offset: 0.4em;
}

/* The trail above a staff page's heading (ApplicationHelper#breadcrumbs). It sits where the old
   "← Parent" link did, in the same small muted type, so the heading still leads. The slashes
   are drawn, not written, so a screen reader reads the levels and not the punctuation. */
.crumbs ol {
  display: flex; flex-wrap: wrap; gap: 0.25rem 0;
  margin: 0; padding: 0; list-style: none;
  font-size: 0.9rem; color: var(--muted);
}
.crumbs li + li::before { content: "/"; margin: 0 0.5rem; color: var(--line-strong); }
.crumbs a { color: var(--muted); }
.crumbs a:hover { color: var(--ink); }
.crumbs [aria-current] { color: var(--ink); }

/* ---------- light switch ----------

   The marketing site's pull-chain bulb (marketing/src/styles/global.css). It hangs from its cord,
   so a pull swings it about the top. 44x48 is the hit area; the drawing is 24x40, too small for a
   thumb on its own. The header's padding is trimmed by the bulb's overhang so it does not make
   the bar taller than the wordmark does.

   It follows the name in the markup and sits last on the bar by `order`, because on a phone the
   links wrap onto a row of their own. Left among them, the bulb went down too and hung in mid-air
   with its cord cut; this way it stays on the top row beside the name, as on the marketing site. */
.theme-bulb {
  order: 1;
  display: grid; place-items: start center;
  width: 44px; height: 48px; margin-block: -0.9rem -0.4rem; padding: 0;
  border: 0; background: none; color: var(--ink); cursor: pointer;
  transform-origin: 50% 0;
  -webkit-tap-highlight-color: transparent;
}
/* No links (signed out, or the quote funnel): nothing pushes the bulb right, so it pushes itself. */
.topnav .brand + .theme-bulb:last-child { margin-left: auto; }
@media (max-width: 29.99rem) {
  .topnav .theme-bulb { margin-left: auto; }
  .topnav .links { order: 2; flex-basis: 100%; }
}
/* display: grid above would otherwise beat the attribute. */
.theme-bulb[hidden] { display: none; }
.theme-bulb svg { display: block; width: 24px; height: 40px; overflow: visible; }
.theme-bulb:focus-visible { border-radius: 6px; }

.bulb-cord,
.bulb-chain { fill: none; stroke: currentColor; stroke-width: 1.2; stroke-linecap: round; }
.bulb-chain { stroke-dasharray: 0.1 1.7; stroke-width: 1.6; }
.bulb-socket,
.bulb-bead { fill: currentColor; }
.bulb-glass { stroke: currentColor; stroke-width: 1.2; transition: fill 0.25s ease, filter 0.25s ease; }
.bulb-filament {
  fill: none; stroke: currentColor; stroke-width: 0.7;
  stroke-linecap: round; stroke-linejoin: round; opacity: 0.75;
}

/* Lit: warm glass and a soft glow on the glass alone, so the cord and socket stay crisp. */
.theme-bulb[aria-pressed="false"] .bulb-glass {
  fill: #F6D48A;
  filter: drop-shadow(0 0 5px rgb(246 212 138 / 85%));
}
/* Off: the glass takes the paper's colour, so only the outline is left. */
.theme-bulb[aria-pressed="true"] .bulb-glass { fill: var(--paper); filter: none; }

@keyframes bulb-swing {
  0%   { rotate: 0deg; }
  18%  { rotate: 9deg; }
  40%  { rotate: -6deg; }
  62%  { rotate: 3.5deg; }
  82%  { rotate: -1.5deg; }
  100% { rotate: 0deg; }
}
.theme-bulb.is-swinging { animation: bulb-swing 1.1s cubic-bezier(0.2, 0.7, 0.3, 1); }

@media (prefers-reduced-motion: reduce) {
  .theme-bulb.is-swinging { animation: none; }
  .bulb-glass { transition: none; }
}

/* A button that reads as a link. The default is for a light surface — cards and tables, where
   most of them live. It used to be the nav's pale mint, which left the portal's Skip button
   near-invisible on a white card; the nav now carries its own colour instead of setting the
   default for everyone. */
.linkish {
  background: none; border: none; color: var(--olive); cursor: pointer;
  font-size: 0.95rem; padding: 0; font-family: inherit;
  text-decoration: underline; text-underline-offset: 0.2em; text-decoration-thickness: 1px;
}
.linkish:hover { color: var(--ink); }

.topnav .linkish {
  color: var(--muted); text-decoration: none; line-height: inherit;
  font-size: 0.8rem; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase;
}
.topnav .linkish:hover { color: var(--ink); }
/* `button_to` wraps its button in a form, and the stacked-form rules below would otherwise
   give it a field's bottom margin. */
.topnav form { margin: 0; display: flex; }

h1, h2, h3 {
  font-family: var(--display); font-weight: 600; letter-spacing: -0.018em; line-height: 1.15;
  text-wrap: balance;
}
h1 { font-size: 1.9rem; margin: 0.75rem 0 1rem; }
h2 { font-size: 1.3rem; margin: 1.75rem 0 0.6rem; }
h3 { font-size: 1.1rem; margin: 1.5rem 0 0.5rem; }
.muted { color: var(--muted); }

.flash { padding: 0.65rem 0.9rem; border-radius: 4px; margin-bottom: 1rem; font-size: 0.95rem; }
.flash.notice { background: var(--ok-bg); color: var(--ok-ink); }
.flash.alert  { background: var(--bad-bg); color: var(--bad-ink); }
.flash.warn   { background: var(--warn-bg); color: var(--warn-ink); }

.card {
  background: var(--card); border: 1px solid var(--line-strong); border-radius: 4px;
  padding: 1rem 1.1rem; margin-bottom: 0.75rem;
}
.card .addr { color: var(--muted); font-size: 0.9rem; }
.card .meta { font-size: 0.85rem; color: var(--muted); margin-top: 0.35rem; }

.pill { display: inline-block; padding: 0.1rem 0.55rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
.pill.scheduled { background: var(--neutral-bg); color: var(--neutral-ink); }
.pill.completed { background: var(--ok-bg); color: var(--ok-ink); }
.pill.skipped   { background: var(--warn-bg); color: var(--warn-ink); }
.pill.unable    { background: var(--bad-bg); color: var(--bad-ink); }
/* Plan lifecycle (wf-2), which shares the pill but not the visit's vocabulary. */
.pill.active    { background: var(--ok-bg); color: var(--ok-ink); }
.pill.paused    { background: var(--warn-bg); color: var(--warn-ink); }
.pill.cancelled { background: var(--neutral-bg); color: var(--neutral-ink); }
/* Money owed on an abandoned signup Checkout (#17). Its own class rather than `unable`, which
   is a visit outcome — they share a red today and have no reason to move together. */
.pill.unpaid    { background: var(--bad-bg); color: var(--bad-ink); }

.card-head { display: flex; justify-content: space-between; align-items: baseline; }

/* A question on the Leads page (wf-27): what they asked, set in from the contact lines above it,
   then the owner's note and the buttons that finish it. Delete is red, like the grid's. */
.question-body { margin: 0.75rem 0; padding-left: 0.85rem; border-left: 3px solid var(--line-strong); }
.question-body p, .question-note p { margin: 0.25rem 0; }
.question-note { font-size: 0.9rem; margin-bottom: 0.5rem; }
.question .actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin-top: 0.5rem; }
.question .actions .linkish { color: var(--bad-ink); }
.question .actions .linkish:hover { color: var(--ink); }
/* A safe action (Reopen) beside Delete, olive like the grid's `.go`. */
.question .actions .linkish.go { color: var(--olive); }
.question .actions .linkish.go:hover { color: var(--ink); }
/* "Have a question before you book?" folds shut under the booking form (wf-27, wf-30). The h2
   stays a heading inside the summary, so it keeps its size and reads as the box's title. */
.question-box summary { cursor: pointer; }
.question-box summary h2 { display: inline; margin: 0; }
.question-box[open] summary { margin-bottom: 0.75rem; }
.done-questions summary { cursor: pointer; font-weight: 600; color: var(--olive); margin: 0.75rem 0; }

/* A plan-level lever (pause / resume), set off from the plan's own details above it. */
.lever { margin-top: 0.85rem; padding-top: 0.85rem; border-top: 1px solid var(--line); }
.lever summary { cursor: pointer; font-size: 0.9rem; color: var(--olive); font-weight: 600; }
.lever p { margin: 0.6rem 0; }
.lever form { margin-top: 0.85rem; }

/* The marketing site's call to action: square-cornered, rust, and it brightens rather than
   darkens under the pointer. */
.btn {
  display: inline-block; background: var(--cta); color: var(--cta-ink);
  border: 1px solid transparent;
  padding: 0.8rem 1.2rem; border-radius: 3px; font-size: 1rem; font-weight: 600;
  letter-spacing: 0.01em; text-decoration: none; cursor: pointer; font-family: inherit;
}
.btn:hover { color: var(--cta-ink); filter: brightness(1.08); }
.btn.wide { display: block; width: 100%; text-align: center; }
/* A paragraph of buttons (the console's shortcuts) wraps on a phone, and inline buttons wrap
   with their edges touching. A gap keeps each one its own target. */
p:has(> .btn) { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.btn.secondary { background: var(--card); color: var(--cta); border-color: var(--cta); }
.btn.secondary:hover { color: var(--cta); }
/* The exception ending (wf-8) — red because it closes the visit without servicing it. */
.btn.danger { background: var(--danger); color: var(--danger-ink); }

/* The folded-away "couldn't service" flow: present on every open visit, never in the way of
   the button the tech actually taps. */
.exception summary { font-weight: 600; cursor: pointer; }
.exception[open] summary { margin-bottom: 0.85rem; }

.stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; margin: 1rem 0; }
.stat { background: var(--card); border: 1px solid var(--line-strong); border-radius: 4px; padding: 1rem 1.1rem; }
.stat .n { font-family: var(--display); font-size: 2rem; font-weight: 600; line-height: 1.2; }
.stat .l { color: var(--muted); font-size: 0.85rem; }

form .field { margin-bottom: 0.85rem; }
form label { display: block; font-size: 0.85rem; color: var(--muted); margin-bottom: 0.25rem; }
form input, form select {
  width: 100%; padding: 0.6rem; border: 1px solid var(--field-line); border-radius: 3px;
  font-size: 1rem; font-family: inherit; color: var(--ink); background: var(--field);
}
/* A form that is one lever rather than a page of data entry: a picker and the button that acts on
   what it holds. The stacked default above is right for entering a customer and wrong here, where
   two controls are a single action and a full-card-width select reads as an unfinished field.
   Wraps rather than overflowing once the line runs out. */
form.inline { display: flex; flex-wrap: wrap; gap: 0.5rem 0.75rem; align-items: center; }
/* Every control, not a list of the types that happened to be here. Naming `select` and `submit`
   individually meant the day-move's date picker (wf-8) fell through to the stacked default above,
   stretched the full card and pushed its own button onto the next line — a lever that looked
   unlike the identical one sitting right above it. */
form.inline input, form.inline select { width: auto; max-width: 100%; }

/* The SMS consent box (wf-9). Left to the rules above, `form input { width: 100% }` stretches a
   checkbox to the full card and `form label { display: block }` drops its text onto the next
   line — the same defect the day-move picker hit. A consent control that doesn't look like a
   checkbox is worse than ugly: the carrier reviewing the campaign is looking at this. */
.consent label {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 1rem; color: var(--ink); margin-bottom: 0.4rem;
}
.consent input[type="checkbox"] { width: auto; flex: none; margin: 0; }
.consent p { margin: 0 0 0.35rem; }

fieldset { border: 1px solid var(--line-strong); border-radius: 4px; padding: 1rem; margin-bottom: 1rem; }
legend { font-weight: 600; padding: 0 0.4rem; }

.errors { background: var(--bad-bg); color: var(--bad-ink); border-radius: 4px; padding: 0.75rem; margin-bottom: 1rem; }
.errors ul { margin: 0.25rem 0 0; padding-left: 1.1rem; }

.empty { text-align: center; color: var(--muted); padding: 2rem 1rem; }

.login { max-width: 360px; margin: 3rem auto; }
.row { display: flex; gap: 0.75rem; }
.row > * { flex: 1; }

form textarea {
  width: 100%; padding: 0.6rem; border: 1px solid var(--field-line); border-radius: 3px;
  font-size: 1rem; font-family: inherit; color: var(--ink); background: var(--field);
}
.small { font-size: 0.85rem; }

/* The public quote/signup funnel (wf-4). */
.wrap.narrow { max-width: 520px; }

.billing-dates { border-left: 3px solid var(--muted); padding-left: 0.75rem; margin: 0.75rem 0; }
.billing-dates p { margin: 0.35rem 0; }
.quote-price { text-align: center; padding: 1.5rem 1rem; }
.quote-price .headline { font-family: var(--display); font-size: 2.6rem; font-weight: 600; margin: 0; line-height: 1.1; }
.quote-price .per { font-size: 1rem; font-weight: 500; color: var(--muted); }
.quote-price hr { border: none; border-top: 1px solid var(--line); margin: 1rem 0; }
.quote-email { margin-top: 2rem; }
.quote-email h2 { font-size: 1.1rem; margin-top: 0; }

/* Owner console tables (pricing, waitlist). A wide one scrolls in its box, so a phone never
   scrolls the whole page sideways to read it. */
.table-scroll { overflow-x: auto; }
.grid { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
.grid th, .grid td { text-align: left; padding: 0.55rem 0.5rem; border-bottom: 1px solid var(--line); }
.grid th { color: var(--muted); font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; border-bottom-color: var(--line-strong); }
/* Not `display: flex` — that drops the cell out of the table layout, so a tall row (a long
   failure reason) leaves the actions floating against the wrong baseline. */
.grid .actions { white-space: nowrap; vertical-align: middle; }
.grid .actions > * { display: inline-block; vertical-align: middle; margin-right: 0.75rem; }
.grid .actions > *:last-child { margin-right: 0; }
.grid .actions .linkish { color: var(--bad-ink); }
.grid .actions .linkish:hover { color: var(--ink); }
/* Non-destructive row action (retry a send) — red would read as "delete". */
.grid .actions .linkish.go { color: var(--olive); }
.grid .actions .linkish.go:hover { color: var(--ink); }

/* The skip lever is a picker *and* a button, where every other row action on the console is one
   short word. Under `.actions`' nowrap that pair cannot break, so the table grows past the
   viewport and the button leaves the right edge — reachable only by scrolling the whole page
   sideways, which nobody does on a page that looks finished. Wrap instead: side by side when
   there is room, stacked when there is not. */
.grid .actions.skip { white-space: normal; }
.grid .actions.skip form { display: flex; flex-wrap: wrap; gap: 0.4rem 0.6rem; align-items: center; }
.grid .actions.skip > * { margin-right: 0; }
/* Undoing the stacked-form default (`form input, form select { width: 100% }`), which is right
   for a signup column and wrong in a table cell: at full width each control claims its own line
   even where both would fit, so the row is twice as tall as it needs to be on any screen. */
.grid .actions.skip select,
.grid .actions.skip input[type="submit"] { width: auto; max-width: 100%; }

/* The closed-gate photo the completion email links to (wf-16). Constrained rather than
   full-bleed: the variant caps the pixels, this caps the layout. */
.proof { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.6rem; }
.proof img { max-width: 100%; width: 220px; border-radius: 3px; display: block; }

/* A dog walker's card code (wf-31). The SVG paints its own white, so it reads in the dark theme. */
.qr-code { max-width: 14rem; }
.qr-code svg { display: block; width: 100%; height: auto; }
