/* app.css — the entire design system for this product.
 *
 * One file, hand-written, no build step, no framework, no third-party request.
 * See specs/002-mobile-first-visual-design/ for why: research.md R1 (no framework),
 * R2 (why this file lives at project level and not inside an app).
 *
 * Three rules this file lives by. Breaking any of them breaks a requirement:
 *
 *   1. No colour, size, or spacing literal appears outside the :root block below.
 *      Every value is a custom property, referenced with var(). This is what keeps
 *      a future dark theme a token swap instead of a rewrite (research R13).
 *
 *   2. Exactly one exception to rule 1: the breakpoint in the single max-width
 *      @media prelude is written as a literal `48rem`. CSS custom properties are not
 *      valid there — `@media (max-width: var(--bp-wide))` raises no error and never
 *      matches, so the mobile table reflow would silently never apply. --bp-wide
 *      documents the value. Feature queries (prefers-reduced-motion, and later
 *      prefers-color-scheme) are not breakpoints and do not count against this.
 *
 *   3. Sizes are in rem, never px, and no element containing text gets a fixed height.
 *      Height comes from padding plus line-height. This is what survives 200% zoom and
 *      an enlarged OS text size (FR-008). px is allowed only for hairline borders and
 *      outline widths, plus the two pre-existing exceptions marked below.
 *
 * Never set `outline: none` anywhere (FR-024).
 */

/* ---------------------------------------------------------------- tokens --
 * Colour values are copied from research.md R3, where every foreground on every
 * background it can appear on was measured against WCAG before a line of this
 * file was written. Do not re-pick them by eye; if one changes, re-run the R3
 * table. Text needs 4.5:1, meaningful boundaries and the focus ring need 3:1.
 */
:root {
  /* Surfaces */
  --bg: #ffffff;
  --surface: #f8fafc;

  /* Text */
  --ink: #0f172a;               /* 17.85:1 on --bg */
  --ink-muted: #475569;         /*  7.58:1 on --bg, 7.24:1 on --surface */

  /* Accent — links, primary action, focus */
  --accent: #1d4ed8;            /*  6.70:1 on --bg */
  --on-accent: #ffffff;         /*  6.70:1 on --accent */
  --focus: #1d4ed8;             /*  6.70:1 on --bg, 6.41:1 on --surface */

  /* Semantic text, for use on --bg */
  --success: #15803d;           /*  5.02:1 on --bg */
  --warning: #b45309;           /*  5.02:1 on --bg */
  --danger: #b91c1c;            /*  6.47:1 on --bg */

  /* Semantic tints, and the darker ink each one is paired with. The -ink pair is
     what a semantic colour becomes when it sits on its own tint rather than white. */
  --success-tint: #f0fdf4;
  --success-ink: #166534;       /*  6.81:1 on --success-tint */
  --warning-tint: #fffbeb;
  --warning-ink: #92400e;       /*  6.84:1 on --warning-tint */
  --danger-tint: #fef2f2;
  --danger-ink: #991b1b;        /*  7.60:1 on --danger-tint */
  --accent-tint: #eff6ff;
  --accent-ink: #1e40af;        /*  8.01:1 on --accent-tint */

  /* Boundaries. --border is for any line that carries meaning: the edge of an
     input, a button outline, the boundary of a stacked record card. It clears 3:1
     because something depends on seeing it. --border-subtle is only for a line
     that could be deleted with no loss of meaning, such as a row rule inside a
     desktop table where the header row and column alignment already do the work.
     If removing the line would leave the reader unsure what belongs to what, it
     is --border. */
  --border: #64748b;            /*  4.76:1 on --bg, 4.55:1 on --surface */
  --border-subtle: #e2e8f0;     /*  decorative only, exempt under WCAG 1.4.11 */

  /* Type. A device font stack downloads nothing, so text is visible in its final
     face at first paint — no flash of invisible text, no third-party request. */
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --text-xs: 0.8125rem;
  --text-sm: 0.9375rem;
  --text-base: 1rem;            /* also the floor for form controls: below 16px,
                                   iOS Safari zooms the viewport on focus */
  --text-lg: 1.25rem;
  --text-xl: 1.5rem;
  --leading: 1.5;
  --leading-tight: 1.25;

  /* Spacing */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;

  /* Shape and measure */
  --radius: 0.375rem;
  --target: 2.75rem;            /* 44px minimum touch target, as a min-height plus
                                   padding — never a fixed height, which would clip
                                   enlarged text */
  --measure: 70ch;
  --page: 72rem;                /* page container cap. Wide enough for the 8-column
                                   delivery log to breathe on a desktop; --measure
                                   is narrower and is for prose only */
  --bp-wide: 48rem;             /* documentation only — see header rule 2 */
}

/* ------------------------------------------------------------------ base -- */

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-base);
  line-height: var(--leading);

  /* FR-013: an 80-character client name or an email address with nothing to wrap
     at must break rather than widen the page. `anywhere` rather than `break-word`
     because it also shrinks min-content, which is what stops a table cell from
     forcing the page wider at 320px. Inherits, so it covers cells and cards too. */
  overflow-wrap: anywhere;

  /* Keeps the user's text-size preference intact and stops Safari inflating text
     in landscape. Never `none`, which would override the preference (FR-008). */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* ------------------------------------------------- accessibility utilities --
 * Moved verbatim out of the inline <style> in base.html. Behaviour is unchanged
 * by design: these were already correct, and the skip link is the first thing a
 * keyboard user meets.
 *
 * The two px values below are the pre-existing exceptions to header rule 3. The
 * 1px box is required by the clip technique itself, and -9999px is the offset
 * that was already here. Neither is a size a reader perceives, so neither is
 * affected by zoom. Leave them.
 */
.skip-link {
  position: absolute;
  left: -9999px;
}

.skip-link:focus {
  position: static;
}

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

/* ----------------------------------------------------------------- focus --
 * :focus-visible rather than :focus, so the ring appears for keyboard users and
 * not on mouse clicks. px is correct for an outline width. Nothing in this file
 * may set `outline: none` (FR-024).
 */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* -------------------------------------------------------- reduced motion --
 * FR-027. Currently the only motion in the product is the htmx in-flight
 * indicator, so this is mostly insurance against whatever gets added later.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ------------------------------------------------------- loading indicator --
 * FR-021. htmx adds `htmx-request` for the life of the request and takes it off
 * again; these rules are the whole behaviour, and no JavaScript is written
 * (research R10).
 *
 * Both selectors are needed, because htmx marks a different element in each of
 * the two shapes this product uses — and the browser run is what found it:
 *
 *   - log and scheduled: the <form> carries hx-get, so the FORM is marked and
 *     the span inside it needs the descendant selector.
 *   - client search: the <input> carries hx-get, so a sibling span is out of
 *     reach and the template points at it with hx-indicator. htmx then marks
 *     the SPAN itself, which lands both class names on one element and matches
 *     only the compound selector.
 *
 * htmx 4 injects an equivalent pair of its own through adoptedStyleSheets
 * (config.includeIndicatorCSS), which is why the client search still worked
 * with the compound selector missing — the product was leaning on a stylesheet
 * nobody here wrote. These rules make app.css stand on its own, and they are
 * also what keeps the indicator hidden when the script has not run at all:
 * without them a browser that never executed htmx shows a permanent
 * "Searching…" beside a search box that is not searching.
 *
 * Opacity rather than display, so the space is reserved on first paint and the
 * indicator appearing does not shift the control the user is typing into. The
 * transition is deliberately absent: htmx's own 200ms fade-in already applies,
 * and the reduced-motion block above overrides it with !important, which wins
 * over an adopted sheet.
 */
.htmx-indicator { opacity: 0; }
.htmx-request .htmx-indicator,   /* the form marks itself, span is inside it */
.htmx-request.htmx-indicator     /* hx-indicator marks the span itself */ {
  opacity: 1;
}

/* ----------------------------------------------------------------- alert --
 * Shared by the message region in base.html and the form error summary, which is
 * why it lives in the foundation rather than in either story that uses it.
 *
 * The four modifier names are not a choice: Django's message.level_tag emits
 * success / warning / error / info, and base.html renders the class straight from
 * it. There is deliberately no --danger alias.
 *
 * FR-003 and FR-017: severity must not be carried by colour alone. The boxed
 * shape separates an alert from body prose, and the glyph separates the four
 * severities from each other without reference to their colour.
 */
.alert {
  margin: var(--space-4) 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-left-width: var(--space-1);
  border-radius: var(--radius);
}

.alert::before {
  font-weight: 700;
  margin-right: var(--space-2);
}

.alert > :first-child { margin-top: 0; }
.alert > :last-child { margin-bottom: 0; }

.alert--success {
  background: var(--success-tint);
  border-color: var(--success);
  color: var(--success-ink);
}
.alert--success::before { content: "\2713"; }   /* check */

.alert--warning {
  background: var(--warning-tint);
  border-color: var(--warning);
  color: var(--warning-ink);
}
.alert--warning::before { content: "\26A0"; }   /* warning triangle */

.alert--error {
  background: var(--danger-tint);
  border-color: var(--danger);
  color: var(--danger-ink);
}
.alert--error::before { content: "\2715"; }     /* cross */

.alert--info {
  background: var(--accent-tint);
  border-color: var(--accent);
  color: var(--accent-ink);
}
.alert--info::before { content: "\2139"; }      /* information */

/* ------------------------------------------------------------ typography --
 * FR-005: three heading levels separable at a glance. The scale is deliberately
 * short — this is a working tool full of dense lists, not a landing page, and a
 * sixth size would be a size nobody could place.
 *
 * h3 sits at body size and is separated by the UA's bold instead. That is a real
 * distinction and it costs nothing; a fourth step above --text-xl would push h1
 * off a 320px line.
 */
h1,
h2,
h3 {
  margin: var(--space-5) 0 var(--space-3);
  line-height: var(--leading-tight);
}

h1 { font-size: var(--text-xl); }
h2 { font-size: var(--text-lg); }
h3 { font-size: var(--text-base); }

/* Prose only. A table is not prose and needs the full width it can get. */
p,
li {
  max-width: var(--measure);
}

caption,
small {
  color: var(--ink-muted);
}

caption {
  font-size: var(--text-sm);
  text-align: left;
}

/* FR-003: the underline is what identifies a link without reference to its
 * colour, so in running text it stays. `text-decoration: none` is legitimate
 * only where something else does that job — the nav strip below is the one
 * place in this file that qualifies.
 */
a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* --------------------------------------------------------------- actions --
 * FR-016: the primary action must be distinguishable from the secondary ones.
 * The markup already draws that line and needs no new class to say it again — a
 * submit button (the default type) is its form's primary action, and the only
 * non-submit button in the product is "Add another contact point", which is
 * genuinely secondary. So the selectors follow the semantics already there.
 *
 * FR-010 and R7: the 44px target is min-height plus padding, never a fixed
 * height. A fixed height clips enlarged text, which is the whole of FR-008.
 * The bottom and right margins are the separation FR-010 also asks for, so no
 * two adjacent controls can be confused with each other.
 */
button {
  min-height: var(--target);
  margin: 0 var(--space-2) var(--space-2) 0;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;            /* buttons do not inherit the body font on their own */
  cursor: pointer;
}

button:hover {
  background: var(--accent-ink);
  border-color: var(--accent-ink);
}

/* The quiet variant. Same target, same rhythm, visibly not the main event. */
button[type="button"] {
  background: var(--bg);
  border-color: var(--border);
  color: var(--accent);
}

button[type="button"]:hover {
  background: var(--accent-tint);
}

/* ---------------------------------------------------------------- layout --
 * FR-009: padding and a max width, and nothing else. A 320px viewport needs no
 * special case under this — it simply has less room, which is the point of
 * starting mobile-first rather than overriding a desktop layout later.
 *
 * FR-007: the rhythm between a heading and its content, between fields, and
 * between records is one number set once here. All 28 templates get it without
 * any of them saying anything about spacing.
 */
main {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--space-4);
}

p,
ul,
ol,
dl,
table,
form {
  margin: 0 0 var(--space-4);
}

/* The heading a page opens with should not push itself away from the top. */
main > :first-child {
  margin-top: 0;
}

/* ------------------------------------------------------------------- nav --
 * FR-011 and research R6: one horizontally scrollable row. Every destination is
 * in it — nothing behind a toggle — and it stays exactly one row high, because
 * seven items wrapping into three stacked rows would eat a landscape phone's
 * 360px of height, which is the other half of FR-011.
 *
 * There is one <nav> in the product, so it is selected as an element. Adding a
 * class to say "this is the nav" when the element already says it is a name for
 * the sake of having one.
 */
nav {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border-bottom: 1px solid var(--border);

  /* The scroll-edge hint (R6): the strip's right edge fades, so a row that runs
     off-screen looks like it continues rather than like it ends. Only a
     gradient's alpha matters to a mask, so the opaque stop is a token rather
     than the `#000` this technique is usually written with — V2 forbids a colour
     literal here and any opaque colour does the identical job. The mask is
     painted on the scrollport, not the content, so the fade stays at the edge
     while the items move under it.

     ponytail: the fade is always drawn, including when nothing is off-screen,
     where it reads as a soft edge rather than a wrong signal. Making it appear
     only when there is overflow needs scroll-driven animations or JS; revisit
     if the destination count ever makes the difference matter. */
  mask-image: linear-gradient(
    to right,
    var(--surface) calc(100% - var(--space-6)),
    transparent
  );
}

nav a,
nav button {
  display: flex;
  align-items: center;
  flex: 0 0 auto;               /* an item may not be squeezed narrower than its label */
  min-height: var(--target);
  padding: 0 var(--space-3);
  border-radius: var(--radius);
  white-space: nowrap;          /* one row means no item wraps inside itself either */
  scroll-snap-align: start;
  text-decoration: none;        /* the landmark, the strip, and the position do the
                                   work the underline does in running text */
}

nav > strong {
  flex: 0 0 auto;
  padding: 0 var(--space-2);
  white-space: nowrap;
}

/* Log out is in the strip because R6 keeps it there, but it is not the page's
   primary action and must not look like one. The form wrapper carries none of
   the vertical rhythm a form gets in `main`. */
nav form {
  margin: 0;
}

nav button {
  margin: 0;
  background: none;
  border-color: transparent;
  color: var(--accent);
}

nav button:hover {
  background: var(--accent-tint);
}

/* Where you are (User Story 1, scenario 1). A filled pill plus bold weight, so
   the current page is marked by shape and weight and not by colour alone
   (FR-003). Set from aria-current rather than a class: the attribute is what a
   screen reader reads, so styling the same hook keeps the two in step. */
nav [aria-current="page"] {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 700;
}

/* ----------------------------------------------------------------- forms --
 * Django renders these with {{ form.as_p }}, so the shape is a <p> per field
 * holding a <label>, a control, and any errors. Nothing here needs a class.
 *
 * --text-base on the controls is not a style choice: below 16px, iOS Safari
 * zooms the viewport when a field takes focus, and the user is then panning a
 * magnified page one-handed. That is the whole of FR-014's second half, and it
 * is a font-size (FR-015 keeps the labels; they are already there and visible,
 * so all they need is room).
 */
label {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--text-sm);
  font-weight: 700;
}

input,
select,
textarea {
  width: 100%;
  min-height: var(--target);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-base);
}

/* A checkbox is not a text field and a full-width one is absurd. There is exactly
   one in the product — the formset's delete box — so this is not speculative:
   it keeps the browser's own control, enlarged, and takes its 44px target from
   its label, which is clickable through `for` and can be padded where a replaced
   element cannot.

   The fixed `height` here is deliberate and does not break V5, which is about
   elements containing text: a checkbox contains none, and the value is a rem
   token, so it still grows with zoom and with the user's text size. */
input[type="checkbox"] {
  width: var(--space-5);
  height: var(--space-5);
  min-height: 0;
  margin-right: var(--space-2);
}

label[for$="-DELETE"] {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  font-weight: 400;
}

textarea {
  min-height: calc(var(--target) * 3);
  resize: vertical;
}

/* as_p's per-field wrapper. This is the rhythm FR-007 asks for between fields. */
form p {
  margin: 0 0 var(--space-4);
  max-width: none;      /* --measure is for prose; a field is not prose */
}

.helptext {
  display: block;
  margin-top: var(--space-1);
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* --------------------------------------------------------- form errors --
 * Django's own class name, and the invalid state beside it. FR-003: an error is
 * not signalled by red alone — the message is text, the glyph is a shape, and
 * the left edge of the summary is a border. Colour is the third cue, not the
 * only one.
 */
.errorlist {
  margin: var(--space-1) 0 0;
  padding: 0;
  list-style: none;
  color: var(--danger);
  font-size: var(--text-sm);
  font-weight: 700;
}

.errorlist li::before {
  content: "\2715\00a0";      /* the same cross the error alert uses */
}

/* The summary's heading sits on the line the alert glyph starts, rather than
   leaving the glyph stranded on a line of its own above it. */
.alert h2 {
  display: inline;
  font-size: var(--text-base);
}

.alert ul {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-5);
}

/* ---------------------------------------------------- contact point rows --
 * The formset in the client form. Each row is a <fieldset> with three controls,
 * and at 320px the three have to read as one group rather than as three loose
 * fields that happen to be adjacent.
 */
fieldset {
  margin: 0 0 var(--space-4);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

/* ---------------------------------------------------------------- tables --
 * The desktop presentation, which is the easy half. Seven tables, none of them
 * narrower than two columns and one of them eight — the reflow below is where
 * the actual requirement lives.
 *
 * Row rules are --border-subtle and not --border on purpose: the header row and
 * the column alignment already tell the reader what belongs to what, so a row
 * line here is decorative and is allowed to be faint. The same line on a mobile
 * card is the only thing separating one record from the next, so there it is
 * --border. That is the whole of the distinction (data-model.md).
 */
table {
  width: 100%;
  border-collapse: collapse;
}

caption {
  padding-bottom: var(--space-2);
}

th,
td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  vertical-align: top;
}

thead th {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

/* border-bottom on the row rather than border-top on `tr + tr`, so the mobile
   card rule below overrides it at equal specificity instead of fighting it. */
tbody tr {
  border-bottom: 1px solid var(--border-subtle);
}

/* FR-020: "No clients match." is a message, not a record with one blank field.
   It is the one cell in the product with a colspan and no data-label, which is
   what makes it selectable without a class of its own. */
td[colspan] {
  color: var(--ink-muted);
  font-style: italic;
  text-align: center;
}

/* --------------------------------------------------- tables, narrow width --
 * FR-012. The one breakpoint in the file, and the literal is deliberate: CSS
 * custom properties are not valid in a @media prelude, so `var(--bp-wide)` here
 * would raise no error, match nothing, and leave the delivery log eight columns
 * wide at 320px. --bp-wide documents what 48rem means; this is the one place the
 * number itself has to be written.
 */
@media (max-width: 48rem) {
  table,
  thead,
  tbody,
  tr,
  td,
  /* caption belongs in this list and was missing from it: left at
     `display: table-caption` under a parent that is no longer a table box, it
     gets its own anonymous table wrapper, shrink-to-fits to the longest word,
     and renders one character per line down the page. It looks like a broken
     stylesheet and it happened on all three tables. Found in the browser — the
     markup is valid and the tests pass either way, so nothing else caught it. */
  caption {
    display: block;
  }

  /* The header leaves the page but must not leave the accessibility tree: the
     `columnheader` roles the markup carries would otherwise point at nothing.
     Same clip technique as .visually-hidden, restated because a class cannot be
     added to a selector from inside a media query. `display: none` here is the
     mistake that looks identical and silently breaks FR-025. */
  thead {
    position: absolute;
    width: 1px;
    height: 1px;
    clip-path: inset(50%);
    overflow: hidden;
  }

  /* One record, one card. --border and not --border-subtle: with the header gone
     this line is the only thing saying where one record ends. */
  tbody tr {
    margin-bottom: var(--space-4);
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
  }

  /* The label the column heading used to supply. Selected on [data-label] rather
     than on td, so a cell with no label — the empty state — produces no empty
     line rather than an unexplained gap. */
  td[data-label]::before {
    content: attr(data-label);
    display: block;
    color: var(--ink-muted);
    font-size: var(--text-xs);
  }

  /* An empty state is not a record, so it does not become a card either. */
  tbody tr:has(td[colspan]) {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
  }

  /* The primary tap target on every list page is a link inside a cell, so FR-010
     applies to it in full even though an inline link in prose is exempt (R7). A
     mis-tap here costs a page load and a scroll back. */
  td a,
  td button {
    display: flex;
    align-items: center;
    min-height: var(--target);
  }
}
