/* ============================================================
   NextUp — design system
   Clean, neutral and modern (think Fresha): white ground, soft
   grey lines, black actions. The working accent is a deep sea
   teal; the Trinidad & Tobago crimson is held back for the mark,
   the flag band and true danger — so it lands when it appears.
   ============================================================ */

:root {
  /* The height the demo / impersonation bar actually occupies, safe area
     included. Everything that sits under that bar offsets by this, so the
     bar and the gap it leaves can never disagree. */
  --imp-h: calc(40px + env(safe-area-inset-top, 0px));
  /* NextUp is a light interface and has no dark theme.
     Declaring that outright stops Samsung Internet and Chrome's
     "force dark" from inverting the page themselves — which is what
     turned the client's booking screen dark grey on Samsung phones,
     dragging the shop's logo and photos down with it. A browser only
     auto-inverts pages that never say what they support. */
  color-scheme: light;

  /* --- the working accent: black ---
     This used to be a deep sea teal, and it carried every selected state,
     every progress marker and every icon chip in both apps. It now reads
     black on a soft grey, which is the register the rest of the interface
     was already in — white ground, grey lines, black actions.

     Green is not gone; it is reserved. It now means exactly two things,
     and a barber can trust it on sight: WhatsApp (.btn.wa), and work that
     is approved or finished (.btn.good, .btn.ca-done, .badge.completed).
     Nothing decorative is allowed to borrow it, because a signal only
     works while it means one thing.

     Every rule in both stylesheets reads these five tokens rather than
     writing a colour of its own, so the whole interface moves together
     from here. --accent is the fill, --accent-deep and --accent-dark are
     ink on a pale ground, --accent-tint is that ground, --accent-line the
     hairline around it. */
  --accent:      #101012;
  --accent-deep: #101012;
  --accent-dark: #000000;
  --accent-tint: #F2F2F4;
  --accent-line: #D8D8DE;
  /* Named so the two hover rules that reach for it stop falling back to a
     pale teal that no longer belongs to anything. */
  --accent-soft: #F3F3F5;

  /* --- reserved: the Trini crimson --- */
  --red:        #CE1126;
  --red-deep:   #A50E1E;
  --red-dark:   #7E0A17;
  --red-tint:   #FDF0F1;
  --red-line:   #F2CDD2;

  /* --- neutral ground --- */
  --ink:        #101012;
  --ink-2:      #36363C;
  --ink-3:      #6E6E76;
  --ink-4:      #9C9CA3;

  --cream:      #FBFBFC;
  --cream-2:    #F3F3F5;
  --card:       #FFFFFF;
  --line:       #E8E8EB;
  --line-soft:  #F1F1F3;

  --green:      #16785A;
  --green-tint: #E6F4EF;
  --amber:      #A96A12;
  --amber-tint: #FBF0DC;

  /* --- the message blue ---
     Two things in this app are the app talking TO the barber rather than
     showing him his own work: the toast and the tutorial card. Both were
     near-black slabs, which is the register of system chrome — the exact
     thing everyone has learned to look past. They are drawn as messages
     now, in the shape his phone already shows him a hundred times a day.

     Reserved for that. Nothing structural borrows these, for the same
     reason green is reserved: a shape only says something while it says
     one thing. --msg-ink is near-black with a blue cast rather than plain
     ink, so the writing sits IN the bubble instead of on top of it. */
  --msg:        #DCEBFC;
  --msg-line:   #C2DCF6;
  --msg-ink:    #12283D;
  --msg-ink-2:  #40607C;

  /* --- type --- */
  /* --- type ---
     Outfit carries the headings and the money: geometric, confident, and it
     is what a shop owner sees first. Manrope does the reading — it was drawn
     for interfaces, with wide open counters that stay legible at 11px on a
     phone held at arm's length in daylight, which is how this app is used.

     The two are both semi-geometric, so the pairing reads as one voice
     rather than two. Hierarchy therefore rests on size and weight rather
     than on a change of letterform — which is why the weights below are not
     decoration. The face that used to do both jobs, Space Grotesk, was a
     display face: right large, unsettled small.

     On weight: body text sits at 500, not 400. A regular weight is thin
     enough to wash out in bright light. Names and labels go to 600 so they
     can be scanned rather than read. Nothing routine uses 700 — when
     everything is bold, nothing is.

     One cost worth remembering: Manrope is wider than a humanist face, so a
     long detail line wraps sooner on a small phone. If a list starts looking
     cramped, that is the reason. */
  --display: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --body: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  /* The two weights the body face actually uses, named so a future change
     happens in one place instead of ninety. */
  --w-read: 500;      /* supporting detail: times, services, captions */
  --w-name: 600;      /* things you scan for: names, labels, totals */

  /* --- shape --- */
  --r-sm: 8px;
  --r:    12px;
  --r-lg: 18px;
  --r-xl: 24px;

  /* Neutral shadows — no warm cast bleeding into the ground. */
  --shadow-sm: 0 1px 2px rgba(16,16,18,.05), 0 1px 3px rgba(16,16,18,.04);
  --shadow:    0 2px 4px rgba(16,16,18,.04), 0 8px 20px -6px rgba(16,16,18,.10);
  --shadow-lg: 0 4px 8px rgba(16,16,18,.05), 0 24px 48px -12px rgba(16,16,18,.18);
  --shadow-accent: 0 6px 18px -6px rgba(16,16,18,.42);
  --shadow-red: 0 6px 18px -6px rgba(206,17,38,.40);

  /* --- motion (one language, used everywhere) --- */
  --ease:      cubic-bezier(.22, 1, .36, 1);
  --ease-in:   cubic-bezier(.55, 0, 1, .45);
  --ease-soft: cubic-bezier(.4, 0, .2, 1);
  --t-fast: .16s;
  --t:      .3s;
  --t-slow: .6s;

  --sidebar-w: 236px;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* text-size-adjust: 100% stops iOS bumping font sizes on rotate.
   overflow-x clip on both html and body is the belt-and-braces against any
   single wide element dragging the whole page sideways (which reads as a
   zoomed-out page on a phone). */
html { -webkit-text-size-adjust: 100%; overflow-x: clip; }

body {
  font-family: var(--body);
  font-weight: var(--w-read);
  background: var(--cream);
  color: var(--ink);
  line-height: 1.55;
  font-size: 15px;
  min-height: 100vh;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Clean, even ground — no tints, no motifs. The colour lives in the
   content, not behind it. */

/* ============================================================
   Keyframes — the shared motion vocabulary
   ============================================================ */

@keyframes drift      { from { transform: rotate(24deg) translateY(0); }
                        to   { transform: rotate(24deg) translateY(-3.5rem); } }
@keyframes rise       { from { opacity: 0; transform: translateY(14px); }
                        to   { opacity: 1; transform: none; } }
@keyframes riseSm     { from { opacity: 0; transform: translateY(7px); }
                        to   { opacity: 1; transform: none; } }
@keyframes fade       { from { opacity: 0; } to { opacity: 1; } }
@keyframes popIn      { 0%   { opacity: 0; transform: translateY(18px) scale(.97); }
                        100% { opacity: 1; transform: none; } }
@keyframes slideInR   { from { opacity: 0; transform: translateX(26px); }
                        to   { opacity: 1; transform: none; } }
@keyframes pole       { to { background-position: 64px 0; } }
@keyframes shimmer    { to { background-position: 220% 0; } }
@keyframes pulseRing  { 0%   { box-shadow: 0 0 0 0 rgba(16,16,18,.26); }
                        70%  { box-shadow: 0 0 0 12px rgba(16,16,18,0); }
                        100% { box-shadow: 0 0 0 0 rgba(16,16,18,0); } }
@keyframes pulseRingRed { 0%   { box-shadow: 0 0 0 0 rgba(206,17,38,.34); }
                        70%  { box-shadow: 0 0 0 12px rgba(206,17,38,0); }
                        100% { box-shadow: 0 0 0 0 rgba(206,17,38,0); } }
@keyframes spin       { to { transform: rotate(360deg); } }
@keyframes growBar    { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes checkPop   { 0%   { transform: scale(.4); opacity: 0; }
                        60%  { transform: scale(1.12); opacity: 1; }
                        100% { transform: scale(1); opacity: 1; } }

/* ============================================================
   Utilities
   ============================================================ */

.hidden { display: none !important; }
.muted  { color: var(--ink-3); font-size: 13.5px; }
.center { text-align: center; }
.right  { text-align: right; }
.nowrap { white-space: nowrap; }

a { color: var(--accent-deep); text-decoration: none; transition: color var(--t-fast) var(--ease); }
a:hover { color: var(--accent); text-decoration: underline; }

h1, h2, h3 { font-family: var(--display); font-weight: 700; letter-spacing: -.018em; color: var(--ink); }
h1 { font-size: 26px; line-height: 1.2; }
h2 { font-size: 18px; margin-bottom: 14px; }
h3 { font-size: 15.5px; }

/* ============================================================
   Brand lockup
   ============================================================ */

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 21px;
  letter-spacing: -.02em;
  color: var(--ink);
}
.brand-mark {
  width: 34px;
  height: 34px;
  flex: 0 0 34px;
  border-radius: 9px;
  background: var(--red) url('/icons/logo.svg') center/cover no-repeat;
  box-shadow: var(--shadow-red);
  transition: transform var(--t) var(--ease);
  font-size: 0;
}
.brand:hover .brand-mark { transform: rotate(-6deg) scale(1.05); }
/* Shop name over the barber's own name, for a team member. */
.brand-names { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
.brand-barber {
  font-family: var(--body);
  font-weight: 500;
  font-size: 12px;
  color: var(--accent-deep);
  letter-spacing: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* --- Notification bell (header, both sidebar and mobile bar) --- */
.bell-btn {
  margin-left: auto;
  position: relative;
  flex: 0 0 auto;
  width: 40px; height: 40px;
  display: grid;
  place-content: center;
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  color: var(--ink);
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.bell-btn:hover { border-color: var(--accent); background: var(--accent-soft, #f1f7f6); }
.bell-btn svg { width: 21px; height: 21px; }
.bell-badge {
  position: absolute;
  top: -6px; right: -6px;
  min-width: 18px; height: 18px;
  padding: 0 5px;
  display: grid;
  place-content: center;
  background: var(--red);
  color: #fff;
  font-family: var(--body);
  font-weight: 700;
  font-size: 11px;
  line-height: 1;
  border-radius: 999px;
  border: 2px solid var(--bg, #fff);
}
.bell-badge.hidden { display: none; }

/* --- Notification panel --- */
.note-backdrop {
  position: fixed;
  inset: 0;
  z-index: 120;
  background: rgba(20, 24, 28, .3);
  backdrop-filter: blur(10px) saturate(140%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  display: flex;
  justify-content: flex-end;
  animation: note-fade var(--t-fast) var(--ease);
}
@keyframes note-fade { from { opacity: 0 } to { opacity: 1 } }
.note-panel {
  width: min(420px, 100%);
  height: 100%;
  background: var(--card);
  border-left: 1px solid var(--line);
  box-shadow: -18px 0 48px rgba(20,24,28,.16);
  display: flex;
  flex-direction: column;
  animation: note-slide var(--t) var(--ease);
}
@keyframes note-slide { from { transform: translateX(24px); opacity: .6 } to { transform: none; opacity: 1 } }
.note-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: calc(16px + env(safe-area-inset-top)) 18px 14px;
  border-bottom: 1px solid var(--line);
}
.note-head h2 { margin: 0; font-family: var(--display); font-size: 19px; font-weight: 700; }
.note-head .btn-row { display: flex; align-items: center; gap: 6px; }
.note-list { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.note-item {
  display: flex;
  gap: 12px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: var(--ink);
  transition: background var(--t-fast) var(--ease);
}
.note-item:hover { background: var(--accent-soft, #f1f7f6); }
.note-item.unread { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.note-item.unread .note-ic { color: var(--accent-deep); }
.note-ic {
  flex: 0 0 34px;
  width: 34px; height: 34px;
  display: grid;
  place-content: center;
  border-radius: 10px;
  background: var(--bg, #f6f7f8);
  color: var(--muted);
}
.note-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.note-body b { font-weight: 700; font-size: 14.5px; }
.note-body > span { font-size: 13.5px; color: var(--muted); }
.note-when { font-size: 12px !important; color: var(--muted); opacity: .8; }
.note-item.unread .note-body b::after {
  content: '';
  display: inline-block;
  width: 7px; height: 7px;
  margin-left: 7px;
  border-radius: 50%;
  background: var(--red);
  vertical-align: middle;
}

/* A barber's own logo replaces our mark. It gets a hairline instead of the
   crimson glow — that shadow belongs to the NextUp mark, and would tint
   somebody else's brand. */
.brand-mark.own-logo {
  box-shadow: none;
  border: 1px solid var(--line);
  background-size: cover;
  background-position: center;
}
.brand-mark.lg { width: 56px; height: 56px; flex-basis: 56px; border-radius: 14px; }
.brand-mark.xl { width: 76px; height: 76px; flex-basis: 76px; border-radius: 19px; }

/* ============================================================
   Auth screen
   ============================================================ */

.auth-wrap {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px;
}
.auth-card {
  width: 100%;
  max-width: 400px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: 34px 30px 28px;
  box-shadow: var(--shadow-lg);
  animation: popIn var(--t-slow) var(--ease) both;
  position: relative;
  overflow: hidden;
}
/* flag band across the top of the card */
.auth-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 5px;
  background: linear-gradient(90deg, var(--red) 0 42%, var(--ink) 42% 58%, var(--red) 58% 100%);
}
.auth-card .brand { justify-content: center; font-size: 26px; margin-bottom: 6px; }
.auth-card form { margin-top: 20px; }
.auth-card .muted.center { margin-top: 16px; }

/* ============================================================
   App shell
   ============================================================ */

.layout {
  display: grid;
  /* minmax(0, 1fr) not 1fr: a grid column defaults to a min size of its
     content, so one wide child (a table, a long link) would stretch the
     column past the screen and make the whole page scroll sideways — which
     on an iPhone shows up as the page zoomed out. Clamping the minimum to 0
     keeps the content column inside the viewport; anything too wide scrolls
     within itself instead. */
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100vh;
  min-height: 100dvh;
}

.sidebar {
  /* True vibrancy formula, not just a translucent panel — saturate(180%)
     is what actually gives the frosted-glass look its life; blur alone
     just looks like fog. This is the same pairing Apple's own toolbars use. */
  background: rgba(255,255,255,.72);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-right: 1px solid var(--line);
  padding: 22px 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 22px;
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
}
.sidebar nav { display: flex; flex-direction: column; gap: 2px; flex: 1; }

/* The menu in four groups rather than one column of thirteen. The headings
   are quiet on purpose — they are there to break the run of links into
   things the eye can take in at a glance, not to be read. The first and
   last groups have no heading at all: "Today" needs no label, and neither
   does "Settings" sitting on its own at the bottom. */
.sidebar nav .nav-group { display: flex; flex-direction: column; gap: 2px; }
.sidebar nav .nav-group + .nav-group { margin-top: 14px; }
.sidebar nav .nav-head {
  padding: 0 12px 4px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.sidebar nav a {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: var(--r);
  color: var(--ink-2);
  font-size: 14.5px;
  font-weight: 500;
  text-decoration: none;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.sidebar nav a::before {
  content: '';
  position: absolute;
  left: 0; top: 50%;
  width: 3px; height: 0;
  border-radius: 0 3px 3px 0;
  background: var(--accent);
  transform: translateY(-50%);
  transition: height var(--t) var(--ease);
}
.sidebar nav a .nav-ic {
  display: inline-flex;
  align-items: center;
  color: var(--ink-4);
  flex: 0 0 auto;
  transition: color var(--t-fast) var(--ease);
}
.sidebar nav a:hover { background: var(--cream-2); color: var(--ink); text-decoration: none; transform: translateX(2px); }
.sidebar nav a:hover .nav-ic { color: var(--ink-2); }
.sidebar nav a.active .nav-ic { color: var(--accent); }
.sidebar nav a.active { background: var(--accent-tint); color: var(--accent-dark); font-weight: 600; }
.sidebar nav a.active::before { height: 60%; }
.sidebar nav a .count {
  margin-left: auto;
  min-width: 20px;
  padding: 0 6px;
  height: 20px;
  border-radius: 10px;
  background: var(--accent);
  color: #fff;
  font-size: 11.5px;
  font-weight: 700;
  display: grid;
  place-items: center;
  animation: pulseRing 2.4s var(--ease-soft) infinite;
}

main {
  padding: 30px 34px 60px;
  max-width: 1180px;
  width: 100%;
  min-width: 0;   /* let the grid clamp it to the viewport (see .layout) */
  overflow-x: clip;   /* a stray wide child is contained, never the page */
}
main.swap { animation: rise .42s var(--ease) both; }

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 22px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--line);
}
.page-head .sub { color: var(--ink-3); font-size: 14px; margin-top: 2px; }

/* ============================================================
   Cards, grids, stats
   ============================================================ */

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 20px;
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--t) var(--ease), transform var(--t) var(--ease), border-color var(--t) var(--ease);
  animation: rise .5s var(--ease) both;
}
.card:hover { box-shadow: var(--shadow); }

.grid { display: grid; gap: 16px; }
.grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid.cols-4 { grid-template-columns: repeat(4, 1fr); }

/* staggered entrance, shared by every grid in the app */
.grid > *:nth-child(1) { animation-delay: .02s; }
.grid > *:nth-child(2) { animation-delay: .07s; }
.grid > *:nth-child(3) { animation-delay: .12s; }
.grid > *:nth-child(4) { animation-delay: .17s; }
.grid > *:nth-child(5) { animation-delay: .22s; }
.grid > *:nth-child(6) { animation-delay: .27s; }
.grid > *:nth-child(n+7) { animation-delay: .3s; }

.stat { position: relative; overflow: hidden; }
.stat .label {
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: .09em;
  font-weight: 600;
  color: var(--ink-4);
}
.stat .value {
  font-family: var(--display);
  font-size: 30px;
  font-weight: 700;
  line-height: 1.15;
  margin-top: 6px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.stat .value.accent { color: var(--accent-deep); }

/* ============================================================
   Forms
   ============================================================ */

label {
  display: block;
  margin-bottom: 14px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}
/* "Add to Home Screen" card at the foot of Shop basics. */
.install-cta {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 20px;
  padding: 16px;
  border: 1px solid var(--accent-line, var(--line));
  border-radius: var(--r);
  background: var(--accent-tint);
}
.install-cta.hidden { display: none; }
.install-cta-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.install-cta-text b { display: inline-flex; align-items: center; gap: 7px; color: var(--accent-dark); }
.install-cta .btn { flex: 0 0 auto; margin-left: auto; }
.install-steps { margin: 10px 0 4px 18px; display: grid; gap: 9px; color: var(--ink-2); line-height: 1.5; }
.install-steps li { padding-left: 4px; }
.ios-share {
  display: inline-grid; place-items: center;
  width: 22px; height: 22px; vertical-align: -5px;
  border: 1px solid var(--line); border-radius: 6px;
  font-size: 13px;
}
@media (max-width: 560px) {
  .install-cta { flex-direction: column; align-items: stretch; }
  .install-cta .btn { margin-left: 0; width: 100%; }
}

/* A small glyph sitting beside a field label — Instagram, Facebook, a pin. */
.lbl-ic { display: inline-flex; align-items: center; gap: 6px; }
.lbl-ic .ui-icon { color: var(--muted); flex: 0 0 auto; }
/* Interface glyphs default to sitting on the text baseline nicely inline. */
.ui-icon { flex: 0 0 auto; vertical-align: -2px; }
/* Phone · email under a client's name, each with its own small glyph. */
.contact-line { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.contact-line .ui-icon { color: var(--muted); }
.contact-line .ui-icon:not(:first-child) { margin-left: 8px; }
input, select, textarea {
  width: 100%;
  margin-top: 6px;
  padding: 10px 12px;
  font-family: inherit;
  font-size: 15px;
  color: var(--ink);
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
  -webkit-appearance: none;
  appearance: none;
}
select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' stroke='%236B645A' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 12px;
  padding-right: 34px;
}
textarea { min-height: 84px; resize: vertical; line-height: 1.5; }
input:focus, select:focus, textarea:focus {
  outline: none;
  /* Focus glows in the app's own accent rather than flat black — it reads
     as the interface responding to you, not just a validation outline. */
  border-color: var(--accent);
  box-shadow: 0 0 0 4px rgba(14,110,94,.16);
}
input::placeholder, textarea::placeholder { color: var(--ink-4); }

/* Checkbox rows — "keep me signed in" and friends */
.check-row {
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 500;
  color: var(--ink-2);
  cursor: pointer;
  margin: 2px 0 16px;
}
.check-row input[type="checkbox"] {
  width: 18px; height: 18px;
  margin: 0;
  flex: 0 0 18px;
  accent-color: var(--accent);
  -webkit-appearance: checkbox;
  appearance: checkbox;
}

/* The list of things a shop can switch on and off — in Settings, and in the
   admin panel when a rep is setting somebody up. Each one is a row you can
   tap anywhere on, with the box aligned to the title rather than floating in
   the middle of a two-line description. */
.feat-list { display: flex; flex-direction: column; }
.feat-row {
  align-items: flex-start;
  margin: 0;
  padding: 13px 2px;
  border-top: 1px solid var(--line-soft);
}
.feat-row:first-child { border-top: none; padding-top: 4px; }
.feat-row input[type="checkbox"] { margin-top: 2px; }
.feat-row b { color: var(--ink); font-weight: 600; }
.feat-row .muted { font-size: 13px; line-height: 1.5; display: inline-block; margin-top: 2px; }

/* Two columns while two columns fit, and one when they do not.

   A fixed 1fr 1fr put "Appointment slots (min)" and "Least notice (hours)"
   side by side on a 360px phone: two labels wrapping to three lines each,
   at different heights, over two boxes barely wide enough for the number
   inside them. The Collect payment panel had the same problem — Amount
   and Method crushed together at the moment a barber is standing at the
   counter with a client waiting.

   auto-fit with a floor means the browser decides: below roughly 380px of
   panel the pair stacks and each field gets the full width, above it they
   sit side by side as before. align-items:end keeps the boxes on one line
   when the labels above them wrap to different heights, which is the other
   half of why this looked crooked. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0 14px;
  align-items: end;
}
.form-grid .full { grid-column: 1 / -1; }
/* Fields carry their own bottom spacing; without this the stacked layout
   closes up and the two rows read as one control. */
.form-grid > label { min-width: 0; }

/* ============================================================
   Buttons
   ============================================================ */

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 10px 17px;
  font-family: inherit;
  font-size: 14.5px;
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink);
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  cursor: pointer;
  white-space: nowrap;
  /* Every property here only moves in response to :hover or :active below.
     Nothing on a button animates while it is being ignored. */
  transition: transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
/* Lift only where a cursor exists. On a phone there is no hover state, and
   a rule that assumes one leaves the button stuck in its raised form after
   a tap. */
@media (hover: hover) {
  .btn:hover { transform: translateY(-1px); box-shadow: var(--shadow); border-color: var(--ink-4); }
}
.btn:active { transform: translateY(0) scale(.975); }
.btn:disabled { opacity: .5; cursor: not-allowed; transform: none; box-shadow: none; }

/* Primary actions are black, Fresha-style — calm and confident.
   Teal carries the everyday accent work; crimson stays reserved
   for the brand mark, the flag band and genuine danger. */
.btn.primary {
  background: var(--ink);
  border-color: transparent;
  color: #fff;
  box-shadow: 0 6px 16px -8px rgba(16,16,18,.5);
}
.btn.primary:hover { background: #26262C; border-color: transparent; }

/* There used to be a white gradient sweep laid over every primary button,
   animated on hover. Two problems with it. The overlay existed at rest, not
   only during the animation, so a pale sheen sat permanently across the
   button and made a flat black surface look plasticky. And the flourish drew
   the eye to Save every time the cursor passed nearby, which is attention
   spent on nothing.

   A button should look completely still until it is touched. What movement
   there is now lives entirely in :hover and :active below — a small lift and
   a small press, both of which report something real rather than decorate. */

.btn.dark { background: var(--ink); border-color: var(--ink); color: #fff; }
.btn.dark:hover { background: var(--ink-2); border-color: var(--ink-2); }

/* WhatsApp green, used where the button really does open WhatsApp and
   nowhere else. A barber recognises this colour before he reads the label,
   which is the whole point — but it only works as a signal while it means
   exactly one thing, so it is not a general-purpose accent. */
.btn.wa { background: #1FA855; border-color: #1FA855; color: #fff; }
.btn.wa:hover { background: #1B9349; border-color: #1B9349; color: #fff; }
.btn.wa .ui-icon { color: #fff; }
.btn.danger { color: var(--red-deep); border-color: var(--red-line); background: var(--red-tint); }
.btn.danger:hover { background: var(--red); border-color: var(--red); color: #fff; }
.btn.good { color: var(--green); border-color: #BFE3D6; background: var(--green-tint); }
.btn.good:hover { background: var(--green); border-color: var(--green); color: #fff; }
.btn.ghost { background: transparent; border-color: var(--line); color: var(--ink-3); }
.btn.ghost:hover { background: var(--cream-2); color: var(--ink); }
.btn.small { padding: 6px 11px; font-size: 13px; border-radius: var(--r-sm); }
/* A full-width button is always the one decision on its screen — login,
   save, confirm. Capsule radius is what makes it read as *the* action,
   the way iOS treats its single prominent button on a sheet or a form. */
.btn.wide { width: 100%; border-radius: 999px; padding: 12px 20px; }

.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }

/* ============================================================
   Tables
   ============================================================ */

table { width: 100%; border-collapse: collapse; font-size: 14.5px; }
th {
  text-align: left;
  padding: 9px 12px;
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: .07em;
  font-weight: 600;
  color: var(--ink-4);
  border-bottom: 1.5px solid var(--line);
}
td { padding: 12px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tbody tr { animation: riseSm .4s var(--ease) both; }
tbody tr:nth-child(1) { animation-delay: .01s; }
tbody tr:nth-child(2) { animation-delay: .04s; }
tbody tr:nth-child(3) { animation-delay: .07s; }
tbody tr:nth-child(4) { animation-delay: .10s; }
tbody tr:nth-child(5) { animation-delay: .13s; }
tbody tr:nth-child(n+6) { animation-delay: .16s; }
tr.clickable { cursor: pointer; transition: background var(--t-fast) var(--ease); }
tr.clickable:hover td { background: var(--cream-2); }

/* ============================================================
   Badges
   ============================================================ */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: 100px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .01em;
  border: 1px solid transparent;
  white-space: nowrap;
}
.badge::before {
  content: '';
  width: 5px; height: 5px;
  border-radius: 50%;
  background: currentColor;
}
/* Booked is the good state — the chair is sold — so it is the one badge
   that carries colour. Green tint, green rule, green dot: readable at a
   glance down a column of times without shouting over the client's name.
   Same pill everywhere it appears: Today, Calendar, the shop's day, a
   client's history. */
.badge.booked {
  background: var(--green-tint);
  color: var(--green);
  border-color: #BFE0D4;
}
.badge.booked::before { background: var(--green); }
.badge.pending   { background: var(--amber-tint); color: var(--amber);    border-color: #EFD9AE; }
.badge.pending::before { animation: pulseRing 2s var(--ease-soft) infinite; }
/* Done is a settled state, not something that needs attention — so no filled
   background, just a clear green tick and a calm label. Reads the same on
   every screen: Today, Calendar, the shop's day, a client's history. */
.badge.completed {
  background: transparent;
  color: var(--ink-2);
  border-color: transparent;
  padding-left: 0;
  gap: 6px;
}
.badge.completed::before {
  content: '';
  width: 5px; height: 9px;
  border: solid var(--green);
  border-width: 0 2px 2px 0;
  border-radius: 0;
  background: none;
  transform: rotate(45deg);
  margin: -2px 1px 1px 1px;
}
.badge.cancelled { background: var(--cream-2);    color: var(--ink-3);    border-color: var(--line); }
.badge.no_show   { background: #F6E9E9;           color: #8C3B3B;         border-color: #E8CACA; }
.badge.walkin    { background: var(--cream-2);    color: var(--ink-2);    border-color: var(--line); }

/* ============================================================
   Appointment rows
   ============================================================ */

.appt-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 4px;
  border-bottom: 1px solid var(--line-soft);
  animation: riseSm .42s var(--ease) both;
  transition: background var(--t-fast) var(--ease);
  border-radius: var(--r-sm);
}
.appt-row:hover { background: var(--cream-2); }
.appt-row:last-child { border-bottom: none; }
.appt-row:nth-child(1) { animation-delay: .02s; }
.appt-row:nth-child(2) { animation-delay: .06s; }
.appt-row:nth-child(3) { animation-delay: .10s; }
.appt-row:nth-child(4) { animation-delay: .14s; }
.appt-row:nth-child(n+5) { animation-delay: .18s; }

/* A row leaving after it has been answered.

   Height is animated as well as opacity so the rows below close the gap
   instead of jumping into it. The height itself is pinned in JS first —
   a transition out of `auto` does not run — and the row is taken off the
   page when this finishes. */
.appt-row.going {
  animation: none;
  pointer-events: none;
  height: 0 !important;
  min-height: 0;
  padding-top: 0;
  padding-bottom: 0;
  opacity: 0;
  transform: translateX(16px);
  border-bottom-color: transparent;
  transition: height .26s var(--ease-in),
              padding .26s var(--ease-in),
              opacity .18s var(--ease-in),
              transform .26s var(--ease-in);
}

/* The left-hand column of a booking row.

   Most rows put the time there on its own; Today's schedule puts the time
   and the state of the booking there together, wrapped in .appt-when, so
   the badge sits under the time rather than being left to find its own
   place — which on a phone landed it under the buttons. Both shapes get
   the same width and the same accent spine, so the two kinds of row line
   up with each other down the page. */
.appt-row > .appt-time,
.appt-when {
  flex: 0 0 104px;
  padding-left: 8px;
  border-left: 3px solid var(--accent);
}
.appt-when {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}
.appt-time {
  font-family: var(--display);
  font-weight: 700;
  font-size: 16px;
  color: var(--accent-deep);
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
}
.appt-main { flex: 1; min-width: 0; }
.appt-main strong { font-weight: 600; }
.appt-main .svc { color: var(--ink-3); font-size: 13px; }
.appt-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }

/* ============================================================
   Today page — compact layout, minimal scrolling
   ============================================================ */

/* one-line requests alert instead of a tall card */
.alert-line {
  /* Plain text flow, not flex: the message is a sentence with a bold number
     in it, and it must wrap like a sentence. As a flex row the number and the
     words became separate items and the wrapping broke. */
  display: block;
  padding: 11px 15px;
  margin-bottom: 14px;
  background: var(--accent-tint);
  border: 1px solid var(--accent-line);
  border-radius: var(--r);
  color: var(--accent-dark);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.5;
  text-decoration: none;
  animation: riseSm .4s var(--ease) both;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.alert-line b { vertical-align: baseline; }
/* The call to action sits on its own line under the summary. */
.al-cta { display: block; margin-top: 3px; color: var(--accent-deep); }
.alert-line:hover { background: #E7E7EB; text-decoration: none; transform: translateY(-1px); }
.alert-line b { font-size: 16px; }

/* Reminders waiting to go out. Real, but not the same order of business as
   a client waiting on an answer — so it says the same thing in a quieter
   voice, and sits under the approvals line rather than beside it. */
.alert-line.quiet {
  background: var(--card);
  border-color: var(--line);
  color: var(--ink-2);
  font-weight: 500;
}
.alert-line.quiet b { font-size: 15px; color: var(--ink); }
.alert-line.quiet .al-cta { color: var(--ink-3); }
.alert-line.quiet:hover { background: var(--cream-2); }

/* Two figures, not four. "Appointments today" and "Completed" were the same
   fact twice, and "Reminders to send" was a zero most days — it is a line
   above now, and only when there is something to send. auto-fit rather than
   a fixed count so a shop with Reports switched off gets one full-width
   tile instead of a half-empty row. */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px;
  margin-bottom: 14px;
}
.mini-stat {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 10px 14px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow-sm);
  text-decoration: none;
  animation: riseSm .4s var(--ease) both;
  min-width: 0;
}
a.mini-stat:hover { border-color: var(--ink-4); text-decoration: none; }
.mini-stat .k {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .07em;
  font-weight: 600;
  color: var(--ink-4);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mini-stat .v {
  font-family: var(--display);
  font-weight: 700;
  font-size: 19px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}
.mini-stat .v.accent { color: var(--accent-deep); }
/* "3 / 7" — the second number is the context, so it sits back a shade
   rather than competing with the count that matters. */
.mini-stat .v .v-of { color: var(--ink-4); font-weight: 600; }
.mini-stat .k.sub-k { text-transform: none; letter-spacing: 0; font-weight: 500; }

/* the one appointment that matters right now */
.next-card {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 15px 18px;
  background: var(--card);
  border: 1.5px solid var(--accent-line);
  border-left: 4px solid var(--accent);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  animation: rise .45s var(--ease) both;
}
.next-card .nc-when {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  flex: 0 0 auto;
}
.next-card .nc-when .badge { margin-top: 2px; }
.next-card .nc-label {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .09em;
  font-weight: 700;
  color: var(--accent-deep);
}
.next-card .nc-time {
  font-family: var(--display);
  font-weight: 700;
  font-size: 24px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}
.next-card .nc-who { flex: 1; min-width: 0; }
.next-card .nc-who b { display: block; font-size: 15.5px; }
.next-card .nc-who span { color: var(--ink-3); font-size: 13px; }
/* Done, No-show, Edit are three words of very different lengths, and left
   to size themselves they came out as three buttons of three widths with
   the ragged gaps that follow. Equal columns instead — the row reads as a
   set of choices rather than a sentence that ran out of room. */
.next-card .nc-acts {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 8px;
  flex: 0 0 auto;
  min-width: 310px;
}
.next-card .nc-acts .btn { justify-content: center; width: 100%; }

/* collapsible schedule groups */
.tl-group { border-bottom: 1px solid var(--line-soft); }
.tl-group:last-child { border-bottom: none; }
.tl-group summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 4px;
  cursor: pointer;
  list-style: none;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .08em;
  font-weight: 700;
  color: var(--ink-3);
  user-select: none;
}
.tl-group summary::-webkit-details-marker { display: none; }
.tl-group summary::before {
  content: '';
  width: 7px; height: 7px;
  border-right: 2px solid var(--ink-4);
  border-bottom: 2px solid var(--ink-4);
  transform: rotate(-45deg);
  transition: transform var(--t-fast) var(--ease);
  flex: 0 0 7px;
}
.tl-group[open] summary::before { transform: rotate(45deg); }
.tl-group .tl-count {
  margin-left: auto;
  min-width: 22px;
  padding: 1px 7px;
  border-radius: 100px;
  background: var(--cream-2);
  border: 1px solid var(--line);
  text-align: center;
  font-size: 11.5px;
  color: var(--ink-2);
}
.tl-group.muted-group summary { color: var(--ink-4); }
.tl-group.muted-group .appt-row { opacity: .62; }
.appt-row.is-next { background: var(--accent-tint); border-radius: var(--r-sm); }

@media (max-width: 860px) {
  /* Vertical scrolling only — the stats stack into a 2×2 grid that always
     fits the screen, nothing slides sideways. */
  .stat-strip { grid-template-columns: repeat(2, 1fr); gap: 8px; }
  .mini-stat { padding: 9px 12px; }
  .mini-stat .v { font-size: 17px; }
  .next-card { flex-wrap: wrap; gap: 10px; }
  .next-card .nc-acts { width: 100%; min-width: 0; }
}

/* ============================================================
   Modal
   ============================================================ */

.modal-backdrop {
  position: fixed;
  inset: 0;
  /* A lighter tint with a much deeper blur reads as a true sheet scrim —
     the page behind stays sensed, not just darkened. */
  background: rgba(28,26,23,.32);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  display: grid;
  place-items: center;
  padding: 20px;
  z-index: 100;
  animation: fade var(--t) var(--ease) both;
}
.modal {
  width: 100%;
  max-width: 480px;
  max-height: 88vh;
  overflow-y: auto;
  background: var(--card);
  border-radius: var(--r-xl);
  padding: 26px;
  box-shadow: var(--shadow-lg);
  animation: popIn .36s var(--ease) both;
  position: relative;
}
.modal h2 { margin-bottom: 18px; }
.modal form { margin-top: 4px; }
.modal-actions { display: flex; gap: 9px; justify-content: flex-end; margin-top: 20px; flex-wrap: wrap; }

/* ============================================================
   Toast — a message, not a panel

   The tail is a clipped triangle rather than the two-piece cut-out that
   draws a real iOS curve. The cut-out works by painting a notch in the
   colour of whatever is behind the bubble, and behind this one is the
   barber's own chosen backdrop, or a white card, or one of his
   photographs, depending on where he is standing when it appears. A
   triangle is right on all three.
   ============================================================ */

.toast {
  position: fixed;
  bottom: 22px;
  right: 22px;
  z-index: 200;
  padding: 11px 16px 12px;
  background: var(--msg);
  color: var(--msg-ink);
  font-size: 14px;
  font-weight: var(--w-name);
  line-height: 1.45;
  /* Three round corners and one tucked, on the side the tail comes out
     of — the silhouette does as much of the work here as the colour. */
  border-radius: 20px 20px 5px 20px;
  box-shadow: 0 2px 5px rgba(16,16,18,.06), 0 14px 30px -10px rgba(18,40,61,.26);
  animation: slideInR .34s var(--ease) both;
  max-width: min(360px, calc(100vw - 44px));
  /* Nothing on a toast is clickable, and for three and a half seconds it
     was sitting over whatever was underneath it and eating the tap. On a
     phone that is most of the bottom of the screen — including the buttons
     of a panel that has just opened, which is how "I pressed it and
     nothing happened" happens. */
  pointer-events: none;
}
/* `inherit` rather than the token, so the error bubble's tail follows its
   own colour without a second rule. */
.toast::after {
  content: '';
  position: absolute;
  right: -6px;
  bottom: 0;
  width: 14px;
  height: 16px;
  background: inherit;
  clip-path: polygon(0 0, 100% 100%, 0 100%);
}
/* Still a message when it is bad news, so it keeps the shape and changes
   only the colour — a barber reading an error is not being handed a
   different kind of object. */
.toast.err { background: var(--red-tint); color: var(--red-dark); }

.error {
  padding: 10px 13px;
  background: var(--red-tint);
  border: 1px solid var(--red-line);
  border-radius: var(--r);
  color: var(--red-dark);
  font-size: 13.5px;
  font-weight: 500;
  margin-bottom: 14px;
  animation: riseSm var(--t) var(--ease) both;
}

/* ============================================================
   Bar chart
   ============================================================ */

/* Takings chart — a labelled SVG column chart. */
.rc { width: 100%; height: auto; display: block; margin-top: 4px; overflow: visible; }
.rc-grid { stroke: var(--line); stroke-width: 1; shape-rendering: crispEdges; }
.rc-ylab { fill: var(--ink-4); font-size: 11px; font-weight: 600; font-family: var(--body); }
.rc-xlab { fill: var(--ink-3); font-size: 11px; font-weight: 600; font-family: var(--body); }
.rc-bar { fill: var(--accent); opacity: .88; transition: opacity var(--t-fast) var(--ease); }
.rc-bar:hover { opacity: 1; }
.rc-best { fill: var(--accent-deep); opacity: 1; }
.rc-foot {
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft);
  font-size: 13px; color: var(--ink-3);
}
.rc-foot b { color: var(--ink); font-weight: 600; }

/* Report filter bar — client / service / method. */
/* The three dropdowns, folded away. The summary is a control in its own
   right — it has to look tappable and it has to say when a filter is on,
   because a report quietly narrowed to one client is a wrong number that
   looks like a right one. */
.rp-filters-wrap { margin-top: 12px; }
.rp-filters-wrap > summary {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink-2);
  list-style: none;
  user-select: none;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.rp-filters-wrap > summary::-webkit-details-marker { display: none; }
.rp-filters-wrap > summary::after {
  content: '';
  width: 6px; height: 6px;
  border: solid currentColor;
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform var(--t-fast) var(--ease);
}
.rp-filters-wrap[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.rp-filters-wrap > summary:hover { border-color: var(--ink-4); color: var(--ink); }
.rp-fcount {
  min-width: 18px; height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--accent);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
}

.rp-filters { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.rp-fl {
  display: inline-flex; align-items: center; gap: 6px;
  margin: 0; padding: 0 4px 0 10px;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--card); color: var(--ink-3); font-weight: 600; font-size: 13px;
  min-width: 0; flex: 1 1 150px;
}
.rp-fl .ui-icon, .rp-fl .svc-icon { flex: 0 0 auto; color: var(--ink-4); }
.rp-fl select {
  border: none; background: none; margin: 0; padding: 8px 4px;
  font-size: 13.5px; font-weight: 500; color: var(--ink); width: 100%; min-width: 0;
}
.rp-fl select:focus { outline: none; }
#rf-clear { flex: 0 0 auto; align-self: center; }

/* ============================================================
   Day navigator
   ============================================================ */

.day-nav {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 18px;
  flex-wrap: wrap;
}
.day-nav .date-label {
  font-family: var(--display);
  font-weight: 700;
  font-size: 17px;
  min-width: 190px;
  text-align: center;
}

/* ============================================================
   Calendar — week strip, day bar, summary and timeline
   ============================================================ */
.cal-head { padding: 14px; }

/* The seven-day strip: a glance at the whole week, one tap to any day. */
.cal-week {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
  margin-bottom: 14px;
}
.cal-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 8px 2px 7px;
  border: 1px solid transparent;
  border-radius: 12px;
  background: var(--cream-2);
  cursor: pointer;
  transition: transform var(--t-fast) var(--ease), background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.cal-day:hover { transform: translateY(-1px); background: var(--accent-tint); }
.cal-day .cd-dow {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--ink-3);
}
.cal-day .cd-num { font-family: var(--display); font-weight: 700; font-size: 17px; color: var(--ink); line-height: 1; }
.cal-day .cd-dot {
  min-width: 17px; height: 17px; padding: 0 4px;
  display: grid; place-content: center;
  font-size: 10.5px; font-weight: 700;
  border-radius: 100px;
  background: var(--accent); color: #fff;
}
.cal-day .cd-dot.empty { background: transparent; }
.cal-day.today { border-color: var(--accent-line); }
.cal-day.today .cd-num { color: var(--accent-dark); }
.cal-day.sel { background: var(--accent); border-color: var(--accent); }
.cal-day.sel .cd-dow { color: rgba(255,255,255,.85); }
.cal-day.sel .cd-num { color: #fff; }
.cal-day.sel .cd-dot { background: rgba(255,255,255,.28); color: #fff; }
.cal-day.sel:hover { background: var(--accent); }
.cal-day.off .cd-num::after {
  content: ''; display: block; width: 4px; height: 4px; margin: 2px auto 0;
  border-radius: 50%; background: var(--amber);
}
.cal-day.sel.off .cd-num::after { background: #fff; }

/* The day bar under the strip: prev / date / next, with a hidden date input. */
.cal-bar { display: flex; align-items: center; gap: 8px; }
.cal-nav {
  width: 36px; height: 36px; flex: 0 0 36px;
  display: grid; place-content: center;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--card); cursor: pointer; font-size: 20px; color: var(--ink-2);
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.cal-nav:hover { border-color: var(--accent); color: var(--accent-dark); }
.cal-bar-mid { flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; min-width: 0; }
.cal-date { font-family: var(--display); font-weight: 700; font-size: 16px; text-align: center; }
.cal-pick-wrap { position: relative; display: inline-flex; margin: 0; }
.cal-pick-wrap input[type="date"] {
  width: 20px; padding: 0; margin: 0; border: none; background: none;
  color: transparent; cursor: pointer;
}
.cal-pick-wrap::before {
  content: '▾'; position: absolute; left: 3px; top: 50%; transform: translateY(-50%);
  color: var(--ink-3); pointer-events: none; font-size: 12px;
}

/* Day summary — three quiet figures. */
.cal-summary {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
  margin: 14px 0;
}
/* Sits under the day's figures, right-aligned and quiet — it is a thing he
   does occasionally, not the thing he came to the page for. */
.cal-off-row { display: flex; justify-content: flex-end; margin: -4px 0 14px; }
.cs-cell {
  display: flex; flex-direction: column; gap: 3px;
  padding: 12px 14px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r);
}
.cs-k { font-size: 12px; color: var(--ink-3); font-weight: 600; }
.cs-v { font-family: var(--display); font-weight: 700; font-size: 21px; }
.cs-v.accent { color: var(--accent-dark); }

.cal-off-line {
  background: var(--amber-tint); border-color: #EFD9AE; color: var(--amber);
  cursor: default; display: flex; align-items: center; gap: 8px;
}

.cal-empty {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 34px 16px; color: var(--ink-3); text-align: center;
}
.cal-empty .ui-icon { color: var(--line-strong, var(--ink-4)); }

/* The day — a stack of appointment cards, big time on the left, a status
   stripe down the side. No rail, no gaps: just the bookings, clearly. */
.cal-day-list { display: flex; flex-direction: column; gap: 10px; }
.cal-appt {
  display: flex; gap: 14px; align-items: flex-start;
  padding: 13px 15px;
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 4px solid var(--accent);
  border-radius: var(--r);
  animation: riseSm .4s var(--ease) both;
  transition: box-shadow var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.cal-appt.booked:hover { box-shadow: 0 4px 16px rgba(20,24,28,.07); }
.cal-appt.completed { border-left-color: var(--green); }
.cal-appt.pending   { border-left-color: var(--amber); }
.cal-appt.no_show   { border-left-color: #B4534F; }
.cal-appt.cancelled { border-left-color: var(--ink-4); }

.ca-time {
  flex: 0 0 56px;
  display: flex; flex-direction: column; align-items: flex-start;
  padding-top: 1px; line-height: 1;
}
.ca-hr { font-family: var(--display); font-weight: 700; font-size: 21px; color: var(--ink); }
.ca-ap { font-size: 11px; font-weight: 700; letter-spacing: .05em; color: var(--ink-3); margin-top: 3px; }

.ca-body { flex: 1; min-width: 0; }
.ca-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.ca-top strong { font-weight: 600; min-width: 0; }
.ca-body .svc { color: var(--ink-3); font-size: 13px; margin-top: 2px; }
.ca-body .appt-actions { margin-top: 11px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* Done is the everyday action, so it is the one big, filled button —
   capsule-shaped like the rest of the app's prominent single actions. */
.btn.ca-done {
  background: var(--green); color: #fff; border-color: var(--green);
  font-size: 14.5px; font-weight: 600; padding: 10px 20px;
  border-radius: 999px;
}
.btn.ca-done:hover { background: var(--green-deep, #12634a); border-color: var(--green-deep, #12634a); color: #fff; }
/* The tick inside Done.

   Drawn as two borders of an empty box turned 45 degrees, which is the
   usual trick and fine on its own. What broke it: .btn animates `transform`
   on hover and active, and this element is inside a button whose own
   transform the tick then inherits the transition of — so pressing Done set
   the tick spinning and sliding along with the press instead of sitting
   still, and on the tour step that points straight at that button it was
   the thing you could not help watching.

   Wrapped in its own span with the rotation isolated: transition:none stops
   it taking part in the button's animation at all, and the fixed line-height
   keeps it from shifting the label's baseline while the button scales. */
.ca-done-tick {
  display: inline-block;
  width: 6px; height: 11px; margin: -2px 3px 0 0;
  border: solid #fff; border-width: 0 2.5px 2.5px 0;
  transform: rotate(45deg);
  transition: none;
  flex: none;
}
/* The button scales on press; nothing inside it should scale separately. */
.btn.ca-done:active .ca-done-tick,
.btn.ca-done:hover .ca-done-tick { transform: rotate(45deg); }

@media (max-width: 560px) {
  .cal-day { padding: 7px 1px 6px; border-radius: 10px; }
  .cal-day .cd-num { font-size: 15.5px; }
  .cal-day .cd-dow { font-size: 9.5px; }
  .cal-summary { gap: 8px; }
  .cs-cell { padding: 10px 11px; }
  .cs-v { font-size: 18px; }
  .ca-time { flex-basis: 50px; }
  .ca-hr { font-size: 19px; }
  /* Done goes full width on its own line above; the rest share the line
     below it evenly rather than bunching to one side. */
  .ca-body .appt-actions { gap: 8px; }
  .ca-body .appt-actions .btn:not(.ca-done) { flex: 1 1 0; justify-content: center; min-width: 0; }
  .btn.ca-done { flex: 1 1 100%; justify-content: center; order: -1; }
}

/* ============================================================
   Loaders — the barber pole / flag band
   ============================================================ */

.pole {
  height: 5px;
  border-radius: 100px;
  overflow: hidden;
  background-image: repeating-linear-gradient(
    115deg,
    var(--red) 0 16px,
    #fff 16px 24px,
    var(--ink) 24px 40px,
    #fff 40px 48px
  );
  background-size: 64px 100%;
  animation: pole 1.1s linear infinite;
}
.pole.thick { height: 8px; }

/* A nail salon gets its own loader rather than a barber's pole.

   The pole is the one piece of decoration in the app that names the trade
   outright — rotating red and white stripes are a barbershop and nothing
   else, and a client waiting on her gel appointment should not be watching
   one turn. The answer is not to strip it back to a plain grey bar for her
   either; that would read as the nail side being the afterthought. So she
   gets something equally deliberate: a lacquered sweep in the shop's own
   accent, the shine moving across a polished surface. Same height, same
   rhythm, same place in the layout — only the character changes. */
body.trade-nails .pole {
  background-image: linear-gradient(
    100deg,
    var(--accent-line) 0%,
    var(--accent) 26%,
    #F3CBD6 46%,
    #FFFFFF 54%,
    var(--accent) 70%,
    var(--accent-line) 100%
  );
  background-size: 220% 100%;
  background-position: 120% 0;
  animation: lacquer 1.5s var(--ease-soft) infinite;
}
@keyframes lacquer { to { background-position: -120% 0; } }

.loading-wrap { display: grid; place-items: center; gap: 14px; padding: 54px 0; animation: fade var(--t) var(--ease) both; }
.loading-wrap .pole { width: 168px; }

.spinner {
  width: 17px; height: 17px;
  border: 2.5px solid rgba(255,255,255,.35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin .7s linear infinite;
}

.skeleton {
  border-radius: var(--r-sm);
  background: linear-gradient(100deg, var(--cream-2) 30%, #FAFAFC 50%, var(--cream-2) 70%);
  background-size: 220% 100%;
  animation: shimmer 1.4s var(--ease-soft) infinite;
  min-height: 15px;
}

/* ============================================================
   Empty states
   ============================================================ */

.empty {
  display: grid;
  place-items: center;
  gap: 10px;
  padding: 44px 20px;
  text-align: center;
  animation: rise var(--t-slow) var(--ease) both;
}
.empty .mark {
  width: 52px; height: 52px;
  border-radius: 14px;
  background: var(--red) url('/icons/logo.svg') center/cover no-repeat;
  opacity: .2;
  filter: grayscale(.35);
}
/* A shop's own logo stands in for the NextUp mark here too. */
.empty .mark.own {
  background-color: #fff;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  border: 1px solid var(--line);
  opacity: .32;
  filter: none;
}
.empty p { color: var(--ink-3); font-size: 14px; max-width: 34ch; }

/* ============================================================
   Impersonation bar — visible whenever an admin is inside a shop
   ============================================================ */

/* The demo / impersonation bar.

   It is fixed to the very top, which on a notched iPhone means underneath
   the clock and the battery — so the bar rendered lower than it measured,
   and everything below it was pushed down by a number that no longer
   matched. That is why it was sitting on top of the app's own menu bar.

   The safe-area inset goes into the padding rather than into `top`, so the
   bar's background still runs up behind the status bar (a white gap up
   there looks broken) while its content sits below it. Everything that
   offsets itself for this bar uses the same --imp-h below, so the two can
   never drift apart again. */
.imp-bar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 300;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 9px 16px;
  padding-top: calc(9px + env(safe-area-inset-top, 0px));
  background: var(--ink);
  color: #fff;
  font-size: 13.5px;
  font-weight: 500;
  box-shadow: 0 2px 12px -2px rgba(28,26,23,.4);
  animation: riseSm var(--t) var(--ease) both;
}
.imp-bar b { font-weight: 700; }

/* The help button in the demo bar. Round and icon-first, because on a phone
   the bar is already carrying a sentence and a way out — the word "Help"
   drops off below 560px and the question mark says it on its own. */
.demo-help { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
             font-family: inherit; }
.leave-short { display: none; }
.demo-help svg { flex: none; }
@media (max-width: 560px) {
  .demo-help-txt { display: none; }
  .demo-help { padding: 5px 8px; }
  /* The sentence gives way first — the two buttons are what the bar is for.
     A whole clause rather than an ellipsis: "nothing here is …" trailing off
     is worse than not saying the second half at all. */
  .demo-say-more { display: none; }
  .demo-say { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .imp-bar { gap: 8px; padding-left: 12px; padding-right: 12px; }
  /* And the button shortens with it. Three things and a sentence do not fit
     across 390px, and of the four the sentence is the one worth keeping —
     an amber bar with a pulsing dot already says "you are somewhere that is
     not yours", but only the words say nothing in here is real. */
  .leave-long { display: none; }
  .leave-short { display: inline; }
}
.imp-bar .btn { padding: 5px 11px; font-size: 12.5px; background: rgba(255,255,255,.12);
                border-color: rgba(255,255,255,.28); color: #fff; }
.imp-bar .btn:hover { background: #fff; color: var(--ink); border-color: #fff; }
.imp-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--red);
  animation: pulseRingRed 2s var(--ease-soft) infinite;
}
body.impersonating .layout { padding-top: var(--imp-h); }
body.impersonating .sidebar { top: var(--imp-h); height: calc(100dvh - var(--imp-h)); }

/* The demo bar sits where the impersonation bar sits and pushes the app down
   the same way. Amber rather than near-black so a barber can tell at a glance
   that he is in a showroom, not in his own shop. */
body.in-demo .layout { padding-top: var(--imp-h); }
body.in-demo .sidebar { top: var(--imp-h); height: calc(100dvh - var(--imp-h)); }
.demo-bar { background: var(--amber); }
.demo-bar .imp-dot { background: #FBF0DC; }
.demo-bar .btn { text-decoration: none; }

/* ============================================================
   Admin panel
   ============================================================ */

.shop-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 4px;
  border-bottom: 1px solid var(--line-soft);
  animation: riseSm .42s var(--ease) both;
  border-radius: var(--r-sm);
  transition: background var(--t-fast) var(--ease);
}
.shop-row:hover { background: var(--cream-2); }
.shop-row:last-child { border-bottom: none; }
.shop-row.off { opacity: .58; }
.shop-row .avatar {
  width: 40px; height: 40px; flex: 0 0 40px;
  border-radius: 11px;
  background: var(--red) url('/icons/logo.svg') center/cover no-repeat;
  box-shadow: var(--shadow-sm);
}
/* A shop that has uploaded its own logo shows it here instead of the
   NextUp mark, so the admin recognises each shop at a glance. */
.shop-row .avatar.own {
  background-color: #fff;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border: 1px solid var(--line);
}
.shop-row.off .avatar { filter: grayscale(1); }
.shop-row .who { flex: 1; min-width: 0; }
.shop-row .who b { font-size: 15px; font-weight: 600; }
.shop-row .who .sub { color: var(--ink-3); font-size: 12.5px; }
.shop-row .figs { display: flex; gap: 20px; flex-wrap: wrap; }
.shop-row .fig { text-align: right; min-width: 62px; }
.shop-row .fig .n {
  font-family: var(--display); font-weight: 700; font-size: 17px;
  font-variant-numeric: tabular-nums; line-height: 1.2;
}
.shop-row .fig .k {
  font-size: 10px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-4); font-weight: 600;
}
.shop-row .acts { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }

/* ------------------------------------------------------------------
   Team rows.

   The admin's shop rows and the owner's team rows share a shell, but a
   team row is one line about one person and has to hold that shape at
   every width. Three rules do the work:

     · the name column may shrink and truncate. Without min-width:0 a
       flex child refuses to go below its content width, so one barber
       called "Christopher Ramsingh-Boodoosingh" pushed the figures out
       of the row and the button onto its own line — the jumble.
     · the figures are a fixed grid, not a wrapping flex. Three columns
       of the same width, always in the same place, so the eye can read
       down them across several barbers.
     · one action button, given a floor so it never squeezes into two
       lines of text.
   ------------------------------------------------------------------ */
.shop-row.team-row { gap: 16px; align-items: center; }
.shop-row.team-row .who { flex: 1 1 auto; min-width: 0; }
.shop-row.team-row .who-line {
  display: flex; align-items: center; gap: 7px; min-width: 0;
}
.shop-row.team-row .who-line b {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shop-row.team-row .who-line .badge { flex: 0 0 auto; }
.shop-row.team-row .who .sub {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  margin-top: 2px;
}
/* width:auto beats the generic 860px rule below, which stretches .figs and
   .acts to the full row for the admin's shop list. A team row does its own
   stacking and must not inherit that one. */
.shop-row.team-row .figs {
  display: grid;
  grid-template-columns: repeat(3, minmax(64px, 78px));
  gap: 8px;
  flex: 0 0 auto;
  width: auto;
}
.shop-row.team-row .fig { min-width: 0; text-align: right; }
.shop-row.team-row .fig .n {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.shop-row.team-row .acts { flex: 0 0 auto; width: auto; justify-content: flex-end; }
.shop-row.team-row .acts .btn { min-width: 92px; white-space: nowrap; }

/* On a phone the row becomes two lines rather than seven: who they are on
   top, then the figures with the button beside them. Still no wrapping
   inside either half. */
@media (max-width: 720px) {
  .shop-row.team-row { flex-wrap: wrap; row-gap: 10px; }
  .shop-row.team-row .who { flex: 1 1 100%; order: 1; }
  .shop-row.team-row .avatar { order: 0; }
  .shop-row.team-row .figs {
    order: 2;
    flex: 1 1 auto;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .shop-row.team-row .fig { text-align: left; }
  .shop-row.team-row .acts { order: 3; flex: 0 0 auto; width: auto; }
}

/* ------------------------------------------------------------------
   The Manage sheet.

   Every control that used to sit in the row, with room for a sentence
   under each one. An owner should not have to press "Open" to find out
   what Open does, and the sheet is the one place there is space to say
   so. Grouped by intent, with the two irreversible-feeling actions
   pushed to the bottom under their own heading.
   ------------------------------------------------------------------ */
.mg-title { margin-bottom: 2px; }
.mg-sub { margin-bottom: 18px; font-size: 13px; }

.mg-group { margin-bottom: 20px; }
.mg-group > h3 {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--ink-4);
  font-weight: 700;
  margin: 0 0 8px;
}
.mg-group.danger-group { border-top: 1px solid var(--line-soft); padding-top: 16px; }

.mg-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 11px 13px;
  margin-bottom: 6px;
  background: var(--cream-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-sm);
  cursor: pointer;
  font: inherit;
  color: inherit;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.mg-item:hover { background: #fff; border-color: var(--line); }
.mg-item:last-child { margin-bottom: 0; }
.mg-item .mg-label { display: block; font-weight: 600; font-size: 14.5px; }
.mg-item .mg-hint {
  display: block;
  margin-top: 2px;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--ink-3);
  font-weight: 400;
}
.mg-item.danger .mg-label { color: var(--red); }
.mg-item.warn .mg-label { color: var(--ink-2); }

.mg-check {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 13px;
  margin-bottom: 6px;
  background: var(--cream-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-sm);
}
.mg-check input[type="checkbox"] { margin-top: 2px; flex: 0 0 auto; }
.mg-check b { display: block; font-size: 14.5px; font-weight: 600; }
.mg-check em {
  display: block;
  margin-top: 2px;
  font-style: normal;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--ink-3);
}

/* The delete confirmation's helper line. Green once what is typed will be
   accepted, so the box answers before the button has to. */
.del-note { font-size: 12.5px; margin-top: 6px; }
.del-note.ok { color: var(--green, #2e7d4f); }
.del-phrase { white-space: pre-wrap; }
#del-confirm.matched { border-color: var(--green, #2e7d4f); }
.linkish {
  background: none; border: 0; padding: 0;
  font: inherit; color: var(--ink-2);
  text-decoration: underline; cursor: pointer;
}

/* ---------------------------------------------------------------------
   The guided tutorial in the demo.

   A card pinned low on the screen and a ring around whatever he should
   touch next. Deliberately not a dark sheet with a hole in it: that means
   covering the app in something he then has to click through, and on a
   phone it fights the drawer and the modals both. An outline says "this
   one" just as clearly and blocks nothing.
   --------------------------------------------------------------------- */
/* Centred with left/right/auto margins rather than a transform.

   It used to be left:50% and translateX(-50%), and it has never once been
   centred, because the entrance animation ends on `transform: none` with
   fill-mode both — which cancels the very transform doing the centring.
   The card has always sat with its left edge on the middle of the screen,
   half of it off the side of a phone. Nobody caught it because the tour
   itself never ran on a first visit, and a second visit is not how anyone
   sees this. Margins cannot be overwritten by a keyframe, so this stays
   put whatever the animation does. */
/* The tour card, anchored to the arrow rather than the screen.

   It used to be a bar pinned to the bottom of the viewport, which put it
   in the one place the app also keeps its primary buttons — so the step
   that says "tap Approve" printed itself over Approve. That was patched by
   flipping it to the top when they collided, and the flipping was its own
   kind of jumpy.

   The honest fix is to stop treating the words and the pointer as two
   things. The arrow knows where the control is; the card now hangs off the
   arrow, on the same side, so the instruction and the thing it names are
   one object. There is no collision to resolve because the card is placed
   relative to the control in the first place.

   Positioned by paint() in JS — CSS cannot know where the control is. */
#tour {
  position: fixed;
  z-index: 1400;
  /* SIZED BY THE SENTENCE, NOT BY A CONSTANT.

     Every card was 300px wide whatever it said, so a three-word step like
     "Tap Approve" printed a short line across a box built for a paragraph
     and sat there mostly empty. width:max-content lets the box shrink to
     the words; min() caps it so a long narrative still wraps at a readable
     measure rather than running the width of a tablet, and min-width stops
     a very short line collapsing to something that looks like an error.

     The paragraph is what has the length, so the cap lives here on the card
     and the text wraps inside it — no per-step measuring, no JS. */
  width: max-content;
  max-width: min(300px, calc(100vw - 34px));
  min-width: 190px;
  background: var(--msg);
  color: var(--msg-ink);
  border-radius: 18px;
  padding: 13px 15px 12px;
  box-shadow: 0 4px 10px rgba(16,16,18,.06), 0 18px 44px -12px rgba(18,40,61,.32);
  animation: tourPop .34s cubic-bezier(.2, .9, .25, 1.02) both;
}

/* HOW THE CARD ARRIVES.

   It used to fade up from 96% over 170ms, the same way for every step.
   Technically an animation; in practice the card simply blinked into
   existence, and because nothing about the movement said where it had come
   from, a step change read as the page having redrawn rather than as the
   walkthrough having moved on. On a phone, where the card is a third of the
   screen, that blink is the single most abrupt thing in the demo.

   Two entrances now, because there are two kinds of step.

   A card hanging off an arrow slides a few pixels out of the control it is
   naming — the direction class paint() already puts on the arrow is mirrored
   onto the card, so the words appear to come out of the pointer. That is the
   thing the words are about, so that is where they should come from.

   A centred announcement has no control, so it rises instead: up a little,
   and a touch of scale, over a longer beat. It is holding the page, and
   something that stops everything else should take a moment to land rather
   than snap.

   Both are on an ease-out with a whisker of overshoot. Not a bounce — a
   bounce on a card carrying instructions is decoration arguing with the
   reading — just enough that it settles rather than stops dead. */
@keyframes tourPop {
  from { opacity: 0; transform: translateY(6px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes tourFromTop    { from { opacity: 0; transform: translateY(-14px) scale(.98); } to { opacity: 1; transform: none; } }
@keyframes tourFromBottom { from { opacity: 0; transform: translateY(14px)  scale(.98); } to { opacity: 1; transform: none; } }
@keyframes tourFromLeft   { from { opacity: 0; transform: translateX(-14px) scale(.98); } to { opacity: 1; transform: none; } }
@keyframes tourFromRight  { from { opacity: 0; transform: translateX(14px)  scale(.98); } to { opacity: 1; transform: none; } }
#tour.from-top    { animation-name: tourFromTop; }
#tour.from-bottom { animation-name: tourFromBottom; }
#tour.from-left   { animation-name: tourFromLeft; }
#tour.from-right  { animation-name: tourFromRight; }

/* The centred announcement, which is doing more work than the others. */
@keyframes tourRise {
  from { opacity: 0; transform: translateY(20px) scale(.94); }
  60%  { opacity: 1; }
  to   { opacity: 1; transform: none; }
}
#tour.wide { animation: tourRise .42s cubic-bezier(.16, .9, .22, 1.04) both; }

@media (prefers-reduced-motion: reduce) {
  #tour, #tour.wide { animation: none; }
}

/* Put the note away for this step.

   A small × in the corner. The arrow stays where it is, so he still knows
   which control the step means; only the words go. The next step brings
   them back, which is why this is a class on the card rather than anything
   persisted. */
.tour-hide {
  position: absolute; top: 6px; right: 6px;
  width: 30px; height: 30px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: 50%;
  background: transparent; color: var(--msg-ink-2);
  cursor: pointer; opacity: .6;
}
.tour-hide:hover { opacity: 1; background: rgba(16,16,18,.06); }
.tour-hide svg { width: 14px; height: 14px; }
#tour.tucked { display: none; }
/* Room for the × so it never sits on the step counter. */
#tour { padding-right: 40px; }

/* Centred announcements — the storefront and the map. No control to sit
   beside, so it takes the middle of the screen and room to be read. */
#tour.wide {
  /* Same reasoning as the arrow-anchored card: as wide as it needs to be,
     capped at a readable measure. A centred announcement carries the
     longest text in the tour, so the cap is what it usually gets — but the
     short ones no longer print two words across a 380px box. */
  width: max-content;
  max-width: min(380px, calc(100vw - 38px));
  min-width: 240px;
  padding: 22px 44px 22px 22px;
  border-radius: 22px;
}
/* An announcement holds the page: nothing behind it moves while it is up,
   the same rule the booking page's bubbles follow. Only for the centred
   ones — a step pointing at a control must leave the page scrollable, or
   he cannot reach the control it is pointing at. */
body.tour-holding { overflow: hidden; }
.tour-scrim {
  position: fixed; inset: 0; z-index: 1398;
  background: rgba(16, 16, 18, .34);
  -webkit-backdrop-filter: blur(1.5px); backdrop-filter: blur(1.5px);
  animation: fadeIn .16s var(--ease) both;
}
#tour.wide b { font-size: 18px; margin-bottom: 5px; }
#tour.wide p { font-size: 15px; line-height: 1.5; }

/* HER FACE, AND HER WORDS ARRIVING.
   -----------------------------------------------------------------
   The card is a message from the rep, so it is built like one: a round
   photograph at the top-left, the text beside it, and the text typing
   itself out. See TOUR.faceHtml() and TOUR.type() for the other half.

   The circle overlaps the card's top-left corner rather than sitting inside
   it. Inside, it ate a line of width on a 300px bubble and pushed the words
   into a narrow column; hanging off the corner costs nothing and reads the
   way an avatar beside a chat bubble does. */
#tour { padding-left: 15px; }
.tour-face {
  position: absolute;
  top: -13px; left: -11px;
  width: 38px; height: 38px;
  border-radius: 50%;
  overflow: hidden;
  display: grid; place-items: center;
  background: var(--accent-soft, #dce9ff);
  color: var(--accent, #0A7CFF);
  box-shadow: 0 0 0 2.5px var(--msg, #fff), 0 3px 9px -2px rgba(18,40,61,.34);
  font-weight: 700; font-size: 16px;
  font-family: var(--display);
  user-select: none;
}
.tour-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Room for the circle, so it never sits on the step counter. */
/* The step counter is gone (see the note where the card is built), so the
   heading is now the first line and has to carry the indent that kept the
   rep's photograph off it. */
#tour b:first-of-type { padding-left: 26px; }
#tour.wide b:first-of-type { padding-left: 30px; }

/* The three dots, while the sentence is on its way.

   Drawn on the paragraph itself with ::after rather than as its own
   element, so there is nothing to insert and remove — the .typing class
   going on and off is the whole mechanism, and a sentence that finishes
   mid-scroll cannot leave an orphan behind.

   min-height holds one line open so the dots have somewhere to sit; the
   paragraph is already sized by the finished sentence (TOUR.type writes the
   full text into a data attribute and the bubble is laid out from it), so
   the card does not grow line by line under the arrow while he reads. */
#tour .tour-body { min-height: 1.45em; }
#tour.typing .tour-body::after {
  content: '···';
  letter-spacing: 2px;
  color: var(--msg-ink-2);
  animation: tourDots 1s ease-in-out infinite;
}
@keyframes tourDots { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  #tour.typing .tour-body::after { content: none; }
}

#tour b { display: block; font-size: 16px; margin-bottom: 3px; font-family: var(--display); }
#tour p { margin: 0; font-size: 14px; line-height: 1.45; color: var(--msg-ink-2); }
.tour-step {
  font-size: 11px; letter-spacing: .09em; text-transform: uppercase;
  font-weight: 700; color: var(--msg-ink-2); margin-bottom: 5px;
}
.tour-acts { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 11px; }
.tour-skip {
  background: none; border: none; padding: 4px 0; cursor: pointer;
  font-family: inherit; font-size: 13px; color: var(--msg-ink-2);
  text-decoration: underline; text-underline-offset: 3px;
}
.tour-skip:hover { color: var(--msg-ink); }
/* Ink on the bubble, not white: on a light ground a white button is a
   hole. This is the same fill as every other primary action in the app. */
.tour-go {
  margin-left: auto;
  font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  border: none; border-radius: 100px; padding: 8px 15px;
  background: var(--ink); color: #fff;
}
.tour-go:hover { background: #26262C; }

/* And the panel gives way too.

   Both were centred, one on top of the other, so the card covered the
   heading of the thing it was talking about. While the tour is running the
   panel sits at the foot of the screen, which leaves the upper half of the
   phone free for the card and its arrow to find the control.

   The card itself is no longer pinned anywhere — paint() places it against
   whatever it is pointing at, and the old "flip it to the top when a modal
   is open" rules had to go with the pinning: they set `top` and `bottom` in
   CSS, which fought the JS every frame and produced the jumping. What is
   left here is only the panel's own behaviour. */
body.touring .modal-backdrop {
  align-items: end;
  /* Room underneath for the toast, which fires at the same moment the
     panel opens and was landing across its buttons. */
  padding-bottom: 76px;
}
body.touring .modal { max-height: calc(100dvh - 300px); }

/* The pointer.

   This was an outline drawn around the control — a 2px ring with a dark
   halo. It worked, but it read as "this thing is selected" rather than
   "press this thing", and around a full-width button on a phone it was a
   rectangle enclosing most of the screen, which points at nothing.

   An arrow is unambiguous in a way a box is not: it has a direction, and
   the direction is at the control. So the ring became a small dark
   lozenge with a triangular tail, parked just outside whichever edge of
   the target has room, tail pointing in. It bobs a couple of pixels along
   its own axis — movement toward the thing it is naming.

   The soft halo stays as a separate element (#tour-halo) because losing it
   entirely made the target hard to find on a busy screen: the arrow says
   which way, the halo says exactly which control. It is much quieter than
   the old ring — no border, just a lift out of the page. */
/* No halo. The arrow is the pointer.

   There was a tinted, pulsing box around the control underneath the arrow.
   It was the last survivor of the original outline, kept on the theory
   that the arrow says "over there" and the box says "this exact one" — but
   two markers on one control is one more than the job needs, and a shaded
   rectangle that breathes is the part that read as clutter.

   The element is still created and positioned by paint(), because the
   card and arrow are placed relative to its measurements; it simply does
   not draw anything. Cheaper than unpicking it from the geometry. */
#tour-ring { display: none !important; }

#tour-arrow {
  position: fixed;
  z-index: 1401;
  pointer-events: none;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: #0A7CFF;
  color: #fff;
  box-shadow: 0 5px 14px -4px rgba(10, 124, 255, .7);
}
#tour-arrow svg { width: 19px; height: 19px; display: block; }
/* The tail: a small triangle on whichever side faces the control. */
#tour-arrow::after {
  content: '';
  position: absolute;
  border: 7px solid transparent;
}
#tour-arrow.from-top::after    { top: 100%; border-top-color: #0A7CFF; border-bottom-width: 0; }
#tour-arrow.from-bottom::after { bottom: 100%; border-bottom-color: #0A7CFF; border-top-width: 0; }
#tour-arrow.from-left::after   { left: 100%; border-left-color: #0A7CFF; border-right-width: 0; }
#tour-arrow.from-right::after  { right: 100%; border-right-color: #0A7CFF; border-left-width: 0; }

/* THE ARROW: ALWAYS VISIBLE, ALWAYS NUDGING TOWARD THE CONTROL.
   -----------------------------------------------------------------
   Two bugs lived here, and the first one made the arrow disappear.

   The drop-in used ONE keyframe per axis and got the other direction by
   adding `reverse`. But `reverse` plays the keyframes to→from, and with
   fill mode `both` the element then HOLDS the `from` state once it
   finishes — and `from` is `opacity: 0`. So on every from-bottom and
   from-right step the arrow faded itself out and stayed out. It was not
   flickering and it was not an iPhone quirk: it was landing invisible,
   every time, on more than half the steps in the tour. (The bob that
   follows only animates transform, so nothing ever brought the opacity
   back.)

   The second was the bounce. `reverse` on a symmetric 0→4→0 keyframe is
   the same animation, so the bob went the same way whichever side the
   arrow was on — sometimes toward the control it was naming and sometimes
   away from it.

   Both are fixed the same way: four explicit keyframes, one per side, and
   no `reverse` anywhere near a filled animation. Each arrow now drops in
   from the far side of its target and then nudges TOWARD it, so the
   movement itself points where the thumb has to go.

   Opacity is also pinned at the end of every drop and never touched by the
   bob, so nothing can leave the arrow transparent again. */

/* The nudge: always toward the control, which is the side the tail is on. */
@keyframes tourBobDown  { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(5px); } }
@keyframes tourBobUp    { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes tourBobRight { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(5px); } }
@keyframes tourBobLeft  { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(-5px); } }

/* The arrival: in from the far side, settling onto its mark. Every one of
   these ENDS at opacity 1 and no transform, so the resting state is the
   same however it got there. */
/* NOT ONE OF THESE TOUCHES OPACITY.

   The arrow going missing has been chased three times now, and every round
   the cause was an animation leaving it transparent. Each fix closed one
   route and left the others open.

   So the door is shut instead: no keyframe here mentions opacity, and
   nothing else in this stylesheet animates the arrow. An animation can now
   fail to start, be interrupted, get thrown away by a repaint, or be
   composited by a browser that disagrees with this one — and the worst it
   can produce is an arrow briefly in the wrong PLACE. It cannot produce an
   arrow that is not there.

   That is the right trade for this element. A scale that pops is a nicety;
   being visible is the entire job. */
@keyframes tourDropDown  { from { transform: translateY(-16px) scale(.6); } 70% { transform: translateY(3px) scale(1.06); } to { transform: none; } }
@keyframes tourDropUp    { from { transform: translateY(16px)  scale(.6); } 70% { transform: translateY(-3px) scale(1.06); } to { transform: none; } }
@keyframes tourDropRight { from { transform: translateX(-16px) scale(.6); } 70% { transform: translateX(3px) scale(1.06); } to { transform: none; } }
@keyframes tourDropLeft  { from { transform: translateX(16px)  scale(.6); } 70% { transform: translateX(-3px) scale(1.06); } to { transform: none; } }

/* Belt and braces: whatever any animation is doing, the arrow itself is
   opaque. An animation that fails to start, is interrupted mid-flight, or
   is thrown away by a repaint can no longer leave it invisible — the worst
   case is now an arrow that sits still, which still tells him where to
   press. */
/* Said outright, and with !important, because this is the property the bug
   keeps coming back through. Nothing gets to make this element transparent:
   not a later stylesheet, not a half-applied animation, not an inline style
   left behind by a script. */
#tour-arrow { opacity: 1 !important; }

@media (prefers-reduced-motion: no-preference) {
  #tour-arrow.from-top {
    animation: tourDropDown .3s cubic-bezier(.2,.9,.25,1.1) both,
               tourBobDown 1.5s ease-in-out .3s infinite;
  }
  #tour-arrow.from-bottom {
    animation: tourDropUp .3s cubic-bezier(.2,.9,.25,1.1) both,
               tourBobUp 1.5s ease-in-out .3s infinite;
  }
  #tour-arrow.from-left {
    animation: tourDropRight .3s cubic-bezier(.2,.9,.25,1.1) both,
               tourBobRight 1.5s ease-in-out .3s infinite;
  }
  #tour-arrow.from-right {
    animation: tourDropLeft .3s cubic-bezier(.2,.9,.25,1.1) both,
               tourBobLeft 1.5s ease-in-out .3s infinite;
  }
}
/* The ring is repositioned on every frame the page scrolls, so a
   transition on top/left is a ring that is permanently 180ms behind the
   button — it lags, overshoots, and slides about the screen while the
   barber is trying to read what it is pointing at. Nailed down by default;
   the glide is put on for a moment when the tour moves to the next
   control, which is the only time it is a movement rather than a lag. */
/* No travel between steps.

   The halo and the arrow used to slide from the old control to the new one
   over 220ms. It looked considered and read as broken: on a step change the
   page is usually scrolling too, so the marker was sliding across a moving
   background and landing somewhere it had already left. It also meant that
   for a fifth of a second the thing pointing at the button was pointing at
   the space between two buttons.

   It now appears where it has to be, full stop. The only movement left is
   the bob on the arrow itself, which is about the control it is already
   sitting on rather than about travel. */
#tour-ring, #tour-arrow { transition: none; }
/* "Have a look" on the map step — a plain link out, beside the button that
   moves the tour on, so opening it in another tab does not end the tour. */
.tour-link {
  font-size: 13px; font-weight: 600; text-decoration: none;
  color: var(--msg-ink); border: 1px solid var(--msg-line);
  border-radius: 100px; padding: 7px 14px; background: rgba(255,255,255,.55);
}
.tour-link:hover { background: #fff; }
@media (prefers-reduced-motion: reduce) {
  #tour-ring, #tour-arrow { animation: none; transition: none; }
  #tour { animation: none; }
}

/* Picking the colour behind everything. Swatches rather than a dropdown of
   colour names: nobody knows what "clay" looks like until they see it, and
   the whole page repaints on tap anyway. */
.bg-picker {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line-soft);
}
.bg-picker .muted { font-size: 13px; }
.bg-swatches { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 8px; }
.bg-swatch {
  width: 38px; height: 38px;
  border-radius: 10px;
  border: 1px solid var(--line);
  padding: 0;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: transform var(--t-fast) var(--ease);
}
.bg-swatch:hover { transform: translateY(-2px); }
.bg-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The custom one wears a spectrum so it reads as "anything else" rather
   than as a seventh preset that happens to be grey. */
.bg-custom {
  position: relative;
  overflow: hidden;
  background: conic-gradient(#F5C8C8, #F6E7C0, #CFEBD5, #C9E2F2, #DCD3F0, #F5C8C8);
}
.bg-custom input[type="color"] {
  position: absolute; inset: -4px;
  width: calc(100% + 8px); height: calc(100% + 8px);
  border: none; padding: 0; opacity: 0; cursor: pointer;
}

/* What a barber shares with the shop. Sits between his figures and the
   buttons because it explains the figures — a dash under Revenue makes
   sense the moment you see the unticked box beside it. Deliberately not
   styled as a button: these are settings that hold, not actions. */
.shop-row .share-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 172px;
}
.shop-row .share-row .check-row {
  margin: 0;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-3);
  gap: 7px;
}
.shop-row .share-row .check-row input[type="checkbox"] {
  width: 15px; height: 15px; flex: 0 0 15px;
}
@media (max-width: 720px) {
  .shop-row .share-row { min-width: 0; width: 100%; }
}

.pw-box {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
  background: var(--cream-2);
  border: 1.5px dashed var(--line);
  border-radius: var(--r);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: .02em;
  word-break: break-all;
}

.log-row {
  display: flex; gap: 12px; align-items: baseline;
  padding: 8px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 13.5px;
}
.log-row:last-child { border-bottom: none; }
.log-row .when { color: var(--ink-4); font-size: 12px; min-width: 118px; flex: 0 0 118px; }
.log-row .what { flex: 1; }

@media (max-width: 860px) {
  .shop-row { flex-wrap: wrap; }
  .shop-row .figs { width: 100%; justify-content: flex-start; gap: 16px; }
  .shop-row .fig { text-align: left; }
  .shop-row .acts { width: 100%; justify-content: flex-start; }
  .log-row { flex-direction: column; gap: 2px; }
  .log-row .when { flex-basis: auto; }
}

/* ============================================================
   Share-link card (barber's booking link)
   ============================================================ */

.link-box {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  background: var(--cream-2);
  border: 1.5px dashed var(--line);
  border-radius: var(--r);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  word-break: break-all;
  transition: border-color var(--t) var(--ease), background var(--t) var(--ease);
}
.link-box:hover { border-color: var(--accent-line); background: var(--accent-tint); }

/* ============================================================
   Booking link card — the dashboard's most useful square inch
   ============================================================ */

/* The QR panel. All that is left of the old booking-link card — it now
   opens from Settings > Online booking instead of sitting on Today. */
.blink-qr {
  display: grid;
  justify-items: center;
  gap: 10px;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--line-soft);
  animation: riseSm var(--t) var(--ease) both;
}
.blink-qr img {
  width: 200px;
  height: 200px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: #fff;
  padding: 8px;
}
.blink-qr p { text-align: center; max-width: 34ch; }

/* Add-ons under the service list.

   Deliberately quieter than the service cards above them: the main service
   is the decision, these are afterthoughts. Selected ones fill with the
   accent so the running total in the summary makes sense at a glance. */
.addons {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  animation: riseSm .35s var(--ease) both;
}
.addons b { font-size: 15px; }
.addons .muted { font-size: 13px; margin-bottom: 8px; }
.addon-list { display: flex; flex-direction: column; gap: 7px; }
.addon {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 12px;
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  color: var(--ink);
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}
.addon:hover { border-color: var(--ink-4); }
.addon:active { transform: scale(.99); }
.addon .a-n { display: flex; flex-direction: column; flex: 1; min-width: 0; line-height: 1.3; }
.addon .a-n b { font-size: 14.5px; font-weight: 600; }
.addon .a-n span { font-size: 12.5px; color: var(--ink-3); }
.addon .a-p {
  font-weight: 600;
  font-size: 14px;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.addon.on {
  border-color: var(--accent);
  background: var(--accent-tint);
}
.addon.on .a-n b, .addon.on .a-p { color: var(--accent-dark); }
.addon.on .svc-chip { background: var(--accent); color: #fff; border-color: var(--accent); }
/* A tick, so a selected add-on reads as chosen and not merely highlighted. */
.addon.on .a-p::after {
  content: '';
  display: inline-block;
  width: 5px; height: 9px;
  margin-left: 8px;
  border: solid var(--accent-dark);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  vertical-align: 1px;
}

/* "Tell me if something opens" — offered on a full day.

   Sits inside the empty state rather than after it, so the page reads as
   one thought: nothing today, but here is what to do about it. */
.wl-box {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-top: 6px;
  padding: 16px;
  background: var(--accent-tint);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  animation: riseSm .4s var(--ease) both;
}
.wl-box b { color: var(--accent-dark); font-size: 15px; }
.wl-box .muted { font-size: 13.5px; line-height: 1.5; }
.wl-box form { margin-top: 10px; }
.wl-box label { margin-bottom: 10px; }
.wl-box .btn { margin-top: 2px; }
/* "You are on the list" is a confirmation the client reads once, not a
   button he acts on — so it stays inside the neutral palette rather than
   borrowing the green that now belongs to WhatsApp and to approved work. */
.wl-box.done {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  align-items: flex-start;
}
.wl-box.done b { color: var(--accent-dark); }
.wl-box.done span { font-size: 13.5px; color: var(--ink-2); }


/* ============================================================
   Service icons
   ============================================================ */

.svc-icon { flex: 0 0 auto; vertical-align: -.18em; }

.svc-chip {
  display: inline-grid;
  place-items: center;
  width: 38px;
  height: 38px;
  flex: 0 0 38px;
  border-radius: 11px;
  background: var(--accent-tint);
  color: var(--accent-deep);
  border: 1px solid var(--accent-line);
}
.svc-chip.lg { width: 44px; height: 44px; flex-basis: 44px; border-radius: 13px; }
/* On a selected card the chip inverts so it stays legible on the teal fill. */
.svc-card.sel .svc-chip { background: var(--accent); color: #fff; border-color: var(--accent); }

/* The picker in the service editor */
.icon-pick {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
  gap: 8px;
  margin-top: 8px;
}
.icon-pick button {
  display: grid;
  place-items: center;
  gap: 3px;
  padding: 9px 4px 7px;
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  cursor: pointer;
  color: var(--ink-2);
  font-family: inherit;
  font-size: 9.5px;
  line-height: 1.2;
  transition: all var(--t-fast) var(--ease);
}
.icon-pick button:hover { border-color: var(--ink-4); transform: translateY(-1px); }
.icon-pick button.sel {
  border-color: var(--accent);
  background: var(--accent-tint);
  color: var(--accent-dark);
  font-weight: 600;
}
.icon-pick .auto-note { grid-column: 1 / -1; }

/* ============================================================
   First-login setup wizard
   ============================================================ */

.w-rail { display: flex; gap: 6px; margin-bottom: 16px; }
.w-node {
  flex: 1;
  height: 4px;
  border-radius: 100px;
  background: var(--line);
  transition: background var(--t) var(--ease);
}
.w-node.done, .w-node.now { background: var(--accent); }
.w-node.now { animation: growBar .4s var(--ease) both; transform-origin: left; }

/* Day picker — seven targets that must stay tappable on a phone */
.day-pick {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 5px;
}
.day-pick button {
  padding: 10px 2px;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-2);
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: all var(--t-fast) var(--ease);
}
.day-pick button:hover { border-color: var(--ink-4); }
.day-pick button.sel {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* Service rows inside the wizard. Each box carries its own small label —
   three bare inputs side by side are a guessing game, and a guess here
   becomes a wrong price on a public booking page. */
.w-svc-list { display: grid; gap: 14px; }
.w-svc {
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr) 96px 88px;
  gap: 8px;
  align-items: end;
}
.w-svc .svc-chip { align-self: end; margin-bottom: 5px; }
.w-svc-field { display: block; min-width: 0; }
.w-svc-field label {
  display: block;
  margin-bottom: 4px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-4);
}
.w-svc input { margin-top: 0; width: 100%; }

/* ------------------------------------------------------------------
   The booking page, shown back to him at the end of setup.

   A scaled-down arrangement of the real page's parts rather than a
   picture of it, because the parts are drawn from what the server will
   actually serve — the same call the booking page makes. It has to look
   like the thing it is standing in for at a glance and be legible at a
   third of the size, so the type steps down and the photographs become a
   four-across strip. Anything more faithful than this would be a second
   booking page to keep in step with the first.
   ------------------------------------------------------------------ */
.w-pv {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.w-pv-top { display: flex; align-items: center; gap: 11px; padding: 14px 14px 12px; }
.w-pv-logo {
  flex: none;
  width: 46px; height: 46px;
  border-radius: 13px;
  overflow: hidden;
  display: grid; place-items: center;
  background: var(--card);
  border: 1px solid var(--line);
  font-family: var(--display);
  font-weight: 700; font-size: 19px; color: var(--ink-3);
}
.w-pv-logo img { width: 100%; height: 100%; object-fit: contain; }
.w-pv-id { min-width: 0; }
.w-pv-id b {
  display: block; font-family: var(--display);
  font-size: 16px; font-weight: 600; letter-spacing: -.01em;
}
.w-pv-id small { display: block; font-size: 12.5px; color: var(--ink-3); line-height: 1.4; }
.w-pv-shots { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3px; padding: 0 3px; }
.w-pv-shots img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; border-radius: 6px; display: block; }
.w-pv-svcs { background: var(--card); margin: 10px 3px 3px; border-radius: var(--r); padding: 4px 12px; }
.w-pv-svc {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 13.5px;
}
.w-pv-svc:last-child { border-bottom: none; }
.w-pv-svc span { min-width: 0; font-weight: var(--w-name); }
.w-pv-svc em { display: block; font-style: normal; font-size: 11.5px; color: var(--ink-3); font-weight: var(--w-read); }
.w-pv-svc b { flex: none; font-family: var(--display); font-size: 13.5px; }
.w-pv-more { padding: 8px 0 10px; font-size: 12.5px; color: var(--ink-3); }
.w-pv-foot {
  display: flex; flex-direction: column; gap: 2px;
  padding: 10px 14px 13px;
  font-size: 12px; color: var(--ink-3);
}
/* First paragraph only. The About box takes as much as he wants to write
   and this panel is three inches tall — a preview that scrolls is a
   preview nobody reads to the end of. */
.w-pv-about { color: var(--ink-2); margin-bottom: 4px; }

@media (max-width: 560px) {
  /* Stacked on a phone: chip and name on the top line, price and minutes
     side by side under it, each still labelled.

     That was the intention before, and the rule did not achieve it. It
     placed the fields with :nth-of-type(2) and (3), which counts elements of
     the same TYPE among their siblings — and the icon chip is a <span> just
     like the three fields are. So (2) was the name and (3) was the price,
     one step off from what the author meant, and only two of the three were
     placed at all. The name and the price ended up fighting over the same
     cell: overlapping labels that read "SERVICEPRICE", the name input
     crushed to a few pixels, and Minutes stranded on a line of its own.

     Named areas say the layout outright instead of counting siblings, and
     the fields are picked out by their own classes, so nothing here can go
     off by one again if the row gains another element. */
  .w-svc {
    grid-template-columns: 32px minmax(0, 1fr) 96px;
    grid-template-areas:
      "chip name  name"
      ".    price minutes";
    gap: 10px;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--line-soft);
  }
  .w-svc:last-child { border-bottom: none; padding-bottom: 0; }
  .w-svc .svc-chip {
    grid-area: chip;
    width: 32px; height: 32px; flex-basis: 32px; border-radius: 9px;
  }
  .w-svc .w-f-name  { grid-area: name; }
  .w-svc .w-f-price { grid-area: price; }
  .w-svc .w-f-dur   { grid-area: minutes; }
  .day-pick button { font-size: 11.5px; padding: 10px 1px; }
}

/* A locked modal shows it means it — no dismiss affordance on the backdrop. */
.modal-backdrop.locked { cursor: default; }

/* A stacked set of full-width choices — an action sheet, phone-friendly. */
.sheet { display: grid; gap: 8px; }
.sheet .btn { justify-content: center; padding: 13px; }

/* 12-hour time picker: hour · minute · AM/PM, one row. */
/* ON ITS OWN LINE, LIKE EVERY OTHER FIELD.

   This was inline-flex, which put the three dropdowns on the SAME line as
   the label text. Every other control on the settings screen sits on the
   line below its label, so the time rows were the only ones indented — and
   worse, indented by different amounts: the pickers began wherever the
   label text happened to end, so "Opening time" and "Closing time" started
   their dropdowns at different x positions and the two rows visibly failed
   to line up with each other or with anything above them.

   flex rather than inline-flex puts the row on its own line and back at the
   field's left edge, which is all it needed to agree with the rest of the
   form. */
.time-pick {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
}
/* The three dropdowns have to be the same HEIGHT as every other control on
   the form, or the two time rows sit shorter than the fields above and
   below them and the whole column loses its rhythm. Only the horizontal
   padding differs — the extra room on the right is for the chevron. The
   vertical figure is the one from the base input rule and has to move with
   it; the narrow-screen override below does the same against its own. */
.time-pick select {
  width: auto;
  margin-top: 0;
  padding: 10px 26px 10px 12px;
  font-variant-numeric: tabular-nums;
}
.time-pick .tf-ap { padding-right: 28px; }
.time-pick .tf-colon { color: var(--ink-3); font-weight: 700; }
@media (max-width: 400px) {
  .time-pick { gap: 4px; }
  /* 12px vertical, to match `input, select, textarea` on this width. It was
     10px, which made every time dropdown 5px shorter than the fields around
     it — the visible part of the settings form being "unequally spaced". */
  .time-pick select { padding: 12px 22px 12px 9px; }
}

/* ============================================================
   Quick start guide
   ============================================================ */

.qs { text-align: center; }
.qs-icon {
  width: 62px;
  height: 62px;
  margin: 4px auto 16px;
  border-radius: 18px;
  display: grid;
  place-items: center;
  background: var(--accent-tint);
  color: var(--accent-deep);
  border: 1px solid var(--accent-line);
  animation: popIn .45s var(--ease) both;
}
.qs h2 { font-size: 20px; margin-bottom: 10px; }
.qs-body {
  color: var(--ink-2);
  font-size: 15px;
  line-height: 1.6;
  max-width: 40ch;
  margin: 0 auto;
}
.qs-tip {
  margin: 14px auto 0;
  padding: 10px 14px;
  max-width: 40ch;
  background: var(--cream-2);
  border-radius: var(--r);
  color: var(--ink-3);
  font-size: 13.5px;
  line-height: 1.5;
}
.qs .modal-actions { align-items: center; }
.qs-count {
  margin-right: auto;
  color: var(--ink-4);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}

/* Two ways to send the same link, stacked and full width so neither is the
   one you hit by accident reaching for the other. The shop's own language
   is filled in and the other is outlined: nine sends in ten are the first
   one, and two identical green bars make him read both every time. */
.share-langs { display: grid; gap: 10px; }
.share-langs .btn { width: 100%; justify-content: center; }
.share-langs .btn.wa:not(.primary) {
  background: transparent;
  border-color: #BFE3D6;
  color: #157F41;
}
.share-langs .btn.wa:not(.primary) .ui-icon { color: #157F41; }
.share-langs .btn.wa:not(.primary):hover {
  background: var(--green-tint);
  border-color: #1FA855;
  color: #157F41;
}

/* The way out, set well apart from Next so it is never the tap he meant to
   make. Quiet, but a real control — not a cross in a corner nobody finds. */
.qs-later {
  display: block;
  margin: 16px auto 0;
  padding: 6px 4px;
  background: none;
  border: none;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-4);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}
.qs-later:hover { color: var(--ink-2); }

@media (max-width: 520px) {
  .qs h2 { font-size: 18.5px; }
  .qs-body { font-size: 14.5px; }
  .qs .modal-actions { flex-wrap: wrap; }
  .qs .modal-actions .btn { flex: 1 1 auto; }
  .qs-count { width: 100%; margin: 0 0 8px; text-align: left; }
}

/* ============================================================
   Reports — period picker and the money summary
   ============================================================ */

.period-pick {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
}
.period-custom {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-left: auto;
  flex-wrap: wrap;
}
.period-custom input[type="date"] {
  width: auto;
  margin-top: 0;
  padding: 7px 10px;
  font-size: 13.5px;
}

/* The money-in-money-out block. Deliberately plain: a barber reading his
   figures should see numbers, not decoration. */
.money-summary .ms-row,
.card .ms-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 14.5px;
}
.card .ms-row:last-of-type { border-bottom: none; }
.card .ms-row b { font-variant-numeric: tabular-nums; font-weight: 600; }
.money-summary .ms-row.total {
  border-top: 1.5px solid var(--line);
  border-bottom: none;
  margin-top: 4px;
  padding-top: 12px;
  font-size: 16px;
}
.money-summary .ms-row.total b {
  font-family: var(--display);
  font-weight: 700;
  font-size: 21px;
  color: var(--accent-deep);
}

@media (max-width: 640px) {
  .period-custom { margin-left: 0; width: 100%; }
  .period-custom input[type="date"] { flex: 1 1 40%; font-size: 16px; }
  .period-pick .btn { flex: 1 1 auto; }
}

/* Me / the shop. A segmented control rather than a dropdown: it is two
   options an owner flips between constantly, and both should be visible. */
.scope-switch {
  display: inline-flex;
  padding: 3px;
  gap: 3px;
  margin-bottom: 14px;
  background: var(--cream-2);
  border: 1px solid var(--line);
  border-radius: 100px;
}
.scope-switch button {
  padding: 7px 18px;
  border: none;
  border-radius: 100px;
  background: transparent;
  color: var(--ink-3);
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  transition: all var(--t-fast) var(--ease);
}
.scope-switch button:hover { color: var(--ink); }
.scope-switch button.on {
  background: var(--card);
  color: var(--accent-dark);
  box-shadow: var(--shadow-sm);
}
@media (max-width: 520px) {
  .scope-switch { display: flex; width: 100%; }
  .scope-switch button { flex: 1; }
}

/* The owner's own row in a shop-wide table. */
tr.is-you td { background: var(--accent-tint); }
tr.is-you td:first-child { border-radius: var(--r-sm) 0 0 var(--r-sm); }
tr.is-you td:last-child { border-radius: 0 var(--r-sm) var(--r-sm) 0; }
@media (max-width: 640px) {
  table.stacked tr.is-you { border-color: var(--accent-line); background: var(--accent-tint); }
  table.stacked tr.is-you td { background: transparent; }
}

/* ============================================================
   Settings sections
   ============================================================ */

.set-tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 16px;
  overflow-x: auto;
  padding-bottom: 4px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.set-tabs::-webkit-scrollbar { display: none; }
.set-tabs button {
  flex: 0 0 auto;
  padding: 9px 16px;
  border-radius: 100px;
  border: 1.5px solid var(--line);
  background: var(--card);
  color: var(--ink-3);
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: all var(--t-fast) var(--ease);
}
.set-tabs button:hover { border-color: var(--ink-4); color: var(--ink); }
.set-tabs button.on {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  color: var(--accent-dark);
}
.set-panel { animation: riseSm .3s var(--ease) both; }

.rp-filter { margin-top: 12px; }
.rp-filter summary {
  cursor: pointer;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--accent-deep);
  list-style: none;
  padding: 4px 0;
  user-select: none;
}
.rp-filter summary::-webkit-details-marker { display: none; }
.rp-filter summary::before { content: '⚙ '; opacity: .7; }

.chip-row { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 10px; }
.chip {
  padding: 6px 12px;
  border-radius: 100px;
  border: 1.5px solid var(--line);
  background: var(--card);
  color: var(--ink-3);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: all var(--t-fast) var(--ease);
}
.chip:hover { border-color: var(--ink-4); color: var(--ink); }
.chip.on {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  color: var(--accent-dark);
}
.chip.on::before { content: '✓ '; }

/* ============================================================
   The shopfront a client sees
   ============================================================ */

.shop-logo {
  display: block;
  width: 84px;
  height: 84px;
  margin: 0 auto 14px;
  border-radius: 22px;
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--card);
  box-shadow: var(--shadow);
}
.shop-logo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Deliberately small. The shopfront should introduce the shop, not fill
   the screen before a client reaches the thing he came to do. Anyone who
   wants a proper look taps one and gets it full screen. */
.shop-gallery {
  display: flex;
  gap: 7px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 14px;
}
.shop-thumb {
  width: 74px;
  height: 74px;
  flex: 0 0 74px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  background: var(--cream-2);
  cursor: pointer;
  transition: transform var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.shop-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shop-thumb:hover { transform: translateY(-2px); border-color: var(--accent-line); }
.shop-thumb:active { transform: scale(.97); }

@media (max-width: 380px) {
  .shop-thumb { width: 64px; height: 64px; flex-basis: 64px; }
}

/* ============================================================
   Full-screen photo viewer
   ============================================================ */

.photo-viewer {
  position: fixed;
  inset: 0;
  z-index: 400;
  background: rgba(10,10,12,.94);
  display: grid;
  place-items: center;
  padding: 20px;
  animation: fade var(--t-fast) var(--ease) both;
}
.pv-img {
  max-width: 100%;
  max-height: calc(100dvh - 120px);
  object-fit: contain;
  border-radius: var(--r);
  animation: popIn .28s var(--ease) both;
}
.pv-close, .pv-nav {
  position: absolute;
  background: rgba(255,255,255,.14);
  color: #fff;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  display: grid;
  place-items: center;
  line-height: 1;
  transition: background var(--t-fast) var(--ease);
}
.pv-close:hover, .pv-nav:hover { background: rgba(255,255,255,.28); }
.pv-close {
  top: calc(14px + env(safe-area-inset-top));
  right: 14px;
  width: 42px; height: 42px;
  font-size: 26px;
}
.pv-nav {
  top: 50%;
  transform: translateY(-50%);
  width: 44px; height: 44px;
  font-size: 30px;
}
.pv-nav.prev { left: 10px; }
.pv-nav.next { right: 10px; }
.pv-count {
  position: absolute;
  bottom: calc(20px + env(safe-area-inset-bottom));
  left: 0; right: 0;
  text-align: center;
  color: rgba(255,255,255,.75);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
@media (max-width: 520px) {
  /* Arrows get in the way of a thumb on a small screen; swiping works. */
  .pv-nav { display: none; }
}

a.chip-info { text-decoration: none; }
a.chip-info:hover { border-color: var(--accent-line); background: var(--accent-tint); color: var(--accent-dark); text-decoration: none; }

/* ============================================================
   "Add to home screen" prompt on the client's booking page
   ============================================================ */

.install-bar {
  position: fixed;
  left: 12px; right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom));
  z-index: 200;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 12px 14px;
  background: var(--ink);
  color: #fff;
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  font-size: 13.5px;
  line-height: 1.45;
  animation: rise var(--t) var(--ease) both;
}
.install-bar span { flex: 1 1 180px; min-width: 0; }
.install-bar .btn { padding: 7px 13px; font-size: 13px; }
.install-bar .btn.ghost { background: transparent; border-color: rgba(255,255,255,.3); color: #fff; }
.install-bar .btn.ghost:hover { background: rgba(255,255,255,.12); color: #fff; }

/* Photo management in the barber's settings */
.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 10px;
  margin-top: 10px;
}
.photo-tile {
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: var(--r);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--cream-2);
}
.photo-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-tile button {
  position: absolute;
  top: 5px; right: 5px;
  width: 26px; height: 26px;
  border-radius: 50%;
  border: none;
  background: rgba(16,16,18,.72);
  color: #fff;
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  display: grid;
  place-items: center;
}
.photo-tile button:hover { background: var(--ink); }

/* ============================================================
   Crop and position
   ============================================================ */

.crop-wrap { display: grid; place-items: center; margin: 4px 0 6px; }
.crop-frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-lg);
  background: var(--cream-2);
  border: 1.5px solid var(--line);
  cursor: grab;
  touch-action: none;          /* we handle the dragging ourselves */
  user-select: none;
}
.crop-frame:active { cursor: grabbing; }
.crop-frame img {
  position: absolute;
  top: 0; left: 0;
  transform-origin: 0 0;
  max-width: none;             /* the frame crops; the image is not fitted */
  pointer-events: none;
  user-select: none;
}
/* A hint of the rule of thirds, so a face lands somewhere sensible. */
.crop-frame::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to right,  transparent 33.33%, rgba(255,255,255,.35) 33.33% 33.5%, transparent 33.5%,
                               transparent 66.5%, rgba(255,255,255,.35) 66.5% 66.67%, transparent 66.67%),
    linear-gradient(to bottom, transparent 33.33%, rgba(255,255,255,.35) 33.33% 33.5%, transparent 33.5%,
                               transparent 66.5%, rgba(255,255,255,.35) 66.5% 66.67%, transparent 66.67%);
}
.crop-zoom {
  display: block;
  margin: 14px 0 4px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-3);
}
.crop-zoom input[type="range"] { width: 100%; margin-top: 8px; accent-color: var(--accent); }

.logo-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.logo-preview {
  width: 68px; height: 68px;
  flex: 0 0 68px;
  border-radius: 18px;
  overflow: hidden;
  border: 1.5px dashed var(--line);
  background: var(--cream-2);
  display: grid;
  place-items: center;
  color: var(--ink-4);
  font-size: 11px;
  text-align: center;
}
.logo-preview img { width: 100%; height: 100%; object-fit: cover; }
.link-box .url { flex: 1; min-width: 0; color: var(--ink-2); }

/* ============================================================
   Responsive — phone first for the barber in the chair.
   On mobile the sidebar becomes a retractable vertical menu:
   hamburger in a fixed top bar, drawer slides in from the left.
   ============================================================ */

.mobile-bar { display: none; }
.menu-backdrop { display: none; }

@media (max-width: 860px) {
  body { font-size: 15.5px; }

  .layout { grid-template-columns: minmax(0, 1fr); }

  main {
    padding: calc(70px + env(safe-area-inset-top)) 16px calc(40px + env(safe-area-inset-bottom));
  }

  /* --- fixed top bar --- */
  .mobile-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 60;
    padding: calc(9px + env(safe-area-inset-top)) 14px 9px;
    /* Solid, not frosted. A blurred bar still passes the page through it,
       so scrolling dragged ghosts of the client names and headings under
       the shop's own name — which reads as a rendering fault, not as
       depth. The bar is opaque and the shadow does the lifting. */
    background: var(--card);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 1px 12px -6px rgba(28,26,23,.28);
  }
  .mobile-bar .brand { font-size: 17px; min-width: 0; }
  .mobile-bar .brand span:last-child {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .mobile-bar .brand-mark { width: 28px; height: 28px; flex-basis: 28px; border-radius: 8px; }

  /* --- hamburger --- */
  .menu-btn {
    position: relative;
    width: 42px; height: 42px;
    flex: 0 0 42px;
    display: grid;
    place-content: center;
    gap: 5px;
    background: var(--card);
    border: 1.5px solid var(--line);
    border-radius: var(--r);
    cursor: pointer;
    transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
  }
  .menu-btn span {
    display: block;
    width: 18px; height: 2px;
    border-radius: 2px;
    background: var(--ink);
    transition: transform var(--t) var(--ease), opacity var(--t-fast) var(--ease);
  }
  body.menu-open .menu-btn span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
  body.menu-open .menu-btn span:nth-child(2) { opacity: 0; }
  body.menu-open .menu-btn span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
  .menu-btn .menu-pip {
    position: absolute;
    top: 6px; right: 6px;
    width: 9px; height: 9px;
    border-radius: 50%;
    background: var(--accent);
    border: 1.5px solid #fff;
    animation: pulseRing 2s var(--ease-soft) infinite;
  }

  /* --- the drawer --- */
  .sidebar {
    position: fixed;
    top: 0; bottom: 0; left: 0;
    width: min(280px, 84vw);
    height: 100dvh;
    padding: calc(20px + env(safe-area-inset-top)) 16px calc(18px + env(safe-area-inset-bottom));
    transform: translateX(-105%);
    transition: transform var(--t) var(--ease);
    box-shadow: none;
    z-index: 80;
    background: rgba(255,255,255,.98);
  }
  body.menu-open .sidebar {
    transform: none;
    box-shadow: 24px 0 60px -18px rgba(28,26,23,.35);
  }
  .sidebar nav { overflow-y: auto; }
  .sidebar nav a { padding: 12px 12px; font-size: 15px; }

  .menu-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 70;
    background: rgba(28,26,23,.4);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    animation: fade var(--t) var(--ease) both;
  }
  .menu-backdrop.hidden { display: none; }

  body.impersonating .mobile-bar { top: var(--imp-h); }
  body.impersonating .layout { padding-top: var(--imp-h); }
  body.in-demo .mobile-bar { top: var(--imp-h); }
  body.in-demo .layout { padding-top: var(--imp-h); }

  .grid.cols-3, .grid.cols-4 { grid-template-columns: repeat(2, 1fr); }
  .page-head { align-items: flex-start; flex-direction: column; gap: 10px; }
  /* Once the head stacks, its buttons have the full width to themselves.
     Splitting it evenly beats letting "+ New booking" and "Send booking
     link" size themselves and leave a ragged gap — and it makes both
     comfortably thumb-sized on the screen this is mostly used on. */
  .page-head .btn-row { width: 100%; }
  .page-head .btn-row > * { flex: 1; justify-content: center; }
  .stat .value { font-size: 25px; }
  h1 { font-size: 23px; }

  .btn { padding: 11px 16px; }
  .btn.small { padding: 8px 12px; font-size: 13.5px; }
  input, select, textarea { font-size: 16px; padding: 12px 13px; }  /* 16px stops iOS zoom */

  /* right, not 16px, because the tail hangs 6px off that edge and the
     page clips horizontal overflow — at 16px it was sliced in half. */
  .toast { left: 16px; right: 24px; bottom: calc(20px + env(safe-area-inset-bottom)); max-width: none; }
  .modal { max-height: 92vh; padding: 22px 18px; }
  .form-grid { grid-template-columns: 1fr; }

  .btn-row { flex-wrap: wrap; }
  .link-box { font-size: 12px; }
  .day-nav .date-label { min-width: 0; flex: 1; }

  /* Tablet range. Below 640 these tables stop being tables altogether and
     stack into cards; here there is still room for columns, just tighter. */
  table { font-size: 13.5px; table-layout: auto; }
  td, th { padding: 9px 7px; word-break: break-word; }

  .appt-row > .appt-time, .appt-when { flex-basis: 84px; }
  .appt-time { font-size: 15px; }
  .appt-actions { justify-content: flex-start; }
  .appt-main { min-width: 0; }
}

/* ============================================================
   Tablets (portrait iPads, Android tablets: ~768–860px).

   A tablet has room for the real sidebar, so it gets it instead of the
   phone's hamburger drawer — it reads as proper software, not a blown-up
   phone. The narrower cards and 2-across grids from the blocks above
   still apply, since the main column is slimmer than on a laptop. This
   sits AFTER the phone chrome above and overrides just the layout parts.
   ============================================================ */
@media (min-width: 768px) and (max-width: 860px) {
  .layout { grid-template-columns: 210px 1fr; }
  .mobile-bar { display: none !important; }
  .menu-backdrop { display: none !important; }

  .sidebar {
    position: sticky;
    top: 0; left: 0;
    width: 210px;
    height: 100dvh;
    transform: none !important;
    box-shadow: none;
    background: rgba(255,255,255,.72);
    padding: 22px 14px 18px;
    z-index: 1;
  }
  body.menu-open .sidebar { box-shadow: none; }

  main { padding: 26px 26px 48px; }

  body.impersonating .layout { padding-top: var(--imp-h); }
  body.impersonating .sidebar { top: var(--imp-h); height: calc(100dvh - var(--imp-h)); }
  body.in-demo .layout { padding-top: var(--imp-h); }
  body.in-demo .sidebar { top: var(--imp-h); height: calc(100dvh - var(--imp-h)); }
}

@media (max-width: 640px) {
  /* Charts and paired cards stack — everything reads top to bottom. */
  .grid.cols-2 { grid-template-columns: 1fr; }

  /* ----------------------------------------------------------
     Appointment and request rows.

     On a wide screen these are one line: time, details, buttons.
     On a phone that same row forces the details into a column two
     or three words wide, and a client's name, service, phone and
     note all wrap into an unreadable ribbon. So the row becomes
     three stacked bands instead — when, who, then what to do —
     each with the full width of the screen to breathe.
     ---------------------------------------------------------- */
  .appt-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "time main"
      "acts acts";
    align-items: start;
    gap: 2px 12px;
    padding: 14px 6px;
  }
  .appt-row > .appt-time,
  .appt-when {
    grid-area: time;
    flex: none;
    min-width: 78px;
    padding-left: 9px;
  }
  .appt-time { font-size: 15.5px; }
  .appt-main {
    grid-area: main;
    min-width: 0;          /* lets long names wrap instead of stretching the row */
  }
  .appt-main > div { margin-bottom: 2px; }
  .appt-main .svc { font-size: 13.5px; line-height: 1.5; }
  .appt-actions {
    grid-area: acts;
    justify-content: flex-start;
    gap: 8px;
    margin-top: 10px;
  }
  /* Two clear targets rather than three cramped ones. */
  .appt-actions .btn { flex: 1 1 auto; min-width: 96px; justify-content: center; }

  .next-card .nc-who b { font-size: 15px; }

  /* ----------------------------------------------------------
     Tables become stacked cards.

     Five columns cannot be read at 360px wide. Each row turns into
     its own card, with the column name beside each value, reading
     downwards. The labels come from the table's own headers — see
     labelTables() in app.js.
     ---------------------------------------------------------- */
  table.stacked { display: block; font-size: 14px; }
  table.stacked thead { display: none; }
  table.stacked tbody,
  table.stacked tr,
  table.stacked td { display: block; width: 100%; }

  table.stacked tr {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r);
    padding: 4px 13px 10px;
    margin-bottom: 9px;
    box-shadow: var(--shadow-sm);
  }
  table.stacked tr:last-child { margin-bottom: 0; }

  /* Label left, value right — but the cell wraps, so a short value like a
     price sits neatly on the label's line while something long like a
     client's preferences drops onto its own full-width line and reads
     left to right as ordinary prose. No value ever gets squeezed into a
     narrow right-hand column. */
  table.stacked td {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 2px 14px;
    padding: 7px 0;
    border: none;
    border-bottom: 1px solid var(--line-soft);
    text-align: left;
    word-break: break-word;
    hyphens: auto;
  }
  table.stacked td:last-child { border-bottom: none; }

  table.stacked td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    color: var(--ink-3);
    font-size: 12.5px;
    font-weight: 600;
    white-space: nowrap;
  }
  table.stacked td.cell-actions {
    display: flex;
    justify-content: flex-start;
    gap: 7px;
    flex-wrap: wrap;
    padding-top: 11px;
  }
  table.stacked td.cell-actions::before { display: none; }
  table.stacked td.cell-actions .btn { flex: 1 1 auto; min-width: 92px; justify-content: center; }

  /* A whole-row link (the clients list) still behaves as one target. */
  table.stacked tr.clickable { cursor: pointer; }
  table.stacked tr.clickable:hover td { background: transparent; }
  table.stacked tr.clickable:active { border-color: var(--accent-line); background: var(--accent-tint); }
}

@media (max-width: 420px) {
  .grid.cols-2, .grid.cols-3, .grid.cols-4 { grid-template-columns: 1fr; }
}

/* ============================================================
   Accessibility
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================
   Full-screen blur is charged by the pixel

   A backdrop-filter over the whole viewport is recomputed on every frame
   of whatever is moving underneath it, and `saturate()` beside a blur
   roughly doubles that work. On a desktop nobody notices. On the phone a
   barber actually owns it is the most expensive thing on the screen, and
   it lands at the worst moment: the "let them know" panel opens the
   instant he approves a booking, so the cost arrives on top of a page
   that is still settling.

   The blur stays — it is what makes these read as sheets rather than
   dialogs — at a radius a phone can carry, and the saturation comes off,
   which is the half of the pairing that is nearly invisible at this
   opacity anyway.
   ============================================================ */
@media (max-width: 860px) {
  .modal-backdrop {
    backdrop-filter: blur(7px);
    -webkit-backdrop-filter: blur(7px);
  }
  .note-backdrop {
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
  }
}

:focus-visible { outline: 2.5px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
::selection { background: rgba(14,110,94,.16); }

/* Editable generated password */
.pw-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: .02em;
  background: var(--cream-2);
  border-style: dashed;
}
.pw-input:focus { background: var(--card); border-style: solid; }

/* ============================================================
   Client book — modelled on the phone's Contacts app
   Two screens: an alphabetical list with a letter rail, and a
   contact card behind each name. The point of borrowing the
   pattern is that nobody has to be taught it.
   ============================================================ */

.contacts-search {
  position: relative;
  display: flex; align-items: center;
  margin-bottom: 14px;
}
.contacts-search .ui-icon {
  position: absolute; left: 13px;
  color: var(--ink-4); pointer-events: none;
}
.contacts-search input {
  width: 100%;
  padding: 11px 14px 11px 38px;
  font-size: 16px;                 /* 16px or iOS zooms the page on focus */
  background: var(--cream-2);
  border: 1px solid transparent;
  border-radius: 999px;
  transition: background var(--t-fast) var(--ease-soft),
              border-color var(--t-fast) var(--ease-soft);
}
.contacts-search input:focus {
  background: var(--card);
  border-color: var(--accent-line);
  outline: none;
}
.contacts-search input::-webkit-search-cancel-button { cursor: pointer; }

.contacts {
  position: relative;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
}

/* The letter heading stays put while its own group scrolls past, which is
   what makes a long list navigable — you always know where you are. */
.c-sec-h {
  position: sticky; top: 0; z-index: 2;
  margin: 0;
  padding: 5px 18px;
  font-family: var(--display);
  font-size: 12px; font-weight: 600; letter-spacing: .06em;
  color: var(--ink-3);
  background: var(--cream-2);
  border-bottom: 1px solid var(--line-soft);
}

.c-row {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 34px 10px 16px;
  color: inherit; text-decoration: none;
  border-bottom: 1px solid var(--line-soft);
  transition: background var(--t-fast) var(--ease-soft);
}
.c-row:last-child { border-bottom: none; }
.c-row:active { background: var(--cream-2); }
@media (hover: hover) { .c-row:hover { background: var(--cream); } }

.c-av {
  flex: none;
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--cream-2);
  color: var(--ink-3);
  font-family: var(--display);
  font-size: 14px; font-weight: 600; letter-spacing: .02em;
}
.c-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.c-txt b { font-weight: 500; font-size: 15.5px; }
.c-txt small {
  color: var(--ink-3); font-size: 13px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.c-row > .ui-icon { flex: none; color: var(--ink-4); }

/* The A–Z rail. Sticky rather than fixed so it can't escape its card, and
   the inactive letters stay visible but dimmed — a rail that changes length
   as you search is disorienting. */
.contacts-index {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 26px;
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  gap: 1px;
  padding: 6px 0;
  background: linear-gradient(to right, transparent, var(--card) 40%);
  user-select: none;
}
.contacts-index .ci {
  all: unset;
  cursor: pointer;
  font-size: 10px; font-weight: 600; line-height: 1.05;
  color: var(--accent);
  padding: 0 6px;
}
.contacts-index .ci.off { color: var(--ink-4); opacity: .45; cursor: default; }
.contacts-index .ci:active { color: var(--accent-dark); }

.contacts-empty { padding: 44px 24px; text-align: center; color: var(--ink-3); }
.contacts-empty .ui-icon { color: var(--ink-4); margin-bottom: 10px; }
.contacts-empty p { margin: 0 0 4px; }

/* --- the contact card --- */

.cd-back {
  display: inline-flex; align-items: center; gap: 5px;
  margin-bottom: 10px;
  color: var(--accent); text-decoration: none;
  font-size: 15px; font-weight: 500;
}
.cd-back:hover { text-decoration: underline; }

.cd-hero { text-align: center; padding: 6px 0 18px; }
.cd-av {
  display: grid; place-items: center;
  width: 86px; height: 86px; margin: 0 auto 12px;
  border-radius: 50%;
  background: var(--cream-2);
  color: var(--ink-3);
  font-family: var(--display);
  font-size: 30px; font-weight: 600; letter-spacing: .02em;
}
.cd-hero h1 { font-size: 25px; margin: 0; letter-spacing: -.01em; }
.cd-hero .muted { margin-top: 3px; font-size: 14px; }

.cd-actions {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px;
  margin-bottom: 18px;
}
.cd-act {
  all: unset;
  cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 12px 4px;
  border-radius: var(--r);
  background: var(--card);
  border: 1px solid var(--line);
  color: var(--accent);
  font-size: 12.5px; font-weight: 500;
  text-align: center;
  transition: transform var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease-soft);
}
.cd-act span { display: grid; place-items: center; }
/* The one action on a client card that opens WhatsApp wears the same green
   as every other WhatsApp button. Green means exactly two things in this
   interface, and this is one of them — a barber should be able to find
   "message him" by colour without reading the label. */
.cd-act#cd-wa:not(.off) { color: #1B9349; }
.cd-act#cd-wa:not(.off) .ui-icon { color: #1FA855; }
.cd-act#cd-wa:not(.off):active { background: rgba(31, 168, 85, .1); }
.cd-act:active { transform: scale(.96); background: var(--accent-tint); }
@media (hover: hover) { .cd-act:hover { background: var(--accent-tint); } }
.cd-act.off { color: var(--ink-4); cursor: default; }
.cd-act.off:active { transform: none; background: var(--card); }
@media (hover: hover) { .cd-act.off:hover { background: var(--card); } }

/* Remove sits in the row with Call, Message, Book and Edit, because that is
   where someone looks for it. It is the only red on the page, which is the
   whole of its warning — it does not need a box of its own underneath, and
   it still asks out loud before anything happens. */
.cd-act.bad { color: var(--red-dark); border-color: var(--red-line); }
.cd-act.bad:active { background: var(--red-tint); }
@media (hover: hover) { .cd-act.bad:hover { background: var(--red-tint); } }

.cd-warn {
  padding: 12px 14px; margin-bottom: 16px;
  border-radius: var(--r);
  background: var(--red-tint);
  border: 1px solid var(--red-line);
  color: var(--red-dark);
  font-size: 14px;
}

/* Four numbers, no boxes — the barber reads them as one line rather than as
   four competing cards. */
.cd-glance {
  display: grid; grid-template-columns: repeat(4, 1fr);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  margin-bottom: 6px;
}
.cd-glance > div {
  padding: 13px 8px;
  text-align: center;
  border-right: 1px solid var(--line-soft);
}
.cd-glance > div:last-child { border-right: none; }
.cd-glance small { display: block; color: var(--ink-3); font-size: 11.5px; }
.cd-glance b {
  display: block; margin-top: 3px;
  font-family: var(--display);
  font-size: 19px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.cd-glance b.bad { color: var(--red-deep); }
.cd-glance b.due { color: var(--accent); }
.cd-note { margin: 0 0 16px; font-size: 13.5px; color: var(--accent); text-align: center; }

.cd-gh {
  margin: 22px 0 8px 4px;
  font-family: var(--display);
  font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-3);
}

.cd-group {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  margin-bottom: 16px;
}
.cd-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 15px;
  color: inherit; text-decoration: none;
  border-bottom: 1px solid var(--line-soft);
}
.cd-row:last-child { border-bottom: none; }
a.cd-row:active { background: var(--cream-2); }
@media (hover: hover) { a.cd-row:hover { background: var(--cream); } }
.cd-ic { flex: none; display: grid; place-items: center; color: var(--ink-4); }
.cd-rt { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.cd-rt small { color: var(--ink-3); font-size: 12px; }
.cd-rt b { font-weight: 500; font-size: 15px; word-break: break-word; }
a.cd-row .cd-rt b { color: var(--accent); }
.cd-row > .ui-icon { flex: none; color: var(--ink-4); }
.cd-row.plain { display: block; padding: 14px 15px; }
.cd-row.plain p { margin: 0; font-size: 14.5px; white-space: pre-wrap; }
.cd-row.hist .cd-rt b { color: inherit; }
.cd-row.hist .badge { flex: none; }

/* On a narrow phone four action tiles are tight but still tappable; the
   labels shrink before the touch targets do. */
@media (max-width: 380px) {
  /* Five across on a 360px phone leaves about 60px a button. The label has
     to shrink to fit "Message" without breaking to two lines. */
  .cd-actions { gap: 5px; }
  .cd-act { font-size: 10.5px; padding: 10px 1px; letter-spacing: -.2px; }
  .cd-glance b { font-size: 17px; }
  .cd-glance small { font-size: 10.5px; }
}

/* Share of the shop's takings, beside the amount in the per-barber table.
   Deliberately quiet — the money is the headline, the percentage is the
   context, so it sits in the neutral tint rather than competing in accent. */
.share-chip {
  display: inline-block;
  margin-right: 8px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--cream-2);
  color: var(--ink-3);
  font-size: 11.5px; font-weight: 600;
  font-variant-numeric: tabular-nums;
  vertical-align: 1px;
}

/* ============================================================
   A barber's own photo
   Shown to a client on the shop link when he picks who to book
   with, and here so the owner can see who still has none.
   ============================================================ */

.face-row {
  display: flex; align-items: flex-start; gap: 16px;
  margin-top: 20px; padding-top: 18px;
  border-top: 1px solid var(--line);
}
.face-preview {
  flex: 0 0 72px; height: 72px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--accent-tint);
  color: var(--accent-dark);
  font-family: var(--display);
  font-size: 22px; font-weight: 600; letter-spacing: .5px;
  background-size: cover; background-position: center;
}
/* Once a real photo is in there the tint behind it would show as a ring on
   anything not perfectly square, so it goes and a hairline takes its place. */
.face-preview.has-photo {
  background-color: transparent;
  box-shadow: inset 0 0 0 1px rgba(16,16,18,.10);
}
.face-copy { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.face-copy b { font-size: 15px; }
.face-copy .muted { font-size: 13.5px; margin-top: 2px; }

/* On the team list, a real face gets the same hairline instead of the
   grey plate the shop mark sits on. */
.shop-row .avatar.is-face { filter: none; box-shadow: inset 0 0 0 1px rgba(16,16,18,.10); }

@media (max-width: 480px) {
  .face-row { gap: 13px; }
  .face-preview { flex-basis: 60px; height: 60px; font-size: 19px; }
}

/* A rep with no photo yet — the same disc as everywhere else, carrying her
   initials rather than the grey plate a missing shop logo leaves behind. */
.shop-row .avatar.rep-initials {
  display: grid; place-items: center;
  background: var(--accent-tint);
  color: var(--accent-dark);
  font-family: var(--display);
  font-size: 14px; font-weight: 600; letter-spacing: .5px;
}

/* Removing a client sits apart from the four things he does every day, and
   below the history rather than above it — so it is found on purpose and
   never on the way to something else. */
/* .cd-danger is gone — Remove moved up into the action row with Call,
   Message, Book and Edit, where people look for it. */

/* ============================================================
   Manage one account
   A panel of grouped choices rather than a row of identical
   buttons. Grouping is by consequence: everyday things first,
   then things that lock somebody out, then the one that
   destroys their data — set apart, at the bottom.
   ============================================================ */

.modal.wide { max-width: 560px; }

.ms-head {
  display: flex; align-items: center; gap: 14px;
  padding-bottom: 16px; margin-bottom: 6px;
  border-bottom: 1px solid var(--line);
}
.ms-head h2 { margin: 0; font-size: 20px; }
.ms-head .muted { margin-top: 2px; font-size: 13.5px; }
.ms-head .badge { margin-left: auto; align-self: flex-start; }

.ms-group { margin-top: 18px; }
.ms-title {
  font-family: var(--display);
  font-size: 11px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 8px;
}

.ms-item {
  display: flex; align-items: center; gap: 13px;
  width: 100%;
  padding: 13px 14px;
  margin-bottom: 8px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease-soft),
              background var(--t-fast) var(--ease-soft),
              transform var(--t-fast) var(--ease);
}
.ms-item:last-child { margin-bottom: 0; }
.ms-item:active { transform: scale(.99); }
@media (hover: hover) {
  .ms-item:hover { border-color: var(--accent-line); background: var(--accent-tint); }
}

.ms-ic {
  flex: none;
  width: 36px; height: 36px; border-radius: 10px;
  display: grid; place-items: center;
  background: var(--accent-tint);
  color: var(--accent-deep);
}
.ms-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.ms-txt b { font-size: 15px; font-weight: var(--w-name); }
.ms-txt small {
  color: var(--ink-3); font-size: 12.5px; margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ms-item > .ui-icon { flex: none; color: var(--ink-4); }

/* The one that cannot be undone reads as red before it is pressed, not
   after — including the icon, so it is recognisable at a glance. */
.ms-group.danger .ms-title { color: var(--red-deep); }
.ms-item.bad { border-color: var(--red-line); }
.ms-item.bad .ms-ic { background: var(--red-tint); color: var(--red-deep); }
.ms-item.bad .ms-txt b { color: var(--red-dark); }
@media (hover: hover) {
  .ms-item.bad:hover { border-color: var(--red-deep); background: var(--red-tint); }
}

/* The deletion confirmation. Plain, itemised, no reassuring language. */
.del-warn {
  background: var(--red-tint);
  border: 1px solid var(--red-line);
  border-radius: var(--r);
  padding: 14px 16px;
  margin-bottom: 16px;
}
.del-warn p { font-size: 14px; color: var(--red-dark); }
.del-warn p.muted { color: var(--ink-3); font-size: 13px; margin-top: 10px; }
.del-warn ul { margin: 8px 0 0 18px; }
.del-warn li { font-size: 14px; color: var(--red-dark); margin-bottom: 3px; }

@media (max-width: 520px) {
  .ms-item { padding: 12px; gap: 11px; }
  .ms-ic { width: 32px; height: 32px; border-radius: 9px; }
  .ms-txt b { font-size: 14.5px; }
  .ms-txt small { font-size: 12px; }
}

/* The booking area takes focus when a client picks his barber, so a screen
   reader announces where he has been taken. It is not in the tab order —
   tabindex is -1 and set from script — so there is no keyboard case that
   wants a ring drawn around the whole step. */
#book-anchor:focus, #book-anchor:focus-visible { outline: none; }

/* ==================================================================
   A client, wherever his name is written.
   ==================================================================
   The Clients book has always drawn a disc of initials beside a name,
   the way the phone's own Contacts does. Everywhere else — the requests
   list, the calendar, the takings table — the same man was a bare line
   of text. One pair now, written by clientTag(), so the two can't drift.

   The whole point of the box is that it does not jumble. Three rules do
   that work, and each of them is there because something broke without it:

     · inline-flex, so the disc and the name are one object and a badge
       sitting after them wraps as a unit rather than splitting the name
       off from its own picture;
     · min-width:0 on the name, because a flex child otherwise refuses to
       go narrower than its text and one long name pushes the badges,
       the figures and the buttons out of the row;
     · vertical-align:middle, so inside a table cell the disc sits on the
       text's centre line instead of dropping the row by four pixels.
   ================================================================== */
.cl-tag {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  max-width: 100%;
  vertical-align: middle;
}
.cl-av {
  flex: 0 0 auto;
  width: 28px; height: 28px;
  border-radius: 50%;
  display: grid; place-items: center;
  background: var(--cream-2);
  color: var(--ink-3);
  font-family: var(--display);
  font-size: 11px; font-weight: 600; letter-spacing: .02em;
  line-height: 1;
  /* A hairline, so the disc still reads as a disc on a card whose own
     background is the same wash. */
  box-shadow: inset 0 0 0 1px rgba(16, 16, 18, .06);
}
.cl-av.sm { width: 22px; height: 22px; font-size: 9.5px; }
.cl-av.lg { width: 34px; height: 34px; font-size: 13px; }
.cl-nm {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
b.cl-nm { font-weight: 600; }

/* The line a name shares with its badges. Wraps as a row, not as prose,
   so a "New client" or "Missed 2 before" badge drops underneath the name
   instead of landing halfway through it. */
.appt-name {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 7px;
  min-width: 0;
}
.appt-name .badge { flex: 0 0 auto; }

/* The calendar card puts the status badge hard right, so the name half
   has to be the part that gives. */
.ca-top .ca-name { display: flex; align-items: center; gap: 7px; min-width: 0; }
.ca-top .ca-name .cl-nm { font-weight: 600; }

/* In a table the disc must not stretch the row: the name gives before the
   money column does. Below 640px the tables stop being tables — each row
   becomes its own card with the value on its own line — so the ceiling
   comes off there, or a name that finally has room gets clipped anyway. */
td .cl-tag { max-width: 22ch; }
@media (max-width: 640px) {
  table.stacked td .cl-tag { max-width: 100%; }
  .cl-av { width: 24px; height: 24px; font-size: 10px; }
  .cl-av.lg { width: 30px; height: 30px; font-size: 12px; }
}

/* The Next-up card styles its own children by tag — `b` for the name, `span`
   for the service line. The tag box brings both of those inside it, so the
   name inherited the service line's grey 13px and the service line came up
   alongside the name instead of under it. These four rules put the card back
   the way it reads, without unpicking its own styling. */
.next-card .nc-who .cl-tag {
  display: flex;
  font-size: 15.5px;
  color: var(--ink);
  margin-bottom: 1px;
}
.next-card .nc-who .cl-nm { display: block; font-size: 15.5px; font-weight: 600; }
.next-card .nc-who .cl-av { font-size: 12px; color: var(--ink-3); }
@media (max-width: 720px) {
  .next-card .nc-who .cl-tag, .next-card .nc-who .cl-nm { font-size: 15px; }
}

/* ==================================================================
   Admin: one shop, one line.
   ==================================================================
   A shop with four chairs used to arrive as five flat rows carrying the
   same name, and reading down twenty of those to find one man is the
   whole complaint. A shop with staff now collapses to a single line —
   its mark, its name, how many chairs, and the shop's own totals — and
   opens to the owner and each of his barbers underneath.

   A one-chair barber is untouched. A dropdown around one man adds a tap
   and tells you nothing.
   ================================================================== */
.shop-group {
  border-bottom: 1px solid var(--line-soft);
}
.shop-group:last-child { border-bottom: none; }
.shop-group.off { opacity: .58; }

.sg-head {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 4px;
  cursor: pointer;
  list-style: none;
  border-radius: var(--r-sm);
  transition: background var(--t-fast) var(--ease);
}
.sg-head::-webkit-details-marker { display: none; }
.sg-head:hover { background: var(--cream-2); }

.sg-caret {
  flex: 0 0 auto;
  display: grid; place-items: center;
  color: var(--ink-4);
  transition: transform var(--t-fast) var(--ease);
}
.shop-group[open] > .sg-head .sg-caret { transform: rotate(90deg); }

/* Same plate as a shop row, so an open group and a solo barber still read
   as the same kind of thing. */
.sg-head .avatar {
  width: 40px; height: 40px; flex: 0 0 40px;
  border-radius: 11px;
  background: var(--red) url('/icons/logo.svg') center/cover no-repeat;
  box-shadow: var(--shadow-sm);
}
.sg-head .avatar.own {
  background-color: #fff;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border: 1px solid var(--line);
}

.sg-who {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 7px;
}
.sg-who b { font-size: 15px; font-weight: 600; min-width: 0;
            overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sg-who .badge { flex: 0 0 auto; }
/* The roll-call takes the whole second line and truncates there, rather
   than fighting the badges for room on the first. */
.sg-who .sub {
  flex: 1 1 100%;
  color: var(--ink-3); font-size: 12.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The same three columns the rows below use, at the same widths, plus a
   spacer standing where their Open and Manage buttons stand. Without it the
   shop's totals sit a hundred and fifty pixels right of the figures they
   are the sum of, and the eye cannot read down the column. */
.sg-head .figs {
  flex: 0 0 auto;
  display: flex;
  gap: 20px;
}
.sg-head .fig { min-width: 62px; text-align: right; display: block; }
.sg-spacer { flex: 0 0 auto; width: 152px; }
.sg-head .fig .n {
  display: block;
  font-family: var(--display); font-weight: 700; font-size: 17px;
  font-variant-numeric: tabular-nums; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sg-head .fig .k {
  display: block;
  font-size: 10px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-4); font-weight: 600;
}

/* The rows inside sit indented under the shop's mark, so it is obvious at
   a glance which line is the header and which are the chairs. */
.sg-body {
  padding-left: 26px;
  border-left: 2px solid var(--line-soft);
  margin-left: 22px;
  margin-bottom: 6px;
}
.sg-body .shop-row { padding-top: 11px; padding-bottom: 11px; }
.sg-body .shop-row:last-child { border-bottom: none; }
/* A chair's own picture, not the shop plate again — the shop's mark is
   already on the line above and repeating it four times says nothing. */
.sg-body .shop-row.staff .avatar { border-radius: 50%; }

@media (max-width: 860px) {
  .sg-head { flex-wrap: wrap; row-gap: 10px; }
  .sg-who { flex: 1 1 100%; order: 1; }
  .sg-caret, .sg-head .avatar { order: 0; }
  .sg-head .figs { order: 2; flex: 1 1 auto; width: 100%;
                   display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
  .sg-head .fig { text-align: left; min-width: 0; }
  .sg-spacer { display: none; }
  .sg-body { padding-left: 12px; margin-left: 10px; }
}

/* ============================================================
   Comparisons — the Team page and Payments
   ============================================================ */

/* --- the column charts on the shop analytics ---
   These markup classes have been in app.js since analytics went in and
   have never had a rule to their name, so every one of them rendered as a
   stack of empty divs and the owner saw three headings with nothing under
   them. Written out here: a flex row of columns growing from a baseline. */
.bars {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 150px;
  padding-top: 6px;
  border-bottom: 1px solid var(--line);
}
.bars .bar {
  flex: 1 1 0;
  min-width: 2px;
  min-height: 2px;
  border-radius: 3px 3px 0 0;
  background: var(--accent);
  opacity: .82;
  transition: opacity var(--t-fast) var(--ease);
}
.bars .bar:hover { opacity: 1; }
.bar-labs {
  display: flex;
  gap: 3px;
  margin-top: 6px;
}
.bar-labs span {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--ink-4);
}

/* --- period pills ---
   The reading and the range it covers should never be more than a glance
   apart, so the choice sits on the card rather than behind a dropdown. On
   a phone it scrolls sideways: six options wrapping into three ragged
   lines is worse than one line he can push along with his thumb. */
.time-seg {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding-bottom: 4px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.time-seg::-webkit-scrollbar { display: none; }
.time-seg button {
  flex: 0 0 auto;
  padding: 7px 14px;
  border-radius: 100px;
  border: 1.5px solid var(--line);
  background: var(--card);
  color: var(--ink-3);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: all var(--t-fast) var(--ease);
}
.time-seg button:hover { border-color: var(--ink-4); color: var(--ink); }
.time-seg button.on {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  color: var(--accent-dark);
}
.time-seg.small button { padding: 5px 12px; font-size: 12.5px; }

/* A heading with its controls on the same line, falling under it when the
   card gets narrow rather than squeezing the title to three words. */
.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.card-head h2 { margin-bottom: 0; }
.card-tools {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-left: auto;
}
.card-tools select {
  width: auto;
  min-width: 150px;
  margin-top: 0;
  padding: 7px 10px;
  font-size: 13px;
}
@media (max-width: 640px) {
  .card-tools { margin-left: 0; width: 100%; }
  .card-tools select { flex: 1 1 100%; min-width: 0; }
  .card-tools .time-seg { width: 100%; }
}

/* --- name against bar ---
   Who is ahead, by how much, and what share of the whole — read left to
   right in one pass. The owner's own row is tinted the same way his row in
   the table is, so the two say the same thing about the same person. */
.hbars { display: grid; gap: 9px; }
.hb {
  display: grid;
  grid-template-columns: minmax(70px, 130px) 1fr auto auto;
  align-items: center;
  gap: 10px;
  padding: 3px 6px;
  border-radius: var(--r-sm);
}
.hb.you { background: var(--accent-tint); }
.hb-name {
  font-size: 13.5px;
  font-weight: var(--w-name);
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hb-track {
  height: 12px;
  border-radius: 100px;
  background: var(--cream-2);
  overflow: hidden;
}
.hb-fill {
  display: block;
  height: 100%;
  border-radius: 100px;
  background: var(--accent);
  opacity: .85;
}
.hb-val {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.hb-pct {
  min-width: 38px;
  text-align: right;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-4);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 560px) {
  .hb { grid-template-columns: 1fr auto auto; row-gap: 4px; }
  .hb-name { grid-column: 1 / -1; }
  .hb-track { grid-column: 1; }
}

/* The period a figure covers, said under it. Without this the tile reads
   as "takings, full stop" and he compares last week against last year. */
.stat-note {
  margin-top: 5px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--ink-4);
}

/* --- the shop's day, cut into chairs ---
   A band with the barber's name and how many he is carrying, then his
   bookings under it. The band is what makes the list navigable: an owner
   scans four names, not forty rows. */
table.day-grouped tbody + tbody tr.grp td { padding-top: 18px; }
table.day-grouped tr.grp td {
  border-bottom: 1px solid var(--line);
  padding-bottom: 7px;
  background: transparent;
}
.grp-name {
  font-family: var(--display);
  font-size: 14.5px;
  font-weight: 700;
  color: var(--ink);
}
.grp-n {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 8px;
  border-radius: 100px;
  background: var(--cream-2);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* On a phone the tables stop being tables and each row becomes a card —
   so the group band stops being a band and becomes what it should be
   there: a plain heading over the cards that belong to it. */
@media (max-width: 640px) {
  table.day-grouped tbody { margin-bottom: 4px; }
  table.stacked tr.grp {
    background: transparent;
    border: none;
    box-shadow: none;
    padding: 0;
    margin: 14px 0 8px;
  }
  table.stacked tbody:first-of-type tr.grp { margin-top: 0; }
  table.stacked tr.grp td {
    display: block;
    padding: 0 0 6px;
    border-bottom: 1px solid var(--line);
  }
  table.stacked tr.grp td::before { content: none; }
}

/* ============================================================
   Billing — the admin's ledger, and the barber's own bill
   ============================================================ */

/* A wide table on a phone scrolls sideways inside its card rather than
   pushing the whole page off the screen. */
.bill-table { width: 100%; display: block; overflow-x: auto; white-space: nowrap; }
.bill-table td, .bill-table th { vertical-align: middle; }
.bill-table td .muted { white-space: normal; }

/* The price field sits in the table and has to read as editable without
   turning every row into a form. */
.rate-in {
  width: 108px;
  text-align: right;
  padding: 6px 9px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font: inherit;
  font-weight: var(--w-name);
  background: var(--card);
}
.rate-in:focus { outline: none; border-color: var(--accent-line); box-shadow: 0 0 0 3px var(--accent-soft); }

/* A wide table keeps its own scrollbar rather than forcing the page to
   scroll sideways or crushing its own columns. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-scroll .bill-table { min-width: 900px; }
.bill-table td.right, .bill-table th.right { white-space: nowrap; }

/* --- the invoice, drawn inside the app ------------------------------
   Laid out like the PDF it can become: a header, who it is for, one
   line, a total, and how to pay. Deliberately the same document rather
   than a summary of it — a barber comparing what he read on screen with
   the file he printed should find them identical. */
.doc-sheet {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 22px 22px 18px;
}
.doc-top { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; }
.doc-brand { font-family: var(--display); font-size: 20px; display: block; }
.doc-top small { color: var(--ink-3); font-size: 12px; display: block; }
.doc-kind { text-align: right; }
.doc-kind b { font-family: var(--display); font-size: 17px; letter-spacing: .08em; }
.doc-void { color: var(--red-deep); font-weight: 700; }

.doc-cols {
  display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
  margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line);
}
.doc-cols small {
  display: block; font-size: 10.5px; font-weight: 700; letter-spacing: .08em;
  color: var(--ink-4); margin-bottom: 6px;
}
.doc-cols b { font-size: 15px; display: block; }
.doc-cols span { color: var(--ink-3); font-size: 13px; display: block; }
.doc-kv { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; margin-bottom: 3px; }
.doc-kv span { color: var(--ink-3); }
.doc-kv b { font-size: 13px; }

.doc-table { width: 100%; border-collapse: collapse; margin-top: 20px; }
.doc-table th {
  text-align: left; font-size: 10.5px; letter-spacing: .08em; color: var(--ink-4);
  border-bottom: 1px solid var(--line); padding: 6px 0;
}
.doc-table td { padding: 12px 0; border-bottom: 1px solid var(--line); font-size: 14.5px; }
.doc-table .right { text-align: right; font-variant-numeric: tabular-nums; }
.doc-note { color: var(--ink-3); font-size: 12.5px; font-style: italic; margin-top: 4px; }
.doc-table tfoot td { border-bottom: none; font-weight: 600; padding-top: 14px; }
.doc-total { font-size: 19px; font-family: var(--display); }
.doc-total.ok { color: var(--green); }

.doc-pay { margin-top: 16px; }
.doc-pay small {
  display: block; font-size: 10.5px; font-weight: 700; letter-spacing: .08em;
  color: var(--ink-4); margin-bottom: 5px;
}
.doc-pay p { margin: 0; font-size: 13.5px; }
.doc-actions { margin-top: 16px; }

@media (max-width: 560px) {
  .doc-cols { grid-template-columns: 1fr; gap: 14px; }
  .doc-sheet { padding: 16px; }
}

/* PRINTING IT.

   Everything but the document itself is taken off the page — a printed
   invoice with an app menu down the side of it is not a document you
   would send to anybody. On a phone the print dialog is also where
   "Save as PDF" lives, which is why the button says Print rather than
   Download. */
@media print {
  body.modal-open { position: static; overflow: visible; }
  body * { visibility: hidden !important; }
  #modal-backdrop, #modal, .doc-sheet, .doc-sheet * { visibility: visible !important; }
  #modal-backdrop {
    position: static !important; background: none !important;
    display: block !important; padding: 0 !important;
  }
  #modal {
    position: static !important; box-shadow: none !important;
    max-width: none !important; width: auto !important;
    max-height: none !important; overflow: visible !important; padding: 0 !important;
  }
  .doc-sheet { border: none; padding: 0; }
  .doc-actions, .modal-actions { display: none !important; }
  @page { margin: 16mm; }
}

/* The page, held still behind a dialog. Fixed rather than
   overflow:hidden, because iOS scrolls the body anyway under an overlay
   and a flick meant for the dialog's own list takes the page with it. */
body.modal-open {
  position: fixed;
  left: 0; right: 0;
  width: 100%;
  overflow: hidden;
}

/* --- billing: a shop, and an invoice, as a row you can open ---------
   A table asks you to read across a row and assemble the shop yourself.
   A row that opens is one line of the four things that matter and the
   rest a tap away, and it survives a phone, which a seven-column table
   does not. */
.shop-lines, .inv-lines { list-style: none; margin: 0; padding: 0; }
.shop-line, .inv-line {
  width: 100%;
  display: flex; align-items: center; gap: 14px;
  padding: 13px 14px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--card);
  text-align: left;
  cursor: pointer;
  margin-bottom: 8px;
  transition: border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.shop-line:hover, .inv-line:hover { border-color: var(--ink-4); }
.shop-line:active, .inv-line:active { transform: scale(.995); }
.shop-line .ui-icon, .inv-line .ui-icon { color: var(--ink-4); flex: 0 0 auto; }

.sl-main, .il-who { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.sl-main b, .il-who b {
  font-family: var(--display); font-size: 15px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sl-main small, .il-who small { color: var(--ink-3); font-size: 12.5px; }

.sl-rate { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.sl-rate b { font-size: 14.5px; font-variant-numeric: tabular-nums; }
.sl-rate small { color: var(--ink-3); font-size: 12px; }
.sl-state, .il-state { flex: 0 0 auto; }
.sl-owed { flex: 0 0 auto; font-size: 13.5px; font-variant-numeric: tabular-nums; }

.il-num { flex: 0 0 auto; font-size: 12.5px; color: var(--ink-3); }
.il-amt { flex: 0 0 auto; font-size: 14.5px; font-variant-numeric: tabular-nums; }
.inv-line.dead { opacity: .55; }

/* Small figures in a modal — four facts across the top of it. */
.mini-stats, .inv-hero {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
  margin-top: 6px;
}
.mini-stats > div, .inv-hero > div {
  background: var(--cream-2); border-radius: var(--r-sm); padding: 10px 12px;
  display: flex; flex-direction: column; gap: 3px;
}
.mini-stats span, .inv-hero span {
  font-size: 11px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--ink-4);
}
.mini-stats b, .inv-hero b { font-size: 15px; font-variant-numeric: tabular-nums; }

.doc-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.doc-row .btn { text-decoration: none; }
.inv-docs { display: flex; gap: 6px; flex-wrap: wrap; }
.inv-docs .btn { text-decoration: none; }

@media (max-width: 640px) {
  .shop-line, .inv-line { flex-wrap: wrap; gap: 8px 12px; }
  .sl-main, .il-who { flex: 1 1 100%; }
  .mini-stats, .inv-hero { grid-template-columns: repeat(2, 1fr); }
}

/* --- billing: the records and the year's figures ---------------------
   A card's own header row: its title on the left, the years or the count
   on the right, so the controls that change what the card shows sit on
   the card rather than floating above it. */
.rec-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.year-tabs { display: flex; gap: 6px; flex-wrap: wrap; }
.rec-status { display: flex; gap: 6px; flex-wrap: wrap; align-self: flex-end; }
.rec-status .chip.on, .year-tabs .chip.on { background: var(--ink); border-color: var(--ink); color: #fff; }
.rec-status .chip.on::before, .year-tabs .chip.on::before { content: none; }

/* Each month of the ledger is its own block with its own subtotal, so a
   year of invoices reads as twelve short lists rather than one long one. */
.rec-month { margin-top: 18px; }
.rec-month-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  padding: 0 2px 7px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 2px;
}
.rec-month-head b { font-family: var(--display); font-size: 15px; }
.rec-month-head .muted { font-size: 12.5px; }
.rec-month .bill-table { margin-top: 0; }

/* The stat tiles inside a card are one step quieter than the page's own,
   which carry the headline numbers. */
.card .card.stat.sub-stat { background: var(--cream-2); box-shadow: none; }
.bill-table tfoot td { border-top: 2px solid var(--line); padding-top: 10px; font-weight: 600; }

.bill-bar { display: flex; flex-wrap: wrap; gap: 14px 22px; align-items: flex-end; }
.bill-bar label.inline { display: flex; flex-direction: column; gap: 5px; font-size: 13px; font-weight: var(--w-name); color: var(--ink-3); }
.bill-bar select, .bill-bar input {
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--r-sm);
  font: inherit; background: var(--card); color: var(--ink);
}

/* Where an invoice stands, in one word. Overdue is amber rather than red:
   a shop a week late is a phone call, not an emergency. */
.inv-chip {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: var(--w-name);
  border: 1px solid transparent;
  white-space: nowrap;
}
.inv-chip.paid   { background: var(--green-tint); color: var(--green);  border-color: #BFE3D6; }
.inv-chip.unpaid { background: var(--cream-2);    color: var(--ink-3);  border-color: var(--line); }
.inv-chip.over   { background: var(--amber-tint); color: var(--amber);  border-color: #EFD9AE; }
.inv-chip.void   { background: var(--red-tint);   color: var(--red-deep); border-color: var(--red-line); }

/* A cancelled invoice stays readable but stops competing for attention. */
.inv-dead { opacity: .55; }
.inv-dead td:not(:first-child) { text-decoration: line-through; }

.owed-cell { color: var(--amber); font-weight: var(--w-name); }
.mono { font-variant-numeric: tabular-nums; letter-spacing: .01em; }

/* The second colour in the billing chart, and its key. */
.rc-bar.owed { fill: var(--amber); opacity: .55; }
.rc-key { display: inline-block; width: 10px; height: 10px; border-radius: 3px; background: var(--accent); margin-right: 6px; }
.rc-key.owed { background: var(--amber); opacity: .55; }

/* The barber's own list. One line per invoice on a laptop, stacked on a
   phone — he has three or four of these, not a ledger. */
.inv-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 0;
  border-bottom: 1px solid var(--line-soft);
}
.inv-row:last-child { border-bottom: 0; }
.inv-when { flex: 1; display: flex; flex-direction: column; gap: 2px; }
.inv-when .mono { font-size: 12.5px; }
.inv-amt { font-family: var(--display); font-weight: 600; font-size: 17px; }
.inv-state { text-align: right; min-width: 96px; }

@media (max-width: 560px) {
  .inv-row { flex-wrap: wrap; }
  .inv-when { flex: 1 0 100%; }
  .inv-amt { flex: 1; }
  .inv-state { min-width: 0; }
}

/* ============================================================
   No zoom inside the app
   ============================================================ */

/* The viewport meta does most of this; touch-action finishes the job by
   killing the double-tap-to-zoom gesture, which survives user-scalable=no
   in some Android browsers. Scoped to the app's own shells rather than
   body, because this stylesheet is shared with the client's booking page
   and a stranger reading that on his phone may well need to zoom. */
#app, #auth-screen { touch-action: manipulation; }

/* Rotating the phone must not resize the text either — a barber who turns
   the phone sideways to read a long client note should get the same app,
   wider, not the same app at 1.3x. */
#app, #auth-screen { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* The billing-day field is narrower than the price beside it — two digits,
   never more, and sitting it in the same width read as a second price. */
.day-in { width: 62px; }

/* ============================================================
   The map listing, in Settings → Shopfront.

   Set apart from the fields above it with the same hairline the booking
   colour picker uses, because it is a different kind of decision: every
   other control on that panel changes what a client sees once he has the
   link, this one decides whether strangers can find the shop at all.
   ============================================================ */
.map-listing {
  display: flex;
  flex-direction: column;
  margin-top: 20px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}
.map-listing > b { font-family: 'Outfit', sans-serif; font-size: 0.98rem; }
.map-listing > .muted { font-size: 0.85rem; margin-top: 4px; line-height: 1.45; }

/* Tall enough to place a pin on a street and see which street it is.
   Any shorter and a barber is dragging a marker around a keyhole. */
.pin-map {
  height: 260px;
  border-radius: 12px;
  border: 1px solid var(--line);
  overflow: hidden;
  background: var(--cream-2);
}
.pin-map .leaflet-container { font: inherit; background: var(--cream-2); }

@media (min-width: 700px) {
  .pin-map { height: 320px; }
}

/* ---- the last step of the setup wizard --------------------------------
   His booking link, big enough to read across a counter and selectable, so
   the phone that will not write to the clipboard can still be pressed and
   held. word-break because the link is one unbroken token and a shop with a
   long name would otherwise push the panel sideways. */
.w-link {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 14.5px; line-height: 1.5;
  padding: 13px 14px; border-radius: 12px;
  background: var(--accent-tint); border: 1px solid var(--accent-line);
  color: var(--ink); word-break: break-all;
  -webkit-user-select: all; user-select: all;
}
/* Where to put it. A list rather than a paragraph because he is going to
   work down it with a thumb, one app at a time. */
.w-where { margin: 16px 0 12px; padding-left: 18px; }
.w-where li { font-size: 14px; color: var(--ink-2); line-height: 1.55; margin-bottom: 7px; }

/* ============================================================
   The booking-page walkthrough (demo shops only)
   ============================================================ */

/* Same bubble as the tour inside the app, so a barber who has just been
   led here recognises the voice. Fixed to the foot of the screen rather
   than positioned against a control: this page is a single column and the
   thing being described is always the block he is looking at. */
/* Centred, over a dimmed page.

   At the foot of the screen this landed squarely on the Send button — the
   one control the last step is telling him to press. Rather than dodge it,
   the bubble stops competing for the page at all: it sits in the middle,
   the page behind it dims and stops scrolling, and he reads it and
   dismisses it before touching anything. One thing at a time.

   The scrim is a sibling element so a tap anywhere outside also closes
   it — on a phone that is the gesture people try first. */
.bk-scrim {
  position: fixed; inset: 0; z-index: 59;
  background: rgba(16, 16, 18, .34);
  -webkit-backdrop-filter: blur(1.5px); backdrop-filter: blur(1.5px);
  animation: fadeIn .16s var(--ease) both;
}
/* Nothing behind it moves while it is up. */
body.bk-guiding { overflow: hidden; }

.bk-guide {
  position: fixed;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 60;
  width: min(400px, calc(100vw - 36px));
  padding: 18px 44px 18px 18px;
  background: var(--msg, #E7F1FF);
  color: var(--msg-ink, #10233A);
  border-radius: 20px;
  box-shadow: 0 4px 10px rgba(16,16,18,.06), 0 24px 60px -14px rgba(18,40,61,.45);
  animation: bkPop .18s var(--ease) both;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes bkPop {
  from { opacity: 0; transform: translate(-50%, -50%) scale(.95); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .bk-guide, .bk-scrim { animation: none; }
}
/* A plain "got it" is the way out — the × alone is easy to miss when the
   bubble is the only thing on the screen. */
.bk-guide-go {
  margin-top: 14px; width: 100%;
  height: 42px; border: 0; border-radius: 12px;
  background: var(--ink, #101012); color: #fff;
  font-family: inherit; font-size: 15px; font-weight: 600;
  cursor: pointer;
}
/* The rep's face and her words typing in — the same treatment the
   walkthrough inside the app uses, for the same reason: this page is the
   middle of that walkthrough, not a different thing. See repFaceHtml() and
   typeGuide() in book.js. */
.bk-face {
  position: absolute;
  top: -13px; left: -11px;
  width: 38px; height: 38px;
  border-radius: 50%;
  overflow: hidden;
  display: grid; place-items: center;
  background: var(--accent-soft, #dce9ff);
  color: var(--accent, #0A7CFF);
  box-shadow: 0 0 0 2.5px #fff, 0 3px 9px -2px rgba(18,40,61,.34);
  font-weight: 700; font-size: 16px;
  font-family: var(--display);
  user-select: none;
}
.bk-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bk-guide .bk-guide-step { padding-left: 26px; }
/* The landing and confirmation bubbles have no step counter, so the heading
   is the first line and the circle would sit on it. */
.bk-guide > b:first-of-type { padding-left: 26px; }
.bk-guide .bk-guide-step + b { padding-left: 0; }
.bk-guide p { min-height: 1.45em; }
.bk-guide.typing p::after {
  content: '···';
  letter-spacing: 2px;
  opacity: .5;
  animation: tourDots 1s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .bk-guide.typing p::after { content: none; }
}

.bk-guide b { display: block; font-size: 15.5px; margin-bottom: 2px; font-family: var(--display); }
.bk-guide p { margin: 0; font-size: 13.5px; line-height: 1.45; color: var(--msg-ink-2, #3D5A76); }
.bk-guide-step {
  font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
  opacity: .55; margin-bottom: 3px;
}
.bk-guide-x {
  position: absolute; top: 6px; right: 6px;
  width: 30px; height: 30px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: 50%;
  background: transparent; color: inherit;
  cursor: pointer; opacity: .55;
}
.bk-guide-x:hover { opacity: 1; background: rgba(16,16,18,.06); }
.bk-guide-x svg { width: 14px; height: 14px; }

/* Help, on the client booking page during a demo.

   Bottom-left, opposite the "book now" float that owns the right, and
   below the guide bubble in the stacking order so it never sits on top of
   an instruction. Only rendered when a rep was actually resolved. */
.bk-help {
  position: fixed;
  left: 14px;
  /* Lifted clear of the service step's confirm bar.

     The bar is pinned to the foot of the screen while a client is choosing
     services, and Help sat squarely on top of "Press to confirm" — two
     controls in the same place, one of them covering the one the page is
     asking him to press. body.svc-picking is on only while that bar is up,
     so Help steps above it for exactly as long as it needs to and sits back
     down afterwards. */
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 55;
  display: inline-flex; align-items: center; gap: 7px;
  height: 40px; padding: 0 15px;
  border: 1px solid var(--line, #E6E4DF);
  border-radius: 100px;
  background: var(--card, #fff);
  color: var(--ink, #101012);
  font-family: inherit; font-size: 14px; font-weight: 600;
  cursor: pointer;
  box-shadow: 0 6px 18px -6px rgba(16,16,18,.28);
}
body.svc-picking .bk-help {
  bottom: calc(142px + env(safe-area-inset-bottom, 0px));
}
/* The way back into the app, for a barber who opened his own booking page
   from setup. Bottom-right, opposite Help, and it stays put for the whole
   visit — see appReturnHtml() in book.js. Dark, because unlike Help it is
   the thing he is most likely to want next. */
.bk-return {
  position: fixed;
  right: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 55;
  display: inline-flex; align-items: center; gap: 7px;
  height: 40px; padding: 0 15px;
  border-radius: 100px;
  background: var(--ink, #101012);
  color: #fff;
  text-decoration: none;
  font-family: inherit; font-size: 14px; font-weight: 600;
  box-shadow: 0 6px 18px -6px rgba(16,16,18,.45);
}
.bk-return svg { flex: none; opacity: .9; }
/* Clear of the service step's confirm bar, exactly as Help is. */
body.svc-picking .bk-return {
  bottom: calc(142px + env(safe-area-inset-bottom, 0px));
}

.bk-help:hover { border-color: var(--ink, #101012); }
.bk-help svg { flex: none; opacity: .8; }

/* The back button, lit for a couple of seconds after the bubble sends him
   to it — so a smooth scroll that ends mid-page still says which control
   it was about. */
@keyframes bkLit {
  0%, 100% { box-shadow: 0 0 0 0 rgba(10, 124, 255, 0); }
  35%      { box-shadow: 0 0 0 7px rgba(10, 124, 255, .28); }
}
.bk-back-lit { animation: bkLit 1.1s ease-in-out 2; }

/* ============================================================
   The admin's menu — admin work only
   ============================================================ */

/* The admin account has no chair, no clients and no takings, so the shop's
   own pages are hidden rather than shown as empty shells. Driven by a body
   class set in showApp(), and lifted the moment he steps into a shop
   through Admin — at that point he is genuinely looking at a barber's app
   and needs all of it.

   Done by selector rather than by deleting the markup, so the same index
   .html serves both and nothing has to be rebuilt when he impersonates. */
body.admin-only .sidebar nav a[data-nav="dashboard"],
body.admin-only .sidebar nav a[data-nav="requests"],
body.admin-only .sidebar nav a[data-nav="calendar"],
body.admin-only .sidebar nav a[data-nav="clients"],
body.admin-only .sidebar nav a[data-nav="services"],
body.admin-only .sidebar nav a[data-nav="reminders"],
body.admin-only .sidebar nav a[data-nav="reviews"],
body.admin-only .sidebar nav a[data-nav="team"],
body.admin-only .sidebar nav a[data-nav="payments"],
body.admin-only .sidebar nav a[data-nav="reports"],
body.admin-only .sidebar nav a[data-nav="supplies"],
body.admin-only .sidebar nav a[data-nav="invoices"] { display: none !important; }

/* "Your work" and "Money" now head empty groups, so the groups go too.
   Marked in the markup rather than inferred with :has(), which would have
   to reason about the rules above it and is the kind of selector that
   quietly stops matching when somebody adds a link. */
body.admin-only .sidebar nav .nav-group[data-group="work"],
body.admin-only .sidebar nav .nav-group[data-group="money"] { display: none; }

/* The quick actions on the phone bar are a barber's, not the admin's. */
body.admin-only .mobile-bar .mbar-act { display: none; }

/* The section a service belongs to, on the barber's own Services table.
   Quiet and beside the name rather than a column of its own: most shops
   will not use sections at all, and an empty column on every row is a
   worse cost than a small label on the rows that have one. */
.svc-cat {
  display: block;
  font-style: normal;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-mute, #8a8a86);
  margin-top: 1px;
}

/* ==================================================================
   THE DEMO — the rep, and the two places she stands.

   She has a card and a panel, and which one is on screen says what is
   being asked of him.

   HER CARD is centred over a dimmed app, and it is where every step
   begins: the screen's own name, then what it does, then Next. Nothing
   else is competing for the page — the same arrangement, and the same
   blue, as the guide on the booking page, because to him it is one person
   talking either side of that link.

   HER PANEL is at the foot of the screen, and it is where the step is
   carried out: her words stay put while the app does the thing she just
   described, out of the way of whatever she is pointing at.

   While either is up, the app underneath does not take presses — the step
   performs its own — so a tap out there nudges her instead. That is the
   difference between "not that, this" and a screen that appears broken.
   ================================================================== */
#film-scrim {
  position: fixed; inset: 0; z-index: 101;
  background: rgba(16, 16, 18, .34);
  -webkit-backdrop-filter: blur(1.5px); backdrop-filter: blur(1.5px);
  opacity: 0; pointer-events: none;
  transition: opacity .18s var(--ease, ease);
}
body.film-telling #film-scrim { opacity: 1; }

#film-card {
  position: fixed; left: 50%; top: 50%;
  transform: translate(-50%, -50%) scale(.96);
  z-index: 103;
  width: min(400px, calc(100vw - 36px));
  padding: 20px 20px 18px;
  background: var(--msg, #E7F1FF);
  color: var(--msg-ink, #10233A);
  border-radius: 20px;
  box-shadow: 0 4px 10px rgba(16,16,18,.06), 0 24px 60px -14px rgba(18,40,61,.45);
  opacity: 0; pointer-events: none;
  transition: opacity .18s var(--ease, ease), transform .18s var(--ease, ease);
}
body.film-telling #film-card {
  opacity: 1; pointer-events: auto;
  transform: translate(-50%, -50%) scale(1);
}
#film-card .film-face {
  position: absolute; top: -13px; left: -11px;
  width: 38px; height: 38px; border-radius: 50%; overflow: hidden;
  display: grid; place-items: center;
  background: var(--accent-soft, #dce9ff); color: var(--accent, #0A7CFF);
  box-shadow: 0 0 0 2.5px #fff, 0 3px 9px -2px rgba(18,40,61,.34);
  font-weight: 700; font-size: 16px; font-family: var(--display);
}
#film-card .film-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The step count sits on the first line, clear of her face. */
#film-card .film-step {
  padding-left: 26px; font-size: 12px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--msg-ink-2, #3D5A76); opacity: .8;
}
/* The screen's name, which is the whole point of the card's first line. */
#film-card b {
  display: block; margin-top: 4px; margin-bottom: 4px;
  font-family: var(--display); font-size: 18px; letter-spacing: -.01em;
}
#film-card p {
  margin: 0; font-size: 14.5px; line-height: 1.5;
  color: var(--msg-ink-2, #3D5A76);
  min-height: 3em;                 /* held open, so it cannot jump as she types */
}
/* The door, where a step has one: the bright thing on her card, above
   Next, because walking through it is what the card is asking for. */
#film-card .film-card-door {
  display: block; margin-top: 14px;
  padding: 13px 16px; border-radius: 12px;
  background: var(--accent, #0A7CFF); color: #fff;
  font-family: inherit; font-size: 15px; font-weight: 600;
  text-align: center; text-decoration: none;
}
#film-card .film-card-door.hidden { display: none; }
/* With a door on the card, Next is the quieter of the two until he has
   been through it — and it says so by being plainly unavailable rather
   than by disappearing. */
#film-card .film-go.waiting {
  background: transparent; color: var(--msg-ink-2, #3D5A76);
  box-shadow: inset 0 0 0 1px rgba(61, 90, 118, .3);
  cursor: default;
}
#film-card .film-go {
  margin-top: 16px; width: 100%; height: 44px;
  border: 0; border-radius: 12px;
  background: var(--ink, #101012); color: #fff;
  font-family: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
}
#film-card .film-quit {
  display: block; margin: 8px auto 0; padding: 6px 10px;
  background: none; border: 0; cursor: pointer;
  font-family: inherit; font-size: 13px;
  color: var(--msg-ink-2, #3D5A76); opacity: .75;
}

/* Above the modal backdrop (z-index 100), not below it: she narrates the
   app's own panels as much as its pages — "approving wrote this" is said
   while the message is on screen — and underneath the dim she was greyed
   out at exactly the moments the demo makes its case. */
#film {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 102;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  background: rgba(27, 27, 26, .95);
  -webkit-backdrop-filter: saturate(1.6) blur(10px);
  backdrop-filter: saturate(1.6) blur(10px);
  color: #fff;
  transform: translateY(0);
  transition: transform .2s var(--ease, ease), opacity .2s var(--ease, ease);
}
/* Out of the way while her card has the screen. */
body.film-telling #film { transform: translateY(105%); opacity: 0; }

#film .film-prog {
  height: 2px; border-radius: 2px; background: rgba(255, 255, 255, .22);
  overflow: hidden; margin-bottom: 12px;
}
#film .film-prog i {
  display: block; height: 100%; width: 0;
  background: #fff; border-radius: 2px;
  transition: width .4s cubic-bezier(.2, .8, .2, 1);
}
#film .film-row { display: flex; align-items: flex-start; gap: 11px; }
#film .film-face-sm {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  overflow: hidden; background: rgba(255, 255, 255, .16);
  display: grid; place-items: center;
  font-family: var(--display); font-weight: 600; font-size: 15px;
}
#film .film-face-sm img { width: 100%; height: 100%; object-fit: cover; }
#film .film-say {
  flex: 1; min-width: 0; margin: 0;
  font-size: 15px; line-height: 1.45; letter-spacing: -.005em;
}
#film .film-acts { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
#film .film-quit {
  font-family: inherit; font-size: 13px; font-weight: 500;
  background: none; border: 0; padding: 6px 0; cursor: pointer;
  color: rgba(255, 255, 255, .6);
}
#film .film-count {
  font-size: 12.5px; color: rgba(255, 255, 255, .5);
  margin-left: auto; letter-spacing: .02em;
}
#film .film-next {
  flex: none; font-family: inherit; font-size: 14.5px; font-weight: 600;
  padding: 10px 20px; border-radius: 100px; cursor: pointer;
  border: 0; background: #fff; color: #1B1B1A;
}
/* A door he has not walked through yet. Next is visibly waiting on it
   rather than missing — a control that vanishes reads as a bug, and one
   that is plainly disabled reads as an instruction. */
#film .film-next.waiting {
  background: transparent; color: rgba(255, 255, 255, .4);
  border: 1px solid rgba(255, 255, 255, .25); cursor: default;
}

/* A press that landed on the app rather than on her. */
@keyframes filmNudge {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}
#film.nudging, #film-card.nudging { animation: filmNudge .26s ease both; }
#film-card.nudging { animation-name: filmNudgeC; }
@keyframes filmNudgeC {
  0%, 100% { transform: translate(-50%, -50%); }
  25% { transform: translate(calc(-50% - 5px), -50%); }
  75% { transform: translate(calc(-50% + 5px), -50%); }
}
@media (prefers-reduced-motion: reduce) {
  #film.nudging, #film-card.nudging { animation: none; }
}

/* THE ARROW the step is about to press with.

   Over the page and never in it, so nothing shifts when it appears; it
   fades in where it belongs rather than sliding in from the corner (see
   point() in app.js), and only glides when it moves from one control to
   the next, which is the one time the movement means something.

   The bob is a transform on a composited element — a pulsing shadow was
   what made the old ring stutter on the phones this is handed to. */
/* ABOVE EVERYTHING, INCLUDING THE APP'S OWN PANELS.

   At z-index 99 it sat under the modal backdrop (100), so every press
   inside a panel — recording the payment, closing the thank-you — was
   pointed at by an arrow nobody could see. That is the "no arrow after
   finishing the appointment": the arrow was there, behind the panel it
   was pointing into. */
#film-arrow {
  position: fixed; z-index: 104; pointer-events: none;
  width: 42px; height: 42px;
  display: grid; place-items: center;
  color: #fff;
  border-radius: 50%;
  /* Blue, and written out rather than taken from --accent: the app's
     accent is #101012, a near-black, so "use the accent" produced exactly
     the dark disc this was meant to stop being. Nothing else in the app is
     this colour, which is the point — it means "here", and only here. */
  background: #0A7CFF;
  box-shadow: 0 6px 20px -4px rgba(10, 124, 255, .55);
  opacity: 0; transform: scale(.86);
  transition: opacity .2s var(--ease, ease), transform .2s var(--ease, ease),
              top .34s cubic-bezier(.2, .8, .2, 1), left .34s cubic-bezier(.2, .8, .2, 1);
  will-change: opacity, transform;
}
#film-arrow svg { width: 22px; height: 22px; }
#film-arrow.from-below svg { transform: rotate(180deg); }
#film-arrow.on { opacity: 1; transform: none; }
@media (prefers-reduced-motion: no-preference) {
  #film-arrow.on { animation: filmBob 1.25s ease-in-out infinite; }
  #film-arrow.on.from-below { animation-name: filmBobUp; }
}
@keyframes filmBob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(6px); }
}
@keyframes filmBobUp {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}

/* The way back in, after Skip. Bottom-right, out of the thumb's way, her
   face on it so it is plainly the walkthrough offering to carry on rather
   than a control belonging to the app. */
#film-resume {
  position: fixed; right: 14px; z-index: 96;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; gap: 9px;
  padding: 8px 15px 8px 8px; border: 0; border-radius: 100px;
  background: var(--ink, #1B1B1A); color: #fff;
  font-family: inherit; font-size: 14px; font-weight: 600;
  box-shadow: 0 10px 26px -10px rgba(27, 27, 26, .6);
  cursor: pointer;
}
#film-resume .film-face-sm {
  width: 28px; height: 28px; border-radius: 50%; overflow: hidden;
  background: rgba(255, 255, 255, .16);
  display: grid; place-items: center; font-size: 13px;
}
#film-resume .film-face-sm img { width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 860px) {
  #film-resume { bottom: calc(72px + env(safe-area-inset-bottom, 0px)); }
}

/* NO DOUBLE-TAP ZOOM ON THE THINGS SHE ASKS HIM TO PRESS.

   The app's own page turns off zooming in its viewport tag; the booking
   page does not, and should not — a client reading a price list is
   entitled to pinch it bigger. But double-tap-to-zoom also fires on two
   quick taps in the same place, which is exactly what pressing Next twice
   looks like, and the page would jump to 200% for no reason anybody could
   explain. touch-action: manipulation turns off the double-tap gesture on
   these controls alone and leaves pinch-zoom alone everywhere. */
#film button, #film-card button, #film-card a,
button, .btn { touch-action: manipulation; }

/* THE DEMO BAR GOES, AND SO DOES THE ROOM IT WAS KEEPING.

   The bar at the top of a demo is a second black strip saying a second
   thing, so it is hidden while she is talking. Hiding it was only half the
   job: the app reserves 40-odd pixels at the top for it (--imp-h, on
   .layout, the sidebar and the mobile bar), and that reservation stayed
   behind as an empty band — which is what the menu appeared to slide down
   from, and what left a gap above the header on every screen of the
   walkthrough.

   Anything that offsets itself for that bar has to be put back while the
   bar is not there. Same rule, same variable, three places, exactly as the
   originals are written. */
body.filming #demo-bar { display: none; }
body.filming { padding-bottom: 0; }
body.filming.in-demo .layout { padding-top: 0; }
body.filming.in-demo .sidebar { top: 0; height: 100dvh; }
body.filming.in-demo .mobile-bar { top: 0; }

/* "See my booking page", at the top of Your shopfront.

   Everything else on that panel changes a page the barber never opens.
   This is the one control that shows him the result, so it is drawn like a
   destination rather than like a setting — the same dark, lifted shape the
   app gives its primary actions. */
.front-see {
  display: flex; align-items: center; gap: 14px;
  margin-bottom: 20px; padding: 15px 16px;
  border-radius: var(--r-lg, 16px);
  background: var(--ink, #1B1B1A); color: #fff;
  text-decoration: none;
  box-shadow: 0 10px 26px -12px rgba(27, 27, 26, .55);
  transition: transform var(--t-fast, .12s) var(--ease, ease);
}
.front-see:active { transform: scale(.99); }
.front-see-ic {
  flex: none; width: 40px; height: 40px; border-radius: 12px;
  background: #fff; color: var(--ink, #1B1B1A);
  display: grid; place-items: center;
}
.front-see-ic svg { width: 20px; height: 20px; }
.front-see-tx { flex: 1; min-width: 0; }
.front-see-tx b { display: block; font-family: var(--display); font-size: 15.5px; font-weight: 600; }
.front-see-tx small { display: block; margin-top: 2px; font-size: 13px; color: rgba(255, 255, 255, .72); }
.front-see-go { flex: none; color: rgba(255, 255, 255, .8); display: grid; place-items: center; }

/* During the walkthrough her panel owns the foot of the screen; a toast
   down there landed on top of her words. Lift it clear. */
body.filming:not(.film-telling) .toast { bottom: 150px; }

/* Settings › Guide */
.guide-list { display: grid; gap: 8px; }
.guide-sec { border: 1px solid var(--line); border-radius: 12px; background: var(--card, transparent); }
.guide-sec > summary { cursor: pointer; padding: 13px 16px; font-weight: 600; list-style: none; display: flex; justify-content: space-between; align-items: center; }
.guide-sec > summary::-webkit-details-marker { display: none; }
.guide-sec > summary::after { content: '+'; font-weight: 500; opacity: .6; }
.guide-sec[open] > summary::after { content: '–'; }
.guide-sec ol { margin: 0; padding: 0 18px 14px 36px; display: grid; gap: 8px; line-height: 1.5; }

/* Room below the last row during the walkthrough, so a booking at the end
   of the day can scroll up clear of her panel for the arrow to point at. */
body.filming #main { padding-bottom: 200px; }

/* Today › Get your shop ready */
.onboard { margin-bottom: 14px; }
.onboard .ob-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.onboard .ob-head h2 { margin: 0 0 2px; }
.onboard .ob-head p { margin: 0; font-size: 13px; }
.onboard .ob-bar { height: 6px; border-radius: 6px; background: var(--line); margin: 12px 0 8px; overflow: hidden; }
.onboard .ob-bar i { display: block; height: 100%; background: var(--good, #1f7a4d); border-radius: 6px; transition: width .3s ease; }
.onboard .ob-list { list-style: none; margin: 0; padding: 0; }
.onboard .ob-list li button {
  width: 100%; display: flex; align-items: center; gap: 12px; padding: 12px 2px;
  background: none; border: 0; border-top: 1px solid var(--line); text-align: left;
  font: inherit; color: inherit; cursor: pointer;
}
.onboard .ob-list li:first-child button { border-top: 0; }
.onboard .ob-tick { flex: none; width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--ink-3, #999); position: relative; }
.onboard .ob-list li.done .ob-tick { background: var(--good, #1f7a4d); border-color: var(--good, #1f7a4d); }
.onboard .ob-list li.done .ob-tick::after {
  content: ''; position: absolute; left: 6px; top: 2px; width: 6px; height: 11px;
  border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.onboard .ob-list li.done .ob-label { color: var(--ink-3, #888); text-decoration: line-through; }
.onboard .ob-label { flex: 1; font-weight: 600; }
.onboard .ob-go { font-size: 20px; opacity: .5; }

/* The share kit */
.kit-img canvas { width: 100%; max-width: 320px; height: auto; display: block; margin: 0 auto; border-radius: 12px; }
.kit-text { margin-top: 12px; }
.kit-text b { display: block; margin-bottom: 6px; }
.kit-text .btn { margin-top: 6px; }

/* ------------------------------------------------------------------
   THE WHITE BUTTONS, MADE VISIBLE.
   A white button with a #E8E8EB hairline on a white card was close to
   invisible — Settings, Hide on Today, Preview, Show QR all read as plain
   text with a faint box round it, and on a phone in a bright shop not even
   that. They now have a real edge, a slight lift off the page, and dark
   text; ghost buttons get a soft grey fill instead of nothing at all.
   ------------------------------------------------------------------ */
.btn {
  border-color: #C9C9D1;
  box-shadow: 0 1px 0 rgba(16, 16, 18, .06), 0 1px 3px rgba(16, 16, 18, .06);
}
.btn.ghost {
  background: var(--cream-2);
  border-color: #D4D4DB;
  color: var(--ink);
  box-shadow: none;
}
.btn.ghost:hover { background: #E9E9ED; border-color: #BDBDC6; }
.btn.primary, .btn.dark, .btn.wa { box-shadow: 0 6px 16px -8px rgba(16,16,18,.5); }
.btn.danger, .btn.good { box-shadow: none; }
.set-tabs button { border-color: #C9C9D1; color: var(--ink-2, #3A3A40); }

/* Settings tabs: a thin bar under the row showing how far across it is. */
.set-tabs-wrap { position: relative; margin-bottom: 16px; }
.set-tabs-wrap .set-tabs { margin-bottom: 0; }
.set-tabs-track {
  height: 3px; border-radius: 3px; background: var(--line);
  margin-top: 6px; overflow: hidden; position: relative;
}
.set-tabs-track i {
  position: absolute; top: 0; bottom: 0; left: 0; border-radius: 3px;
  background: var(--ink-3); transition: left .08s linear;
}
.set-tabs-track.hidden { display: none; }

/* Folded: heading and progress only. */
.onboard .ob-list { overflow: hidden; transition: max-height .25s ease, opacity .2s ease; max-height: 600px; }
.onboard.collapsed .ob-list { max-height: 0; opacity: 0; }
.onboard.collapsed .ob-bar { margin-bottom: 0; }

/* Message language switch — slim, above the message */
.lang-switch {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  background: #EEF6F3; border: 1px solid #BFE3D6; border-radius: 10px;
  padding: 5px 5px 5px 10px; margin: 0 0 8px;
}
.lang-switch .ls-label { font-size: 12.5px; font-weight: 700; color: #1f5e47; white-space: nowrap; }
.lang-switch .ls-seg { display: inline-flex; background: #fff; border: 1px solid #BFE3D6; border-radius: 999px; padding: 2px; flex: none; }
.lang-switch .ls-seg button {
  border: 0; background: none; font: inherit; font-size: 12.5px; font-weight: 700;
  padding: 4px 11px; border-radius: 999px; color: #3A3A40; cursor: pointer;
}
.lang-switch .ls-seg button.on { background: #1f7a4d; color: #fff; box-shadow: 0 2px 6px rgba(31,122,77,.35); }
.link-box.msg-text {
  font-family: var(--body); font-size: 14px; line-height: 1.45; padding: 10px 12px;
  white-space: pre-wrap; word-break: normal; overflow-wrap: anywhere;
}
.ls-saved { font-size: 12px; font-weight: 600; color: #1f7a4d; margin: 6px 0 0; }
.ls-saved.hidden { display: none; }
.msg-note { font-size: 12px; color: var(--ink-3); margin: 6px 0 0; line-height: 1.4; }
.lang-switch .ls-seg button { border: 0 !important; outline: none; -webkit-tap-highlight-color: transparent; }
.lang-switch .ls-seg button:not(.on) { box-shadow: none !important; }
.lang-switch .ls-seg button:focus-visible { outline: 2px solid #1f7a4d; outline-offset: 1px; }
