/*
 * One stylesheet, hand-written, no build step.
 *
 * The public business page has a Lighthouse performance budget to meet
 * (ETE-14) and the app is installed as a PWA on phones, so every kilobyte of
 * framework CSS would be paid for on a cold 4G start for no benefit at this
 * size. Custom properties cover the parts a framework would otherwise give us.
 *
 * Everything here is mobile-first: this app is used one-handed, standing at a
 * counter. The wider layout is the exception, not the base case.
 */

:root {
  --ink: #16181d;
  --ink-soft: #5b6270;
  --paper: #ffffff;
  --surface: #f5f6f8;
  --line: #dfe2e8;
  /* The edge of something you type into is a control, not decoration, so it
     needs 3:1 against both surfaces (WCAG 1.4.11). --line is 1.3:1 and stays
     for dividers and cards; this one is for fields. */
  --field-line: #7d8492;
  /* #1f6feb was the obvious blue and it fails WCAG AA as link text: 4.29:1
     against --surface, where 4.5 is the floor. This one clears it against
     both surfaces, and white on it reaches 5.85:1 for button labels.
     Lighthouse's accessibility audit is what caught the original. */
  --accent: #1a5fd0;
  --accent-ink: #ffffff;
  --danger: #b3261e;
  --ok: #156c3b;

  --radius: 10px;
  --gap: 1rem;
  --measure: 34rem;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #e8eaee;
    --ink-soft: #a0a7b4;
    --paper: #14161a;
    --surface: #1c1f25;
    --line: #2c313a;
    --field-line: #6b7280;
    --accent: #4d9bff;
    /* Dark ink on the lighter dark-mode blue: white on #4d9bff is 2.82:1,
       under the 4.5 a button label needs; this is 6.64:1. */
    --accent-ink: #0b1220;
    --danger: #ff6b61;
    --ok: #57cc8a;
  }
}

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

body {
  margin: 0;
  font-family: var(--font);
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
  -webkit-text-size-adjust: 100%;
}

main {
  max-width: var(--measure);
  /* env(safe-area-inset-*) keeps content clear of the iPhone home indicator
     once the app is installed and running without browser chrome. */
  margin: 0 auto;
  padding: var(--gap) max(var(--gap), env(safe-area-inset-left)) calc(var(--gap) * 2)
    max(var(--gap), env(safe-area-inset-right));
}

h1 {
  font-size: 1.5rem;
  line-height: 1.25;
  margin: 0 0 0.25em;
}

h2 {
  font-size: 1.15rem;
  margin: 1.5em 0 0.5em;
}

p {
  margin: 0 0 1em;
}

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

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

/* -- Header ---------------------------------------------------------------- */

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: 0.75rem var(--gap);
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

.site-header nav {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.site-header form {
  margin: 0;
}

.brand {
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--ink);
  text-decoration: none;
}

.who {
  color: var(--ink-soft);
  font-size: 0.9rem;
  max-width: 10rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* -- Footer ---------------------------------------------------------------- */

.site-footer {
  display: flex;
  justify-content: center;
  padding: var(--gap) var(--gap) max(var(--gap), env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 0.9rem;
}

.lang-switch {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0;
}

.lang-switch .current {
  font-weight: 600;
  color: var(--ink);
}

/* -- Forms ----------------------------------------------------------------- */

.field {
  margin: 0 0 1.1rem;
}

/* A radio group's field wrapper is a <fieldset>, which browsers give a border,
   padding and a floating legend by default. No form has one since ETE-33 took
   the signup radio away, but partials/field.html still renders the element for
   any group that comes back, and unstyled is not the same as unhandled. */
fieldset.field {
  border: 0;
  padding: 0;
  min-inline-size: 0;
}

label,
legend {
  display: block;
  font-weight: 600;
  font-size: 0.9rem;
  margin-bottom: 0.25rem;
  padding: 0;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="url"],
textarea,
select {
  width: 100%;
  /* 1rem, not less: iOS Safari zooms the viewport on focus below 16px. */
  font: inherit;
  padding: 0.6rem 0.7rem;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--field-line);
  border-radius: var(--radius);
}

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

.helptext,
.helptext ul {
  display: block;
  margin: 0.35rem 0 0;
  padding-left: 1rem;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

ul.errorlist {
  margin: 0 0 0.5rem;
  padding-left: 1.1rem;
  color: var(--danger);
  font-size: 0.9rem;
}

/* -- Buttons --------------------------------------------------------------- */

button,
.button {
  display: inline-block;
  font: inherit;
  font-weight: 600;
  /* 44px tall: the smallest target a thumb reliably hits. */
  min-height: 2.75rem;
  padding: 0.6rem 1.1rem;
  color: var(--accent-ink);
  background: var(--accent);
  border: 1px solid transparent;
  border-radius: var(--radius);
  text-decoration: none;
  text-align: center;
  cursor: pointer;
}

button[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}

button.link {
  /* Looks like a link, but is still a thumb's worth tall (QA-MOB-1): "Log
     out", the language switch, Share and Remove were 24-26px, about half the
     44px a thumb reliably hits. The height is invisible -- no background, no
     border -- so only the target grows, not the line. */
  min-height: 2.75rem;
  padding: 0 0.15rem;
  color: var(--accent);
  background: none;
  font-weight: 400;
  text-decoration: underline;
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
}

/* Links that stand on their own as a thing to tap -- "Edit" beside a button,
   "Back to your shop", the account links under the dashboard, the sort links
   -- are a thumb's worth tall (R2-MOB-2), like button.link above. At 380px
   they were 19-24px. inline-flex rather than padding: the height is part of
   the box, so it is the target and not an overlap with the line above. The
   one class is `.links` on a paragraph of them; a link inside a sentence is
   left as text, which WCAG 2.5.8 exempts. */
.button-row > a:not(.button),
.links a,
.sorts a,
.sorts .current,
.pager a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
}

/* -- Cards and messages ---------------------------------------------------- */

.card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--gap);
  margin: 0 0 var(--gap);
}

.card h2,
.card h3 {
  margin-top: 0;
}

/* The dashboard's section headings ("My stores", "My store") sit between
   cards rather than inside one, so they need the air the card's own padding
   would otherwise have given them. */
.section-heading {
  margin: calc(var(--gap) * 1.5) 0 0.75rem;
}

/* The shop page's list of collaborators: a name on the left, its remove
   button on the right, wrapping under each other on a narrow phone. */
.team {
  list-style: none;
  margin: 0 0 var(--gap);
  padding: 0;
}

.team li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}

.team form {
  margin: 0;
}

.messages {
  list-style: none;
  max-width: var(--measure);
  margin: var(--gap) auto 0;
  padding: 0 var(--gap);
}

.message {
  border: 1px solid var(--line);
  border-left-width: 4px;
  border-radius: var(--radius);
  background: var(--surface);
  padding: 0.6rem 0.75rem;
  margin-bottom: 0.5rem;
}

.message.success {
  border-left-color: var(--ok);
}

.message.error {
  border-left-color: var(--danger);
}

/* Password validator help arrives as a <ul> inside the help <span>. */
.helptext ul {
  margin: 0.25rem 0 0;
}

.field.has-error input,
.field.has-error select {
  border-color: var(--danger);
}

/* The birthday's two selects (ETE-28), side by side because they are one
   answer. The day is the narrower of the two: "31" against "September". They
   are allowed to wrap on a very narrow phone rather than being squeezed to
   the point where the month name is cut off. */
.field-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.field-row .field {
  flex: 1 1 8rem;
}

.field-row .field:first-child {
  flex: 0 1 6rem;
}

/* The legend and the sentence under it belong to the pair, so the gap goes
   below them rather than under each select. */
fieldset.birthday .helptext {
  margin-bottom: 0.6rem;
}

/* -- Business ------------------------------------------------------------- */

.logo {
  display: block;
  width: 96px;
  height: 96px;
  /* Shop logos arrive at whatever aspect ratio the owner had to hand. */
  object-fit: contain;
  border-radius: var(--radius);
  background: var(--paper);
}

code {
  font-size: 0.95em;
  padding: 0.1em 0.35em;
  border-radius: 6px;
  background: var(--surface);
  border: 1px solid var(--line);
}

textarea {
  resize: vertical;
}

/* -- Loyalty card --------------------------------------------------------- */

/* The points on one card (ETE-57). There was a row of dots here until the
   reward menu -- `.stamps`, `.stamp`, `.stamp.filled` and `.stamp-count` --
   drawn as wide as the shop's one threshold and filled to the card in hand.
   With five rewards at five prices there is no single card to be a fraction
   of, so the balance is a number and this is the type for it: big enough to
   read across a counter, tabular so it does not jitter when it goes from 9 to
   10 as the panel swaps in. */
.points {
  margin: 0 0 0.75rem;
  font-size: 1.4rem;
  font-variant-numeric: tabular-nums;
}

/* Cancelling a reward that has not been handed over (ETE-58). A link rather
   than a button, and set apart from the QR above it: pressing "show the code"
   is what people opened this page for, and a Cancel of the same weight beside
   it is a Cancel somebody taps by accident at a counter. */
.cancel-reward {
  margin: var(--gap) 0 0;
}

/* The shop's menu: what each reward is on the left, what it costs on the
   right. The same row shape as `.team` above, because it is the same kind of
   list -- a handful of rows with something at each end -- and it is used on
   the owner's page (with a Remove button), the public page and the customer's
   card. */
.reward-menu {
  list-style: none;
  margin: 0 0 var(--gap);
  padding: 0;
}

.reward-menu li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}

.reward-menu form {
  margin: 0;
}

/* What a visit earns (ETE-120). The number and its Save button on one line:
   it is one short answer, and a field the full width of the card above a menu
   of five reads as the more important of the two. */
.rate-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem;
  margin: 0 0 var(--gap);
}

.rate-form .field {
  flex: 0 1 9rem;
  margin: 0;
}

/* The repricing step, where each row carries a price box rather than a Remove
   button: the labelled box, and under it the two sentences saying what this
   reward costs today and what leaving the price alone would do. Stacked rather
   than side by side, because the sentences are the length of a sentence. */
.reward-menu.reprice li {
  flex-direction: column;
  align-items: stretch;
  gap: 0.25rem;
}

.reward-menu.reprice .field {
  margin: 0;
}

.reward-menu.reprice .field input {
  max-width: 7rem;
}

.ready {
  font-weight: 600;
  color: var(--ok);
}

/* The one flourish in the app (ETE-26). Everything else here is deliberately
   flat -- a till in a hurry is not the place for motion -- but a surprise double
   stamp is the single moment worth marking, and a line that looks like every
   other line does not land as a surprise. So: boxed and tinted like the
   regulars banner, and given one short entrance so it arrives rather than
   simply being there when the panel swaps in.

   `--ok` rather than `--accent`: this is good news, and the accent colour is
   what the app uses for "here is a thing to press". */
.surprise {
  margin: 0 0 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 2px solid var(--ok);
  border-radius: var(--radius);
  font-weight: 600;
  color: var(--ok);
  text-align: center;
  animation: surprise-in 320ms ease-out both;
}

@keyframes surprise-in {
  from {
    opacity: 0;
    transform: scale(0.94);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* Somebody who has asked their operating system for less motion has asked for
   it everywhere, including the nice bits. The line still appears; it just does
   not move, and the border and the colour carry it on their own. */
@media (prefers-reduced-motion: reduce) {
  .surprise {
    animation: none;
  }
}

/* -- The visits behind a card's number (ETE-41) --------------------------- */

.history {
  list-style: none;
  max-width: var(--measure);
  margin: 0 0 var(--gap);
  padding: 0;
}

.history li {
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--line);
}

/* The date leads every row, so it is fixed-width and set back: the eye runs
   down the timestamps looking for the day being argued about. */
.history .when {
  display: block;
  font-size: 0.85rem;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* A taken-back visit is struck through rather than hidden or greyed out.
   Grey alone says "less important"; this says "this one does not count", and
   the line under it says who took it back. */
.history .entry-voided .what {
  color: var(--ink-soft);
}

.history .entry-reward .what,
.history .entry-redeemed .what {
  font-weight: 600;
}

/* What the row was worth (ETE-136). Tabular figures and its own line, so the
   column of them can be read down and added up -- which is the argument this
   page exists to settle. Muted rather than bold: the label is what a customer
   is looking for, and the number is what they check once they have found it. */
.history .amount {
  display: block;
  font-size: 0.85rem;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.logo-lead {
  margin-bottom: 0.75rem;
}

/* -- Check-in ------------------------------------------------------------- */

.check-in {
  text-align: center;
}

.check-in form {
  margin: 0;
}

.qr {
  /* Capped rather than full width: past about 18rem the modules get bigger
     without getting easier to scan, and the countdown falls below the fold on
     a small phone. */
  width: 100%;
  max-width: 18rem;
  margin: 0 auto 0.75rem;
  aspect-ratio: 1;
  padding: 0.5rem;
  border-radius: var(--radius);
  /* The code is always dark-on-white, in both themes. Inverting it for dark
     mode produces a code many scanners refuse to read. */
  background: #ffffff;
}

.qr svg {
  display: block;
}

.qr path {
  fill: #000000;
}

.countdown {
  margin: 0 0 0.75rem;
  font-size: 0.9rem;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.check-in .points {
  text-align: center;
}

/* "Show a different code" and "Another code" span the card on a phone
   (QA-DASH-2): the panel is a column of one thing under another, and a button
   sized to its words sits off to the left of a centred QR. */
.check-in form button {
  width: 100%;
}

.read-aloud {
  /* Between the QR and the "different code" button, because it is the same
     code in another form rather than a separate offer (ETE-40). */
  margin: 0 0 0.75rem;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.short-code {
  /* Big and monospaced: this is read across a counter in a noisy room, so it
     has to be legible from arm's length and character by character. The
     spacing keeps the two groups of four apart, which is how it gets said. */
  display: block;
  margin-top: 0.15rem;
  font-family: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace;
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  color: var(--ink);
}

.disclaimer {
  /* Close under the button it belongs to: it explains what that tap costs
     (ETE-55), so read as one thing with it rather than as the next paragraph. */
  margin: 0.4rem 0 0;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

/* -- Scanner -------------------------------------------------------------- */

.scanner {
  text-align: center;
}

#scanner-video,
#scanner-fallback {
  width: 100%;
  max-width: 24rem;
  margin: 0 auto 0.75rem;
  /* Reserve the box before the stream arrives, so the result panel below does
     not jump down the moment the camera starts. */
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius);
  background: #000000;
}

#scanner-status {
  min-height: 1.5em;
}

.scan-result {
  text-align: center;
}

.scan-result form {
  margin: 0;
}

.scan-result .points {
  text-align: center;
}

/* The one line that is the whole of ETE-25. Weighted like a banner rather than
   a caption on purpose: someone stamping cards at speed reads the result panel
   for half a second, and a perk that reads as decoration is a perk that never
   gets handed over. Boxed, tinted and larger than the text around it, so the
   difference between a regular and anyone else is visible before it is read. */
.scan-result .tier {
  margin: 0 0 0.75rem;
  padding: 0.6rem 0.75rem;
  border-radius: var(--radius);
  border: 2px solid var(--accent);
  background: var(--surface);
  text-align: center;
}

.tier-name {
  display: block;
  font-size: 1.15rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--accent);
}

.tier-count {
  display: block;
  font-size: 0.85rem;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.tier-perk {
  display: block;
  margin-top: 0.2rem;
  font-weight: 600;
}

.code-entry {
  /* Under the camera and visually quieter than it: this is the fallback for
     when the camera will not do (ETE-40), not a second front door. */
  margin-top: 1.25rem;
  text-align: center;
}

.code-entry label {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 0.9rem;
  color: var(--ink-soft);
}

.code-entry input[type="text"] {
  /* Typed by someone reading it out character by character, so it is shown
     the same way it is spoken. */
  max-width: 12rem;
  margin: 0 auto 0.5rem;
  font-family: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace;
  font-size: 1.1rem;
  letter-spacing: 0.08em;
  text-align: center;
  text-transform: uppercase;
}

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

/* -- Install prompt ------------------------------------------------------- */

.install h2 {
  margin-top: 0;
}

.steps {
  margin: 0 0 0.75rem;
  padding-left: 1.25rem;
}

.steps li {
  margin-bottom: 0.4rem;
}

.glyph {
  /* Sized in em so it tracks the sentence it sits inside. */
  width: 1.2em;
  height: 1.2em;
  vertical-align: -0.25em;
  color: var(--accent);
}

/* -- The customers page ---------------------------------------------------- */

/* Seven things about a person, on a 380px screen held in one hand behind a
   counter. A <table> is the obvious markup and the wrong one: seven columns
   at that width is a horizontal scrollbar, and the numbers end up so far from
   their headings that the row stops saying anything. So the row is a block
   that reflows -- who, then the numbers -- and nothing here is ever wider
   than the screen it is read on. */

.customer-search {
  margin-bottom: 0.75rem;
}

.customer-search .button-row {
  /* Side by side even at 380px: a search box that wraps its own button under
     itself spends a line of vertical space this page has better uses for.
     min-width:0 is what lets the input shrink inside the flex row rather than
     insisting on its default size and pushing the button off the end. */
  flex-wrap: nowrap;
}

.customer-search input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;
  /* The general input rule names its types one by one and search is not among
     them, so this one was the browser's default 22px (QA-MOB-1). */
  min-height: 2.75rem;
  font: inherit;
  padding: 0.6rem 0.7rem;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--field-line);
  border-radius: var(--radius);
}

.download {
  font-size: 0.9rem;
  margin-bottom: 0.75rem;
}

.sorts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 0.75rem;
  font-size: 0.9rem;
  margin-bottom: 0.5rem;
}

.sorts .current {
  font-weight: 600;
  color: var(--ink);
}

.count {
  font-size: 0.9rem;
  margin-bottom: 0.5rem;
}

/* One definition of a row that reflows, shared with the two insights lists
   (ETE-50) through `.people` and with the activity log (ETE-56) through
   `.activity`. Grouped selectors rather than a second copy under each page's
   own heading: the three pages are read on the same phone at the same counter,
   a line of small facts under a bold one should look the same on all of them,
   and a duplicate rule set is a pair that drifts the first time one is
   adjusted. The `.email` rule below is the one part the other two never use --
   those pages carry no address, on purpose. */
.customers,
.people,
.activity,
.customer-visits {
  list-style: none;
  margin: 0 0 var(--gap);
  padding: 0;
}

.customers li,
.people li,
.activity li,
.customer-visits li {
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--line);
}

/* The whole row is the customer's link (R2-MOB-2): the name alone was a 22px
   line of text, and the row it sits in is the thing a thumb aims at. The
   link's ::after is stretched over the row, so the email and the numbers
   tap through to the same page without a second link for a screen reader to
   announce. */
.customers li {
  position: relative;
}

.customers .name::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Above the numbers line, whose clip-path makes it a stacking context
     painted (and hit-tested) after this. */
  z-index: 1;
}

/* `.person` rather than `.customers`' own `.who`, and the difference is not
   cosmetic: `.who` is already the site header's signed-in-name chip, which is
   `max-width: 10rem` with `white-space: nowrap` and an ellipsis. Reusing the
   name here silently truncated every customer called anything longer than about
   twenty characters -- at 380px the name got 160 of them and the rest became a
   "…" -- and nothing about the markup would have suggested why. (The customers
   page inherits the same chip on its own rows; that is its issue to fix, not
   this one's, and it is noted rather than changed here.) */
.customers .who,
.people .person,
.activity .person {
  margin: 0;
}

.customers .name,
.people .name,
.activity .name {
  display: block;
  font-weight: 600;
  /* A customer types their own name and an address can be long: both break
     rather than widening the page past the viewport it is read in. */
  overflow-wrap: anywhere;
}

.customers .email {
  display: block;
  font-size: 0.9rem;
  overflow-wrap: anywhere;
}

.customers .numbers,
.people .numbers,
.activity .numbers,
.customer-summary {
  display: flex;
  flex-wrap: wrap;
  /* Row gap only, so numbers that have wrapped onto a second line do not
     close up against the ones above them. The space between two numbers on
     one line is the separator's own width, below. */
  gap: 0.1rem 0;
  /* Pulled one separator's width to the left and clipped there: every item
     carries a separator in front of it, and the one in front of whichever
     item starts a line -- the first, or one that wrapped -- lands in the
     clipped strip. A line never starts "· joined …" (R2-CUST-4), whatever
     the width and whatever the language. */
  margin: 0.15rem 0 0 -1em;
  clip-path: inset(0 0 0 1em);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

/* The separator is drawn rather than written into the strings, so a line that
   wraps does not leave a middle dot stranded at the end of it -- and no
   translator has to carry punctuation that belongs to the layout. */
.customers .numbers > span::before,
.people .numbers > span::before,
.activity .numbers > span::before,
.customer-summary > span::before {
  content: "·";
  display: inline-block;
  width: 1em;
  text-align: center;
  font-weight: 400;
  color: var(--ink-soft);
}

/* The part of the line the page was opened for. Everything else on it is
   context, so this is the one piece in full-strength ink. On the two customer
   lists that is the visit count; on the activity log it is the timestamp,
   because a log is read by when rather than by how many. */
/* The balance sits inside the line as one of its numbers (QA-CUST-1). The
   global `.points` is the card's big balance -- 1.4rem with a margin under it
   -- and inherited here it jumped the baseline and pushed the row into
   wrapping. */
.customers .numbers .points,
.customer-summary .points {
  margin: 0;
  font-size: inherit;
}

.customers .numbers .visits,
.people .numbers .visits,
.activity .numbers .when,
.customer-summary .visits {
  font-weight: 600;
  color: var(--ink);
}

/* One customer's own page (ETE-122): the numbers, then their visits, then the
   button that takes one back. The list inherits the three rules above; what is
   left is the shape of a row, which is a timestamp with two soft lines under it
   and at most one control. */
.customer-visits .when {
  margin: 0;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.customer-visits .who-served,
.customer-visits .undone,
.customer-visits .too-late {
  margin: 0.15rem 0 0;
  font-size: 0.85rem;
}

/* A taken-back visit keeps its row and is struck through rather than hidden,
   for the reason the customer's own history does it: a row that vanished would
   make an undo indistinguishable from the shop quietly editing a balance. The
   strike is on the timestamp in the markup; this softens the rest of the row so
   the two read as one entry that does not count. */
.customer-visits .voided .when,
.customer-visits .voided .who-served {
  color: var(--ink-soft);
}

/* The button is the point of the page, so it is not a full-width block that
   pushes every other row off the screen. */
.customer-visits form {
  margin: 0.35rem 0 0;
}

.customer-visits form button {
  width: auto;
  padding: 0.35rem 0.75rem;
  font-size: 0.9rem;
}

/* The last row of a list inside a card: the card's own edge is the line under
   it, so a second one is a doubled rule. */
.people li:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

/* The tier a regular holds, beside their name (ETE-50/ETE-25). Inline rather
   than a second line, because it is an adjective on the name rather than
   another number, and it borrows the accent the rest of the page already uses.
   A badge shape with a background would be the loudest thing in a list of ten
   names, which is the wrong emphasis -- the ranking is the information and the
   tier is a note on it. Weight as well as colour, so it survives greyscale. */
.people .tier-mark {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--accent);
}

/* The activity log's customer, beside what happened to their card (ETE-56).
   A block rather than another item on the numbers line: what happened and to
   whom is one fact in two halves, and a customer's name belongs next to it
   rather than down among the timestamps. Muted, because the log is scanned by
   time and by kind first -- the name is what an owner reads once a row has
   already caught their eye. It breaks rather than widening the page, for the
   reason `.name` does: people type their own names and some of them are long. */
.activity .whom {
  display: block;
  font-size: 0.9rem;
  overflow-wrap: anywhere;
}

/* Both insights empty states. Quiet, inside a card that already has a heading
   above it -- so no padding of its own and no consoling paragraph underneath. */
.empty-list {
  margin: 0.25rem 0 0;
  font-size: 0.95rem;
}

/* The tier headline over the regulars list. Tight to the sentence above it,
   because the two lines are one explanation. */
.regulars-holding {
  margin-top: 0.15rem;
  font-size: 0.9rem;
}

.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: 0.9rem;
  /* The empty spans that stand in for a missing Previous or Next are what
     keep "Page 2 of 7" in the middle rather than letting it slide to one end,
     and the height keeps both links a thumb's worth of target apart. */
  min-height: 2.75rem;
}

.empty {
  text-align: center;
}

/* -- Referrals (ETE-27, ETE-47) ------------------------------------------- */

.invitation {
  /* "Ana invited you to Café Plaza", at the top of a shop's public page. Given
     the accent rather than a card of its own: it is one line explaining why
     this page was opened, and a box around it would compete with the deal
     underneath, which is the thing the page is actually for. */
  margin: 0 0 var(--gap);
  font-weight: 600;
  color: var(--accent);
}

.invitation-offer {
  /* "Come in for the first time and you will both get an extra stamp"
     (ETE-47), under the greeting it belongs to. Quiet rather than accented:
     the line above is why this page was opened and the deal below is what the
     page is for, and a third thing shouting would leave the reader with
     nothing to look at first. */
  margin: 0 0 var(--gap);
  color: var(--ink-soft);
}

.invite {
  margin-top: 0.75rem;
  border-top: 1px solid var(--line);
  padding-top: 0.75rem;
}

.invite summary {
  /* A summary is focusable and toggles on Enter by itself; the cursor is the
     only thing the browser does not make obvious. */
  cursor: pointer;
  color: var(--accent);
}

.invite p {
  margin: 0.5rem 0;
}

.share-link {
  /* Monospaced, like every other code in this app: the last eight characters
     of this URL are the code, and somebody reading them down a phone needs
     them one at a time. The size stays whatever the shared input rule says --
     `font: inherit`, which is 16px, which is what stops iOS Safari zooming the
     page the moment the field is focused. */
  font-family: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace;
}

/* -- Insights (ETE-29) ----------------------------------------------------- */

/* Off the screen and still in the accessibility tree. `display: none` and
   `visibility: hidden` take an element out of both, which is the opposite of
   what the chart's table is for -- it exists precisely to be read aloud. The
   1px clipped box rather than a negative text-indent because it works for a
   <table>, and `white-space: nowrap` stops a long cell being wrapped into a
   column of single characters, which some screen readers then spell out. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* And the one part of that recipe a <table> does not honour. A table's used
   width is at least its min-content width under automatic layout, so
   `width: 1px` above left the chart's hidden table around 360px wide, absolutely
   positioned, and its border box past the right edge of a 380px screen -- which
   put the whole insights page into horizontal scroll while painting nothing
   anybody could see. `table-layout: fixed` is what makes the declared width
   authoritative, and it costs the table nothing: it is read aloud, never laid
   out. Found while checking this page at 380px for ETE-50; it arrived with the
   chart in ETE-29. */
table.visually-hidden {
  table-layout: fixed;
}

/* One column on a phone, two once there is room for a pair of numbers to be
   compared without moving your eyes down the page. Four across is never right
   here: at --measure each tile would be eight characters wide. */
.figures {
  display: grid;
  gap: var(--gap);
  margin-bottom: var(--gap);
}

@media (min-width: 30rem) {
  .figures {
    grid-template-columns: 1fr 1fr;
  }
}

.figure {
  /* The grid owns the spacing between tiles; .card's own bottom margin would
     add a second, uneven one under the first row only. */
  margin: 0;
}

.figure h2 {
  font-size: 0.9rem;
  font-weight: 600;
  margin: 0;
  color: var(--ink-soft);
}

.figure-value {
  /* The one thing on the tile read from across a counter. Tabular figures so
     that four tiles of different numbers still line up as a block, and a
     tight line-height because this is a single line that never wraps. */
  margin: 0.1rem 0 0;
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.figure-detail,
.figure-previous {
  margin: 0.15rem 0 0;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

/* Direction is carried by the sign in the text as well as by this colour, so
   the tile still reads correctly in greyscale and to the one reader in twelve
   who cannot tell these two hues apart. */
.delta.up {
  color: var(--ok);
}

.delta.down {
  color: var(--danger);
}

/* Full width, aspect kept by the viewBox: the drawing is the same 5:2 shape
   whether it holds three weeks or twelve, so a young shop's chart is a chart
   rather than a tall sliver. height:auto is what makes the viewBox the single
   place the proportions are decided. */
.chart {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: 0.5rem;
}

/* The two rules that are the whole of "no colour literal in the SVG". Every
   fill in the drawing resolves through a custom property, so the dark-mode
   block at the top of this file -- which moves --accent from #1a5fd0 to
   #4d9bff -- reaches the chart along with everything else. A fill attribute
   in the template would be unreachable from here and would keep its
   light-mode blue on a dark background. */
.chart-bar {
  fill: var(--accent);
}

.chart-floor {
  fill: var(--line);
}

/* The top of the scale: the same neutral as the floor, and thinner in the
   geometry, so it frames the bars without competing with them. */
.chart-grid {
  fill: var(--line);
}

/* Text inside the drawing is filled, not coloured -- `color` does nothing to
   SVG text -- and filled from the same two inks as the prose around it, so
   the numbers follow the dark-mode block like everything else. The size is a
   `font-size` attribute in viewBox units rather than a rule here; see
   CHART_TEXT in apps/businesses/insights.py for why it cannot be pixels. */
.chart-value {
  fill: var(--ink);
  font-variant-numeric: tabular-nums;
}

.chart-scale,
.chart-axis {
  fill: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* This week is not over. Drawn softer so a Monday-morning stub does not read
   as a collapse beside eleven finished weeks -- opacity rather than a second
   colour, so there is still exactly one accent in the drawing and it is still
   the one the theme decides. */
.chart-bar.partial {
  opacity: 0.45;
}

.chart-legend {
  margin: 0 0 0.25rem;
  font-size: 0.85rem;
}

/* -- htmx ------------------------------------------------------------------ */

/* htmx's own indicator rules, shipped here instead of injected (QA-SEC-1).
   htmx adds them as an inline <style> at load, which `style-src 'self'` blocks
   and reports on every page; base.html turns that off with
   `includeIndicatorStyles: false`, so the rules live in this file. No template
   uses an indicator today -- this keeps the class meaning what htmx documents
   for the first one that does. */
.htmx-indicator {
  opacity: 0;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
  transition: opacity 200ms ease-in;
}
