/* BeautyRaena dashboard — design system.

   REWORKED 8 September 2026 to the structure of the reference dashboard the
   client approved (tier1-tau.vercel.app). What was taken from it is the
   SYSTEM - the measurements, the rhythm, the way depth is spent - and nothing
   else. None of its numbers, none of its copy, and none of its claims: every
   figure on that page was typed in by hand for a business that does not exist.

   What changed, and why each one:

     - Type is DM Sans, as the reference is throughout. Cormorant Garamond
       stays for the wordmark and the editorial greeting, because that is our
       identity and the funnel already uses it.
     - Two shadows instead of one, so raising something means something.
     - Labels are 10px uppercase with real letter-spacing. Ours were 11.5px
       and read as small body text rather than as labels.
     - A gold rule across the top of a stat tile. It is the one place the
       reference spends colour on structure, and it is what makes a row of
       tiles read as a set.
     - Tables get the reference's padding and its 10px heads.

   The palette does NOT change. Ours and the reference's were already the same
   family - gold, green, a near-white ground, a 12px radius - which is why it
   read as ours to the client in the first place. */

:root{
  /* TAKEN FROM THE REFERENCE ITSELF, not from the build spec.
     ------------------------------------------------------------------------
     The spec's moss (#5f6a3e) and gold (#9c7c33) are both duller and browner
     than what is actually on screen there. Read off the reference:

       green  #3D6B35   the wordmark, a healthy figure, a filled progress bar
       gold   #C9A96E   rules, discs, badges - anything FILLED
       gold   #a8833f   the same gold as TEXT, because #C9A96E on white is
                        about 2:1 and unreadable

     Two golds is not indecision. A rule and a word need different values of the
     same colour to look like the same colour. */
  --ink:#1a1a1a; --ground:#FAFAF8; --surface:#FFFFFF; --surface-hover:#F5F4F0;
  --accent:#3D6B35; --gold:#a8833f; --gold-fill:#C9A96E;
  --gold-tint:#FAF6EE; --moss-fill:#E8F0E6;
  --gold-dim:rgba(201,169,110,.08); --gold-soft:#C9A96E;
  --border:#EAE7DE; --muted:#555555; --faint:#999999;
  --danger:#C0564F; --ok:#3D6B35;
  --radius:12px;
  --shadow:0 1px 3px rgba(20,20,15,.04);
  --shadow-md:0 4px 16px rgba(20,20,15,.07);
  --rail-w:248px;
  /* One number for the topbar's height, because three things depend on it: the
     bar itself, where the rail starts, and how tall the rail is. When they were
     three separate numbers the rail overflowed by exactly the height of the bar
     and its footer scrolled off the bottom. */
  --topbar-h:68px;
  --font-ui:'DM Sans',system-ui,-apple-system,sans-serif;
  /* ONE TYPEFACE, as the reference has. Its stylesheet declares DM Sans and
     nothing else - 33 font-family rules, all of them DM Sans - and the serif
     wordmark on its screen is not a font at all, it is a PNG:
     <img class="topbar-logo" src="brx-logo.png">.
     So the serif survives here for the wordmark only, which is a logotype
     standing in for that image, and nowhere else. Headings that were set in it
     are now DM Sans, because on the reference they are. */
  --font-editorial:'Cormorant Garamond',Georgia,serif;
}

/* ===========================================================================
   TYPE SCALE, 12 September 2026.
   Reported: "ayusin mo rin sizing ng mga letra sa buong website".
   There were TWENTY-TWO different font sizes in this stylesheet, six of them
   on half a pixel - 10.5, 11.5, 12.5, 13.5, 14.5, 15.5. That is not a scale,
   it is accretion: every screen picked a number that looked right next to the
   screen before it, and nobody ever picked the same one twice.
   Eight steps now, named by the JOB rather than the size. A name makes the
   next person choose a role; a number invites them to invent 13.5 again.
     micro    uppercase labels, badges, counts
     small    hints, meta, secondary text in tables
     body     body copy, table cells, buttons, navigation
     input    form fields, and body copy that needs to carry
     head     a card heading
     value    a figure inside a card
     title    a page title, a person's name, a headline figure
     display  a price, a hero number
   =========================================================================== */
:root {
  --t-micro:11px;
  --t-small:13px;
  --t-body:14px;
  --t-input:15px;
  --t-head:17px;
  --t-value:20px;
  --t-title:24px;
  --t-display:32px;
}

*{box-sizing:border-box;}
html,body{height:100%;}
/* THE BASE SIZE, declared at last on 12 September 2026. This rule set a family
   and a weight and no size, so the document ran at the browser's 16px and every
   component in the product had to name its own size to look right next to the
   one before it. That is where twenty-two sizes came from. With a base, a
   component only declares a size when it genuinely differs. */
body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--font-ui);
  font-size:var(--t-body);line-height:1.55;
  font-weight:400;-webkit-font-smoothing:antialiased;}
h1,h2,h3{font-family:var(--font-ui);font-weight:600;margin:0;}
h1.editorial{font-weight:700;letter-spacing:-.02em;}
a{color:inherit;}
.wordmark{font-family:var(--font-editorial);letter-spacing:.14em;text-transform:uppercase;
  font-size:var(--t-input);font-weight:600;}

/* ---- shell ---- */
/* THE PAGE DOES NOT SCROLL. The main column does.
   ---------------------------------------------------------------------------
   The rail was sticky inside a scrolling page, which is not the same thing: at
   the top of the page it sat flush, and the moment anything scrolled it lifted
   away from the bar above it and read as a floating panel.

   Now the shell is exactly one viewport tall and only the main column scrolls
   inside it. The rail cannot move, because there is no scroll under it to move
   with. This is also how the reference does it. */
.shell{display:flex;flex-direction:column;height:100vh;overflow:hidden;}
.shell>.body{display:flex;flex:1;min-height:0;}
/* The column to the right of the rail: the bar, then the scrolling page. */
.col{display:flex;flex-direction:column;flex:1;min-width:0;min-height:0;}

/* Sticky, and a fixed height. It was neither: it scrolled away, so on a long
   client list there was nothing on screen saying which account you were in. */
.page-topbar{display:flex;align-items:center;justify-content:space-between;
  padding:0 32px;min-height:var(--topbar-h);height:var(--topbar-h);flex:none;z-index:50;
  border-bottom:1px solid var(--border);background:var(--surface);flex:none;}
/* THE WORDMARK, and it is the only thing in the top bar on the left.
   It replaced a breadcrumb that read "BeautyRaena / Founder Dashboard" - the
   brand and the page name competing in the same line, at the same size, saying
   one thing the customer already knows and one the rail already shows.

   Moss rather than ink: it is the one place the brand speaks, so it is allowed
   to be the brand's colour. Wide letter-spacing because the mark is a word to
   be recognised, not a sentence to be read. */
.brandmark{display:block;line-height:0;text-decoration:none;
  transition:opacity .18s ease;}
/* Height, never width: the file is 1188 by 116, and letting the height lead
   keeps the mark the same size whatever the rail is doing. */
.brandmark img{height:clamp(22px,2.2vw,30px);width:auto;display:block;}
.brandmark:hover{opacity:.78;}

/* The greeting, in the rail. Two lines: the time of day, quiet, and the name,
   which is the part that matters. */
/* Level with the wordmark across the divide: the rail's top padding and the
   bar's height are set so the greeting and the brand sit on the same line. */
.rail-greet{padding:0 12px 26px;line-height:1.35;}
.rail-greet .g{display:block;font-size:var(--t-small);color:var(--muted);}
.rail-greet .n{display:block;font-size:var(--t-head);font-weight:600;color:var(--ink);
  letter-spacing:-.01em;}
.rail-collapsed .rail-greet{opacity:0;visibility:hidden;height:0;padding:0;overflow:hidden;}

.page-switch{display:flex;align-items:center;gap:8px;font-size:var(--t-body);font-weight:600;}
.page-switch .chev{color:var(--muted);font-size:var(--t-micro);}
.top-actions{display:flex;align-items:center;gap:12px;}
.pill{display:flex;align-items:center;gap:6px;border:1px solid var(--border);
  background:var(--surface);border-radius:999px;
  padding:8px 14px;font-size:var(--t-small);color:var(--muted);white-space:nowrap;}
.pill svg.ic{width:14px;height:14px;color:var(--muted);}
.bell{position:relative;width:38px;height:38px;border-radius:50%;background:var(--surface);cursor:pointer;
  border:1px solid var(--border);display:flex;align-items:center;justify-content:center;}
.bell .dot{position:absolute;top:6px;right:6px;width:8px;height:8px;border-radius:50%;
  background:var(--danger);}
.avatar{width:38px;height:38px;border-radius:50%;flex:none;cursor:pointer;
  background:var(--gold-tint);color:var(--gold);border:1px solid var(--border);
  display:flex;align-items:center;justify-content:center;
  font-weight:600;font-size:var(--t-small);letter-spacing:.02em;user-select:none;}
.avatar:hover{border-color:var(--gold);}

svg.ic{width:16px;height:16px;flex:none;vertical-align:-3px;}
.bell svg.ic{width:17px;height:17px;color:var(--muted);}

/* ---- rail ---- */
/* Sticks BELOW the topbar and is exactly as tall as what is left.
   It was 100vh starting at 0 while a 68px bar sat above it, so it overran the
   viewport by the height of that bar - the brand mark and Settings fell off the
   bottom and the whole menu scrolled with the page.

   overflow-x is hidden because a nav item one pixel too wide put a horizontal
   scrollbar across the foot of the menu. */
.rail{width:var(--rail-w);flex:0 0 var(--rail-w);background:var(--surface);
  border-right:1px solid var(--border);
  display:flex;flex-direction:column;padding:26px 16px 12px;
  height:100%;overflow:visible;}

/* ---- THE COLLAPSE CONTROL WAS UNCLICKABLE ---------------------------------
   The third fault behind "the side dashboards don't close correctly", and the
   one that made it permanent. The control straddles the rail's right border by
   design, half of it overhanging - `right:-14px`. The rail was the scroll
   container (`overflow-y:auto`, and `overflow-x:hidden` to stop one wide nav
   item putting a scrollbar across the foot of the menu), and a scroll
   container CLIPS its overflow. So the overhanging half was cut away, and a
   hit test at the button's own centre returned the rail rather than the
   button. Measured, not assumed: the button occupied x 61-89, the rail ended
   at 76.

   What that meant for Ms. Ashley: she could collapse the menu, and then there
   was no control to open it again. Collapsed was a one-way trip, and the
   clipped two-letter labels she screenshotted were a state she could not leave.

   The scrolling moves to `.nav`, which is what actually needed it - a rail
   with more items than height should scroll its list, not its greeting and its
   name. The rail itself can overflow again, so the control is whole and
   clickable. `overflow-x:hidden` stays with the list, where the wide nav item
   that caused it lives. */
.rail .wordmark{padding:0 10px 26px;}
.nav{display:flex;flex-direction:column;gap:2px;flex:1;
  overflow-y:auto;overflow-x:hidden;}
/* Left border on every item, transparent until active. Declared here rather
   than only on .active so the label does not shift sideways when it becomes
   the current page. */
.nav a{display:flex;align-items:center;gap:12px;
  padding:11px 14px;border-radius:10px;border-left:3px solid transparent;
  color:var(--muted);text-decoration:none;font-size:var(--t-body);font-weight:500;
  white-space:nowrap;overflow:hidden;
  transition:background-color .18s ease,color .18s ease;}
/* 20px, the reference's size, at its stroke of 2. */
.nav a .nav-icon{width:20px;height:20px;flex:0 0 20px;}
.nav a:hover{background:var(--surface-hover);color:var(--ink);}
.nav a.active{background:var(--gold-dim);color:var(--gold);
  border-left-color:var(--gold-fill);font-weight:600;}
.nav a.active .nav-icon{color:var(--gold);}
/* A filled gold disc with a white number, not a moss pill. Unread is the one
   thing in the menu asking to be acted on, and gold is this product's colour
   for act-on-this. Fixed size so a 3 and a 12 are the same shape. */
/* A padlock and a price, in the menu item itself.
   The feature stays visible rather than being hidden: hiding it means the
   customer never learns it exists, and a menu that quietly differs per plan is
   impossible to support over the phone. */
.rail-label.lock{display:inline-flex;align-items:center;gap:6px;margin-left:6px;}
.rail-label.lock svg.ic{width:13px;height:13px;color:var(--faint);}
.rail-label.lock .price{font-size:var(--t-micro);font-weight:600;letter-spacing:.02em;
  color:var(--gold);background:var(--gold-dim);border-radius:999px;padding:2px 7px;
  white-space:nowrap;}

.nav a .badge{margin-left:auto;background:var(--gold-fill);color:#fff;font-size:var(--t-micro);
  min-width:20px;height:20px;border-radius:999px;padding:0 6px;font-weight:600;
  display:inline-flex;align-items:center;justify-content:center;
  font-variant-numeric:tabular-nums;letter-spacing:0;text-transform:none;}
/* Brand mark, sitting directly above the Settings link. The SVG carries its own
   gold frame, so no border here. Spacing matches the nav item rhythm. */
/* The foot of the rail is WHO IS SIGNED IN, not the logo again.
   The logo was there twice - once in the rail and once in the bar - and the
   copy in the rail sat above the Settings link doing nothing. The reference
   puts the person there instead, which is the one piece of information a menu
   can usefully end with. */
.rail-user{display:flex;align-items:center;gap:11px;padding:12px;margin-top:10px;
  border-top:1px solid var(--border);}
.rail-user .disc{width:34px;height:34px;border-radius:50%;flex:none;
  background:var(--gold-dim);color:var(--gold);border:1px solid var(--border);
  display:flex;align-items:center;justify-content:center;
  font-size:var(--t-small);font-weight:600;letter-spacing:.02em;}
.rail-user .who{min-width:0;}
.rail-user .nm{display:block;font-size:var(--t-small);font-weight:600;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.rail-user .ro{display:block;font-size:var(--t-micro);color:var(--faint);}
.rail-collapsed .rail-user{justify-content:center;padding:12px 0;}
.rail-collapsed .rail-user .who{display:none;}

/* ---- main ---- */
/* Gutters and page padding to the reference's rhythm: consistent 20px between
   cards, 24px around the page. Ours ran 16 and 28, which is close enough to
   look accidental rather than chosen. */
/* Full width. The 1320px cap left a dead band down the right of every wide
   screen while the content squeezed itself into the left. The reference runs
   edge to edge and so does this. */
.main{flex:1;padding:24px 32px 56px;min-width:0;
  height:100%;overflow-y:auto;scrollbar-gutter:stable;}
.topbar{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;
  margin-bottom:24px;flex-wrap:wrap;}
/* The greeting and the day line are the one editorial moment in the product.
   Everything else is set in DM Sans at working sizes; this is allowed to be
   large and to use the serif, because it is the only thing on the page a person
   reads rather than scans. */
.topbar h1{font-size:var(--t-title);line-height:1.2;}
.topbar h1.editorial{font-size:clamp(24px,2.6vw,30px);letter-spacing:-.025em;line-height:1.15;}
.topbar .sub{color:var(--muted);font-size:var(--t-input);margin-top:6px;}
.topbar h1.editorial + .sub{font-size:var(--t-input);color:var(--ink);margin-top:8px;max-width:52ch;}

/* ---- cards & grids ---- */
/* EVERY PANEL LIFTS ON HOVER. The client's call, 8 September 2026, and it is
   applied everywhere rather than to the few cards that happen to be clickable -
   a page where some panels answer the mouse and others ignore it feels broken
   rather than restrained.

   Two pixels and a softer shadow. Enough to feel, small enough that a page of
   twelve cards does not jitter as the mouse crosses it. Transform and shadow
   only, so it runs on the compositor; and it stops entirely under
   prefers-reduced-motion, where a page that moves under the cursor is a
   problem rather than a pleasure. */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
  box-shadow:var(--shadow);padding:24px;
  transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease;}
.card:hover{transform:translateY(-2px);box-shadow:var(--shadow-md);border-color:#E2DCCC;}
@media (prefers-reduced-motion:reduce){
  .card{transition:none;}
  .card:hover{transform:none;}
}
/* Equal heights across a row, and the action pinned to the foot of each card.
   Chrome advised against copying this - a tile with less to say carries empty
   white below it - and the client's instruction is to copy the reference, which
   does exactly this. It is his product and his call, and it is not wrong: a row
   of four cards with a ragged bottom edge reads as four unrelated boxes, and
   the flat baseline is what makes them read as one set of figures. */
.grid-4,.grid-3,.grid-2,.grid-60-40{align-items:stretch;}
.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;}
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;}
.grid-60-40{display:grid;grid-template-columns:1.6fr 1fr;gap:20px;}
.mt{margin-top:20px;}

.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px;gap:12px;}
.card-head h3{font-size:var(--t-input);font-weight:600;letter-spacing:-.01em;}
.dropdown{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);
  border-radius:8px;padding:6px 12px;font-size:var(--t-small);
  color:var(--muted);background:var(--surface);cursor:pointer;}
.dropdown svg.ic{width:13px;height:13px;}
.dropdown:hover,.pill:hover{border-color:var(--gold);color:var(--ink);}
.pill{cursor:pointer;}

/* Shared popover for every dropdown in the app. One implementation. */
.menu{position:absolute;z-index:60;min-width:168px;background:var(--surface);
  border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow);
  padding:6px;margin-top:6px;}
.menu button{display:block;width:100%;text-align:left;background:none;border:0;
  padding:9px 12px;border-radius:7px;font-family:var(--font-ui);font-size:var(--t-small);
  color:var(--ink);cursor:pointer;}
.menu button:hover{background:var(--ground);}

/* Notification panel: same popover, richer rows. */
.menu.notif{width:330px;max-height:420px;overflow-y:auto;padding:0;}
.notif-head{display:flex;align-items:center;justify-content:space-between;
  padding:12px 14px;border-bottom:1px solid var(--border);position:sticky;top:0;
  background:var(--surface);}
.notif-head strong{font-size:var(--t-small);}
.notif-head button{width:auto;padding:4px 8px;font-size:var(--t-small);color:var(--gold);border-radius:6px;}
.notif-item{display:block;width:100%;text-align:left;padding:12px 14px;border:0;
  border-bottom:1px solid var(--border);background:none;cursor:pointer;
  font-family:var(--font-ui);border-radius:0;}
.notif-item:last-child{border-bottom:0;}
.notif-item:hover{background:var(--ground);}
.notif-item.unread{background:var(--gold-tint);}
.notif-item .t{display:block;font-size:var(--t-small);font-weight:600;color:var(--ink);}
.notif-item .b{display:block;font-size:var(--t-small);color:var(--muted);margin-top:3px;line-height:1.45;}
.notif-item .w{display:block;font-size:var(--t-micro);color:var(--muted);margin-top:5px;}
.notif-empty{padding:30px 16px;text-align:center;color:var(--muted);font-size:var(--t-small);}
.menu button[aria-current="true"]{background:var(--gold-tint);font-weight:600;}

/* ---- stat tiles ---- */
/* TWO HUES WITH DIFFERENT JOBS, not one accent painted over everything.
   ---------------------------------------------------------------------------
   This is the thing that separates a considered dashboard from a template, and
   the first version of this got it wrong: it put the same gold rule on all four
   tiles, which is one accent used as decoration.

   Green means achieved, healthy, on track.
   Gold means money, and it means act on this.

   So the money tile carries gold and the rest carry green, and a green number
   and a gold number mean different things on every page, consistently. One gold
   tile among three green ones is also doing the hierarchy work that a shadow
   cannot: it says which of the four to read first.

   Depth here is the border plus this rule - deliberately not a shadow. */
.stat{border-top:3px solid var(--accent);
  display:flex;flex-direction:column;min-height:186px;}
.stat.money{border-top-color:var(--gold-fill);}
.stat.money .value{color:var(--ink);}
.stat.good .value{color:var(--ok);}
.stat .stat-top{display:flex;align-items:center;gap:8px;margin-bottom:14px;}
/* The gold rule follows the card's corners rather than cutting straight across
   them. A 3px straight line on a 12px radius leaves two small squared shoulders
   at the top of every tile - visible, and the kind of thing that reads as
   unfinished without anyone being able to say why. */
.stat{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);}
.stat label{display:block;margin-bottom:12px;}
.stat .ic-dot{width:36px;height:36px;border-radius:50%;background:var(--gold-tint);
  color:var(--gold);display:flex;align-items:center;justify-content:center;flex:0 0 36px;}
.ic-dot svg.ic{width:20px;height:20px;}
.stat label{font-size:var(--t-micro);letter-spacing:.12em;text-transform:uppercase;color:var(--faint);
  font-weight:600;}
.statline{display:flex;align-items:center;gap:16px;}
.stat .under{font-size:var(--t-small);color:var(--muted);margin-top:6px;}
.stat .value{font-weight:700;
  /* The one fluid size in the product, and it stays fluid: this is the biggest
     figure on the dashboard and it has to hold its line from a phone to a wide
     monitor. Tied to the two ends of the scale rather than to numbers of its
     own, so it cannot drift away from the system again. */
  font-size:clamp(var(--t-title),2.6vw,var(--t-display));
  font-variant-numeric:tabular-nums;line-height:1;letter-spacing:-.025em;color:var(--ink);}
.stat .value small{font-size:var(--t-body);font-weight:400;color:var(--muted);margin-left:2px;}
.stat .goal-tag{font-size:var(--t-small);color:var(--muted);margin-top:2px;}
.delta{font-size:var(--t-small);color:var(--accent);margin-top:8px;}

/* NO CARD ON THIS DASHBOARD HOLDS A SINGLE NUMBER.
   A figure, then what it is doing, then a way in. A tile that is one number and
   a label is the other half of why the first attempt read as a template.

   `.stat-more` is the breakdown line; `.stat-act` is the way in. Both are
   optional - a tile with nothing true to say in them simply omits them rather
   than padding itself out. */
.stat-more{margin-top:12px;padding-top:10px;border-top:1px solid var(--border);
  font-size:var(--t-small);color:var(--muted);display:flex;justify-content:space-between;gap:10px;}
.stat-more b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums;}
/* Pinned to the bottom of the card by `margin-top:auto`, so every action in the
   row sits on one line however much is above it. Grey rather than gold: on the
   reference these are quiet, and four gold links across the top of the page
   would compete with the figures they sit under. */
.stat-act{display:inline-block;margin-top:auto;padding-top:14px;
  font-size:var(--t-small);font-weight:500;color:var(--muted);text-decoration:none;}
.stat-act:hover{color:var(--gold);}
/* Deliberately no hover on the tile itself: it is a surface, not a control. */

/* ---- upsell card ----
   From Ashley's approved dashboard (WhatsApp, 27 Aug): the marketing strip was
   removed and replaced with an upgrade prompt scoped to the current tier.
   Found missing from the live build during DESIGN-QA. */
.upsell{display:flex;align-items:center;justify-content:space-between;gap:20px;
  margin-top:16px;background:var(--moss-fill);border-color:var(--accent);}
.upsell .eyebrow{font-size:var(--t-micro);font-weight:600;letter-spacing:.08em;
  text-transform:uppercase;color:var(--accent);margin:0 0 4px;}
.upsell h3{font-size:var(--t-head);margin:0 0 4px;}
.upsell .upsell-body{font-size:var(--t-small);color:var(--muted);margin:0;max-width:52ch;}
.upsell-cta{flex:none;background:var(--accent);color:#fff;text-decoration:none;
  font-weight:600;font-size:var(--t-small);padding:11px 18px;border-radius:8px;white-space:nowrap;}
@media (max-width:640px){.upsell{flex-direction:column;align-items:flex-start;}
  .upsell-cta{width:100%;text-align:center;}}

/* ---- rows ---- */
.row{display:flex;align-items:center;gap:12px;padding:10px 0;
  border-bottom:1px solid var(--border);font-size:var(--t-body);}
.row:last-child{border-bottom:0;}
.row.between{justify-content:space-between;}
.num-badge{width:30px;height:30px;border-radius:50%;background:var(--moss-fill);
  color:var(--accent);display:flex;align-items:center;justify-content:center;
  font-weight:700;font-size:var(--t-small);flex:0 0 30px;}
.row .ic-dot{width:30px;height:30px;flex:0 0 30px;}
.row .ic-dot svg.ic{width:17px;height:17px;}
.row .n{font-weight:700;}
.view-all{display:inline-block;margin-top:12px;color:var(--gold);font-size:var(--t-small);
  font-weight:600;text-decoration:none;}

/* ---- table ---- */
.tablewrap{overflow-x:auto;}
table.data{width:100%;border-collapse:collapse;font-size:var(--t-body);}
table.data th{text-align:left;font-size:var(--t-micro);letter-spacing:.08em;text-transform:uppercase;
  color:var(--faint);font-weight:600;padding:0 14px 10px 0;border-bottom:1px solid var(--border);
  white-space:nowrap;}
table.data td{padding:18px 14px 18px 0;font-size:var(--t-small);
  border-bottom:1px solid var(--border);vertical-align:middle;}
/* The first column is the thing the row is ABOUT - a person, a date, a service.
   Everything else in the row is detail about it, and reads as detail. */
table.data td:first-child{font-weight:600;color:var(--ink);}
/* Numbers right-aligned and tabular so they line up as a column of money rather
   than a column of text. The reference left-aligns its figures, which is the
   one thing in its tables that is simply wrong. */
table.data td.num, table.data th.num{text-align:right;font-variant-numeric:tabular-nums;}
/* The mark for a figure GoHighLevel does not report. Quieter than the figures
   beside it, because it is the absence of one rather than a value - and it is
   the one em dash this project allows, in a table of numbers, where it is the
   standard no-value mark. A zero would be a lie about the salon's business and
   an empty cell reads as broken. */
table.data td.nofig{color:var(--faint);}

/* ---- a campaign, in the shape the reference gives one ---------------------
   Name and standing on one line, a strip of four figures under it, the revenue
   on its own line at the foot. Read off the BRX template rather than invented:
   Ms. Ashley asked for "a screen just like this one", and a table of my own
   devising would not have been it.
   Four across on a laptop, two on a phone. Not one column: these four figures
   are read against each other, and a single column turns a comparison into a
   list. */
.campaign-head{display:flex;align-items:flex-start;justify-content:space-between;
  gap:14px;margin-bottom:18px;}
.campaign-name{font-weight:600;font-size:var(--t-input);color:var(--ink);min-width:0;
  overflow-wrap:anywhere;}
.campaign-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;}
.cstat{display:flex;flex-direction:column;gap:5px;min-width:0;}
.cstat .k,.campaign-foot .k{font-size:var(--t-micro);letter-spacing:.08em;
  text-transform:uppercase;color:var(--faint);}
.cstat .v{font-size:var(--t-value);font-weight:600;color:var(--ink);
  font-variant-numeric:tabular-nums;}
.campaign-foot{display:flex;align-items:baseline;gap:9px;margin-top:18px;
  padding-top:16px;border-top:1px solid var(--border);}
.campaign-foot .v{font-size:var(--t-head);font-weight:600;color:var(--ink);
  font-variant-numeric:tabular-nums;}
/* The absent-figure mark, quieter than a figure because it is the absence of
   one. Same rule as the table's. */
.cstat .nofig,.campaign-foot .nofig{color:var(--faint);font-weight:400;}
/* The header the reference puts above this screen: what the section is, whether
   it is running, what it does, and the two things you can do from here. */
.suite-header .suite-title{display:flex;align-items:center;gap:10px;}
.suite-header .suite-title h3{margin:0;}
.suite-header .hint{margin:6px 0 0;}
.suite-acts{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:16px;}
.campaign-sub{font-size:var(--t-small);color:var(--muted);margin-top:3px;}
.campaign-acts{display:flex;gap:8px;flex-wrap:wrap;margin-left:auto;}
.campaign-foot .money{display:inline-flex;align-items:baseline;gap:9px;}
/* Bookings in gold and revenue in green, as the reference tints them - the two
   figures a salon owner is actually looking for. The no-value mark keeps its
   own faint colour: tinting an absence would give it the weight of a number. */
.cstat .v.count{color:var(--gold);}
.campaign-foot .money .v{color:var(--accent);}
.cstat .v.count .nofig,.campaign-foot .money .v .nofig{color:var(--faint);}
@media (max-width:560px){
  .campaign-stats{grid-template-columns:1fr 1fr;gap:16px 14px;}
  .cstat .v{font-size:var(--t-head);}
  /* Four controls in one row is the squeeze that broke the client header.
     Each takes the width it needs and wraps onto its own line when it cannot. */
  .campaign-foot{flex-direction:column;align-items:stretch;gap:14px;}
  .campaign-acts{margin-left:0;}
  .campaign-acts .btn{flex:1 1 auto;justify-content:center;}
  .suite-acts .btn{flex:1 1 100%;justify-content:center;}
}

/* For a heading a screen reader needs and the eye does not: the last column of
   the client list is a row of open links, and a visible word above them would
   be a label on an icon that already says what it does. Clipped rather than
   `display:none`, which would take it out of the accessibility tree too and
   leave the column announced as nothing at all.
   NOT `position:absolute`, which is the recipe everyone copies and which broke
   the client list on a phone. An absolutely positioned box is laid out against
   its nearest POSITIONED ancestor, and a table inside an `overflow-x:auto`
   wrapper has none - so the span escaped the scroller it was supposed to be
   inside, came to rest 458px across a 390px screen, and took the whole
   document's width with it. Measured: the page scrolled sideways by exactly
   the 68px the span sat beyond the viewport.
   In flow, one pixel square, and clipped. It cannot escape anything. */
.sr-only{display:inline-block;width:1px;height:1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;vertical-align:middle;}

/* ---- the heading row on a phone -------------------------------------------
   A search box and an action button side by side is a laptop arrangement. At
   390px the two of them have about 300px between them, and the button's label
   broke over two lines - "Add a / client" - which reads as a squeeze rather
   than a control. Each takes the full width instead, search first because
   finding a client is the commoner errand and the one the page opens for.
   Scoped to `.topbar`, the page's own heading row. The bar at the very top of
   the window - the date, the theme switch, the bell, the avatar - is
   `.page-topbar` and is left exactly as it is. */
@media (max-width: 560px) {
  .topbar { gap: 12px; }
  .topbar .top-actions { flex-wrap: wrap; width: 100%; gap: 10px; }
  .topbar .top-actions > .search { flex: 1 1 100%; }
  .topbar .top-actions > .btn { flex: 1 1 100%; justify-content: center; }
}

/* The open control at the end of a client's row. Quiet until the row is under
   the cursor, because fourteen bright chevrons down the side of a table is a
   column of decoration competing with the figures beside it. */
table.data td.rowgo{width:1px;padding-right:0;text-align:right;}
table.data td.rowgo a{display:inline-flex;align-items:center;justify-content:center;
  width:28px;height:28px;border-radius:8px;color:var(--faint);
  transition:color .15s ease,background .15s ease;}
table.data td.rowgo a svg.ic{width:14px;height:14px;}
table.data tr:hover td.rowgo a{color:var(--gold);background:var(--gold-dim);}
table.data td.rowgo a:focus-visible{outline:2px solid var(--accent);outline-offset:1px;
  color:var(--gold);}
table.data tr:last-child td{border-bottom:0;}
table.data tbody tr{cursor:default;}
/* No transition: the fill snaps, which reads as responsive rather than as the
   row slowly deciding whether it has been hovered. */
table.data tbody tr.clickable:hover{background:var(--surface-hover);cursor:pointer;}

/* ---- tags ---- */
.tag{display:inline-flex;align-items:center;font-size:var(--t-micro);font-weight:700;padding:3px 9px;
  border-radius:999px;background:var(--surface);border:1px solid var(--border);color:var(--muted);
  white-space:nowrap;}
.tag.gold{background:var(--gold-tint);border-color:var(--gold-tint);color:var(--gold);}
.tag.moss{background:var(--moss-fill);border-color:var(--moss-fill);color:var(--accent);}
.tag.warn{background:#F7ECD8;border-color:#F7ECD8;color:#9c6b1f;}

/* ---- buttons / forms ---- */
/* ---------------------------------------------------------------------------
   EVERY BUTTON, MATCHED TO THE REFERENCE. 13 September 2026, asked three times:
   "ayusin mo mga kulay ng button, lahat ng klase ng button na nag-eexist sa app
   na ginawa nya ay dapat kakulay at kasing ganda."

   Read against her app rather than guessed at, and the difference turned out
   not to be the hue at all - after yesterday's fix both use #3D6B35 to act and
   gold to mark what is chosen. What differs is the TREATMENT: hers are rounder,
   heavier, sit on a soft shadow tinted with their own colour, and lift a little
   when a cursor is over them. That is what reads as finished.

   So: 10px corners rather than 8, weight 700 rather than 600, and a moss-tinted
   glow under a primary button rather than the grey one every card uses. The
   lift is two pixels and it is disabled for anybody who has asked their system
   not to animate.

   THE PALETTE IS UNCHANGED. Moss green is still what you press and gold is
   still what is selected; white on this green measures 6.26, which is why the
   text can stay white.

   SECONDARY BUTTONS take her outline treatment: gold rather than the grey
   hairline they had. A grey outline beside a green button reads as disabled,
   and none of these are.
   --------------------------------------------------------------------------- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;background:var(--accent);
  color:#fff;border:0;border-radius:10px;padding:13px 24px;font-family:var(--font-ui);
  font-size:var(--t-body);font-weight:700;cursor:pointer;text-decoration:none;min-height:44px;
  box-shadow:0 4px 14px rgba(61,107,53,.22);}
.btn{transition:background-color .15s ease,box-shadow .15s ease,transform .15s ease,filter .15s ease;}
.btn:hover{filter:brightness(1.06);transform:translateY(-2px);
  box-shadow:0 8px 22px rgba(61,107,53,.30);}
.btn:active{transform:translateY(0);box-shadow:0 3px 10px rgba(61,107,53,.22);}
/* The secondary button is defined ONCE, further down, where its colours were
   already chosen against measured contrast. Adding a second copy here is how
   four competing rules for one button happen - there were three before this
   line was written, and the last one silently won. */
/* A button that cannot be pressed must not lift or glow when hovered. */
.btn[disabled],.btn.is-loading{opacity:.6;pointer-events:none;box-shadow:none;transform:none;}
@media (prefers-reduced-motion:reduce){
  .btn,.btn:hover,.btn:active,.btn.secondary:hover{transform:none;}
}
.field{margin-bottom:16px;}
.field label{display:block;font-size:var(--t-small);font-weight:600;margin-bottom:6px;}
.field input,.field select{width:100%;font-family:var(--font-ui);font-size:var(--t-input);padding:12px 14px;
  border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--ink);
  min-height:46px;}
.field input:focus,.field select:focus{outline:2px solid var(--accent);outline-offset:1px;}
.field .hint{font-size:var(--t-small);color:var(--muted);margin-top:5px;
  display:flex;align-items:center;justify-content:space-between;gap:8px;}
/* Text-styled button, for "Show/Hide password". */
.linkish{background:none;border:0;padding:0;font-family:var(--font-ui);font-size:var(--t-small);
  color:var(--gold);cursor:pointer;text-decoration:underline;}

/* Consent block. Checkboxes are stacked and separately labelled so agreeing to
   the terms is never confused with agreeing to marketing. */
.consent{margin:4px 0 20px;display:flex;flex-direction:column;gap:12px;}
.check{display:flex;align-items:flex-start;gap:10px;cursor:pointer;
  font-size:var(--t-small);line-height:1.5;color:var(--muted);}
.check input{margin:1px 0 0;width:16px;height:16px;flex:none;accent-color:var(--accent);cursor:pointer;}
.check a{color:var(--gold);}

/* Long-form legal copy. */
.legal{max-width:720px;margin:0 auto;padding:48px 20px 80px;}
.legal h1{font-family:var(--font-editorial);font-size:var(--t-display);font-weight:600;margin-bottom:6px;}
.legal .updated{color:var(--muted);font-size:var(--t-small);margin-bottom:28px;}
.legal h2{font-size:var(--t-input);margin:28px 0 8px;}
.legal p,.legal li{font-size:var(--t-body);line-height:1.65;color:var(--ink);}
.legal ul{padding-left:20px;}
.legal .notice{border:1px solid var(--gold);background:var(--gold-tint);
  border-radius:var(--radius);padding:16px 18px;margin-bottom:28px;font-size:var(--t-small);}

/* ---- states ---------------------------------------------------------------
   `.state` is the empty state, and it is used in nineteen places. Rather than
   introduce a second class and convert them one at a time - which would have
   left the product looking half-finished in whichever half was not done - this
   is the same class, raised.

   It matters more than its size suggests: EVERY page is empty on day one. No
   clients, no bookings, no reviews. A bare line of grey text in the middle of a
   card reads as something failing, not as something not yet started, and it is
   the first thing every new customer sees. */
.state{padding:44px 24px;text-align:center;color:var(--muted);font-size:var(--t-small);
  line-height:1.6;}
.state .state-title{display:block;font-weight:600;color:var(--ink);
  margin-bottom:6px;font-size:var(--t-input);letter-spacing:-.01em;}
/* A short gold rule above the title, not a disc.
   The disc was a filled gold circle with nothing in it, which reads as an image
   that failed to load rather than as a decision. The rule is the same mark that
   runs across the top of every stat tile, so the empty state is recognisably
   part of this product rather than a placeholder in it. */
.state .state-title::before{content:"";display:block;width:34px;height:3px;
  margin:0 auto 16px;border-radius:2px;background:var(--gold-fill);}
.state.error{color:var(--danger);}
.state.error .state-title::before{background:var(--danger);}
.skeleton{background:linear-gradient(90deg,var(--border) 25%,var(--ground) 50%,var(--border) 75%);
  background-size:200% 100%;animation:sh 1.4s ease-in-out infinite;border-radius:6px;height:14px;}
@keyframes sh{0%{background-position:200% 0;}100%{background-position:-200% 0;}}
@media (prefers-reduced-motion:reduce){.skeleton{animation:none;}}

.banner{display:flex;align-items:center;gap:12px;padding:12px 16px;border-radius:8px;
  font-size:var(--t-body);margin-bottom:18px;}
.banner.demo{background:var(--gold-tint);color:#6b5420;}
.banner.warn{background:#F7ECD8;color:#7d5518;}

/* ---- auth pages ---- */
.auth-wrap{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;}
.auth-card{width:100%;max-width:420px;background:var(--surface);border:1px solid var(--border);
  border-radius:var(--radius);box-shadow:var(--shadow);padding:32px;}
.auth-card .wordmark{display:block;text-align:center;margin-bottom:6px;font-size:var(--t-head);}
.auth-card h1{font-size:var(--t-value);text-align:center;margin-bottom:4px;}
.auth-card .sub{text-align:center;color:var(--muted);font-size:var(--t-body);margin:0 0 24px;}
.auth-alt{text-align:center;font-size:var(--t-body);color:var(--muted);margin-top:18px;}
.auth-alt a{color:var(--accent);font-weight:600;}
.form-error{background:#FBEAE6;color:#A23E32;padding:11px 14px;border-radius:8px;
  font-size:var(--t-small);margin-bottom:16px;}

/* ---- mobile ---- */
.hamburger{display:none;width:38px;height:38px;border:1px solid var(--border);border-radius:8px;
  background:var(--surface);align-items:center;justify-content:center;cursor:pointer;}
.rail-scrim{display:none;}
@media (max-width:980px){
  :root{--topbar-h:60px;}
  .hamburger{display:flex;}
  .rail{position:fixed;left:0;top:0;height:100dvh;z-index:40;transform:translateX(-100%);
    transition:transform .2s ease;box-shadow:0 0 40px rgba(0,0,0,.15);}
  .rail.open{transform:translateX(0);}
  .rail-scrim.show{display:block;position:fixed;inset:0;background:rgba(20,20,15,.35);z-index:30;}
  .grid-4{grid-template-columns:1fr 1fr;}
  .grid-3,.grid-2,.grid-60-40{grid-template-columns:1fr;}
  .main{padding:20px 16px 48px;}
  .rail{width:270px;flex-basis:270px;}
  .page-topbar{padding:0 16px;}
  .card{padding:18px;}
}
@media (max-width:560px){
  .grid-4{grid-template-columns:1fr 1fr;}
  .topbar h1{font-size:var(--t-value);}
  .page-topbar .pill{display:none;}
}
@media (prefers-reduced-motion:reduce){.rail{transition:none;}}

/* ---- inline help ----------------------------------------------------------
   A small "?" beside a label, opening a short explanation on CLICK. Hover is
   deliberately not a trigger: most of these customers are on a phone, where a
   hover tooltip simply never appears.

   Sized to the 44px touch target rule even though the circle is 16px, so it is
   tappable without being visually loud. */
.help{
  display:inline-flex;align-items:center;justify-content:center;
  width:16px;height:16px;margin-left:6px;padding:0;
  border:1px solid var(--border);border-radius:50%;
  background:transparent;color:var(--muted);
  font-family:var(--font-ui);font-size:var(--t-micro);font-weight:600;line-height:1;
  cursor:pointer;vertical-align:middle;position:relative;
}
.help::after{content:"";position:absolute;inset:-14px;}   /* touch target */
.help:hover,.help[aria-expanded="true"]{border-color:var(--accent);color:var(--accent);}
.help:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

.helpbox{
  position:absolute;z-index:60;max-width:280px;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
  padding:12px 14px;box-shadow:0 8px 28px -12px rgba(0,0,0,.28);
}
.helpbox strong{display:block;font-size:var(--t-small);margin-bottom:4px;}
.helpbox p{margin:0;font-size:var(--t-small);line-height:1.6;color:var(--muted);}
@media (max-width:560px){
  .helpbox{max-width:calc(100vw - 24px);}
}

/* Onboarding sections.
   The twelve questions used to be one flat grid, which read as a form to get
   through rather than a set of things worth answering. Grouped at the client's
   request, 6 September 2026.
   A rule above each heading rather than a card per group: five stacked cards
   inside a card is heavier than the content, and these are short sections. */
.onb-group + .onb-group{margin-top:28px;padding-top:24px;border-top:1px solid var(--border);}
.onb-group-title{
  font-size:var(--t-input);font-weight:700;
  color:var(--ink);margin:0 0 2px;letter-spacing:-.01em;}
.onb-group-blurb{margin:0 0 14px;}

/* ---- section label -------------------------------------------------------
   The 10px uppercase label that runs above a value or a group. Ours existed
   only inside .stat; every other page invented its own, at a different size
   each time. */
.label{font-size:var(--t-micro);font-weight:600;letter-spacing:.08em;text-transform:uppercase;
  color:var(--faint);margin-bottom:4px;}

/* ---- badge ----
   Distinct from .tag on purpose: .tag is a rounded pill for a status a person
   reads, this is a square-ish marker for a state a person scans. */
.badge{display:inline-block;font-size:var(--t-micro);font-weight:600;letter-spacing:.05em;
  text-transform:uppercase;padding:3px 8px;border-radius:4px;
  background:var(--surface-hover);color:var(--muted);}
.badge.gold{background:var(--gold-dim);color:var(--gold);}
.badge.moss{background:var(--moss-fill);color:var(--accent);}
.badge.danger{background:#FBEAE6;color:var(--danger);}

/* ---- charts --------------------------------------------------------------
   Fixed heights, because a canvas with no height collapses to nothing and a
   canvas sized by its parent grows without limit on every redraw. Both are
   easy to ship and neither is obvious in review. */
.chart{position:relative;height:280px;}
.chart.sm{height:200px;}
.chart-legend{display:flex;flex-wrap:wrap;gap:14px;margin-top:14px;
  font-size:var(--t-small);color:var(--muted);}
.chart-legend span{display:inline-flex;align-items:center;gap:7px;}
.chart-legend i{width:10px;height:10px;border-radius:3px;display:inline-block;}

/* ---- empty states --------------------------------------------------------
   THIS IS THE FIRST THING A NEW CUSTOMER SEES. Every page is empty on day one -
   no clients, no bookings, no reviews - and until now those pages showed a bare
   line of grey text, which reads as something failing rather than something not
   started. It is the least designed part of the product and the most seen. */
.empty{padding:44px 24px;text-align:center;}
.empty .empty-ic{width:44px;height:44px;border-radius:50%;margin:0 auto 14px;
  background:var(--gold-dim);color:var(--gold);
  display:flex;align-items:center;justify-content:center;}
.empty .empty-ic svg.ic{width:20px;height:20px;}
.empty h4{font-size:var(--t-input);font-weight:600;color:var(--ink);margin:0 0 6px;}
.empty p{font-size:var(--t-small);color:var(--muted);margin:0 auto;max-width:44ch;line-height:1.6;}
.empty .btn{margin-top:18px;}

/* Loading. A card-shaped shimmer rather than an empty page, so a slow account
   looks like it is working instead of broken. */
.skeleton-card{padding:24px;}
.skeleton-card .skeleton + .skeleton{margin-top:10px;}
.skeleton.w40{width:40%;} .skeleton.w60{width:60%;} .skeleton.w80{width:80%;}
.skeleton.tall{height:30px;}

/* ---- status ----
   Tinted background with matching text, never a bare coloured word. Three
   states and they mean the same thing everywhere they appear. */
.status{display:inline-block;font-size:var(--t-micro);font-weight:600;letter-spacing:.05em;
  text-transform:uppercase;padding:3px 9px;border-radius:4px;}
.status.ok{background:var(--moss-fill);color:var(--ok);}
.status.wait{background:#F7ECD8;color:#8a6417;}
.status.bad{background:#FBEAE6;color:var(--danger);}

/* ---- inset ---------------------------------------------------------------
   A grey panel nested inside a white card. The one place the reference finds
   real depth without a shadow: the surface recedes instead of the card lifting.
   Used for a group of figures that belong together inside a larger card. */
.inset{background:var(--ground);border:1px solid var(--border);border-radius:10px;
  padding:18px;}
.inset .label{margin-bottom:6px;}
.inset .value{font-size:var(--t-value);font-weight:600;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;}

/* The scrollbar appearing and disappearing between pages shifts the whole
   layout sideways by its own width. Reserving the gutter costs nothing and
   removes a shift that reads as the page twitching. */
/* The main column reserves its own gutter; the document no longer scrolls. */

/* ---- collapsing rail -----------------------------------------------------
   248px down to icons. The toggle straddles the rail's right border, half over
   each side - a small detail, and the thing that makes it read as part of the
   rail rather than a button parked next to it.

   The rail and the content move together, so nothing overlaps mid-animation.
   Collapsed items keep a title attribute (set in app.js), because a menu of
   unlabelled icons with no tooltips is the reference's one real usability
   miss and there is no reason to inherit it. */
.rail{position:relative;transition:width .25s ease,flex-basis .25s ease;}
.rail-collapse{
  position:absolute;top:22px;right:-14px;width:28px;height:28px;border-radius:50%;
  background:var(--surface);border:1px solid var(--border);color:var(--muted);
  display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;
  box-shadow:var(--shadow);z-index:2;transition:color .15s ease,border-color .15s ease;}
.rail-collapse:hover{color:var(--ink);border-color:var(--gold);}
.rail-collapse svg.ic{width:13px;height:13px;transform:rotate(90deg);transition:transform .25s ease;}
.rail-collapsed .rail-collapse svg.ic{transform:rotate(-90deg);}

.rail-collapsed .rail{width:76px;flex-basis:76px;padding-left:12px;padding-right:12px;}

/* ---- collapsing hides the labels, it does not squeeze them ----------------
   WHAT MS. ASHLEY REPORTED ON 11 SEPTEMBER: "the side dashboards don't close
   correctly, they get cut off", on her phone, her iPad and her laptop. One
   cause, all three devices, and it was in this block.

   The labels used to be hidden with `height:0;overflow:hidden` and a
   `padding-bottom:14px` in the same declaration - a padding that was written
   for the wordmark and reached the labels and badges because all three shared
   one selector list. `overflow:hidden` clips at the PADDING box, not the
   content box, so 14px of bottom padding left a 14px-tall window with the
   label still painted inside it. Every item in a 76px rail therefore read as
   the first two letters of its name: "Da", "In", "Bo", "Bo". Cut off, exactly
   as described, and measurably so - a 16px wide box holding the word
   "Dashboard".

   Hidden means hidden. `display:none` takes the label out of layout, which is
   what "collapse to icons" means, and there is no partial height left for text
   to show through. The name is not lost: app.js puts it on the link's `title`
   while collapsed, so hovering an icon still says where it goes. */
.rail-collapsed .rail-label,
.rail-collapsed .rail .badge{display:none;}

/* The wordmark is the one element that genuinely wanted this treatment: it
   collapses to nothing but keeps the space above the first icon. */
.rail-collapsed .rail .wordmark{padding-bottom:14px;height:0;overflow:hidden;}

.rail-collapsed .nav a{justify-content:center;padding-left:0;padding-right:0;
  border-left-color:transparent;}
/* Active reads as a filled square rather than a tinted bar, because a 2px left
   border on a 52px icon button is invisible. */
.rail-collapsed .nav a.active,
.rail-collapsed .nav a.active{background:var(--gold);color:#fff;}
.rail-collapsed .nav a.active svg.ic{color:#fff;}
/* ---- on a phone the collapsed state is switched off, not adjusted ---------
   THE SECOND HALF OF THE SAME REPORT, and the worse half. Collapse is a
   desktop affordance; below 980px the rail is a drawer and there is nothing to
   collapse. But `rail-collapsed` is REMEMBERED in localStorage, so a customer
   who collapsed the menu on a laptop carried the class onto her phone, where
   this block used to hand the labels a `padding:11px 14px` - again a padding
   meant for the link, not for the text inside it - and never unwound the
   `height:0` above.

   What that looked like on her phone: the drawer opened at its full width and
   every item was sliced through the middle of its own word, with the greeting
   gone from the top. A menu of half-letters.

   So the state is neutralised here rather than restyled: at drawer widths the
   rail renders exactly as it does uncollapsed. Every property the collapsed
   rules touch is named and put back, because a reset that only covers the ones
   that happened to break is the same bug waiting for the next device. */
@media (max-width:980px){
  /* The rail is already a drawer here; collapsing a drawer means nothing. */
  .rail-collapse{display:none;}
  .rail-collapsed .rail{width:270px;flex-basis:270px;padding:26px 16px;}
  .rail-collapsed .rail-label,
  .rail-collapsed .rail .badge{display:block;height:auto;padding:0;overflow:visible;}
  .rail-collapsed .rail-label.lock{display:inline-flex;}
  .rail-collapsed .rail .wordmark{height:auto;padding:0 10px 26px;overflow:visible;}
  .rail-collapsed .rail-greet{opacity:1;visibility:visible;height:auto;
    padding:0 12px 26px;overflow:visible;}
  .rail-collapsed .rail-user{justify-content:flex-start;padding:12px;}
  .rail-collapsed .rail-user .who{display:block;}
  .rail-collapsed .nav a{justify-content:flex-start;padding:11px 14px;}
  .rail-collapsed .nav a.active{background:var(--gold-dim);color:var(--gold);
    border-left-color:var(--gold);}
}
@media (prefers-reduced-motion:reduce){
  .rail,.rail-collapse svg.ic{transition:none;}
}

/* ---- split bar ----
   One bar, one colour per part, the count inside the part. Sized by content
   rather than by an axis: for four or five values a bar with a scale down the
   side says less than the numbers themselves. */
.splitbar{display:flex;height:34px;border-radius:8px;overflow:hidden;
  border:1px solid var(--border);}
.splitbar span{display:flex;align-items:center;justify-content:center;
  color:#fff;font-size:var(--t-micro);font-weight:600;font-variant-numeric:tabular-nums;
  min-width:0;}

/* ---- filter chips --------------------------------------------------------
   A row of pills, each carrying its own count. The count is the point: it tells
   you whether a filter is worth pressing before you press it, which a bare word
   does not. Active is outlined in gold rather than filled, so the row stays
   quiet while still saying where you are. */
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:18px;}
.chip{display:inline-flex;align-items:center;gap:7px;padding:7px 14px;border-radius:999px;
  border:1px solid var(--border);background:var(--surface);color:var(--muted);
  font-family:var(--font-ui);font-size:var(--t-small);font-weight:500;cursor:pointer;
  transition:background-color .15s ease,border-color .15s ease,color .15s ease;}
.chip:hover{background:var(--surface-hover);color:var(--ink);}
.chip.on{border-color:var(--gold);color:var(--gold);background:var(--gold-dim);font-weight:600;}
.chip .c{font-size:var(--t-micro);font-variant-numeric:tabular-nums;opacity:.7;}

/* ---- person header -------------------------------------------------------
   Initials in a disc, then the name, then how to reach them. A client record
   opens with who it is, not with a figure about them. */
.person{display:flex;align-items:center;gap:16px;margin-bottom:20px;}
.person .disc{width:56px;height:56px;border-radius:50%;flex:none;
  background:var(--gold-dim);color:var(--gold);border:1px solid var(--border);
  display:flex;align-items:center;justify-content:center;
  font-size:var(--t-head);font-weight:700;letter-spacing:.02em;}
.person h2{font-size:var(--t-title);font-weight:700;margin:0;
  letter-spacing:-.02em;line-height:1.15;}
.person .meta{color:var(--muted);font-size:var(--t-small);margin-top:3px;
  display:flex;flex-wrap:wrap;gap:14px;}
.person .spacer{flex:1;}

/* ---- locked row ----------------------------------------------------------
   A feature the customer's plan does not include stays VISIBLE, greyed, with a
   padlock and the price of the tier that unlocks it. Hiding it means they never
   learn it exists; showing it as an empty promise is worse. This is the one
   pattern worth taking wholesale from the reference.

   Dashed rather than solid border: the shape says "not yet" before the words
   do. */
.locked{position:relative;border:1px dashed var(--border);border-radius:10px;
  padding:16px 18px;background:var(--ground);}
.locked .locked-head{display:flex;align-items:center;gap:8px;font-weight:600;
  font-size:var(--t-body);color:var(--muted);}
.locked .locked-head svg.ic{width:15px;height:15px;color:var(--faint);}
.locked p{margin:6px 0 0;font-size:var(--t-small);color:var(--faint);line-height:1.55;max-width:56ch;}
.locked .locked-cta{display:inline-block;margin-top:10px;font-size:var(--t-small);font-weight:600;
  color:var(--gold);text-decoration:none;}
.locked .locked-cta:hover{text-decoration:underline;}

/* ---- toast ----
   A dark pill, bottom right, gone in a few seconds. Deliberately not a banner:
   a banner sits in the layout and pushes the page around; this is a receipt for
   something the person just did. */
.toast{position:fixed;right:20px;bottom:20px;z-index:80;
  background:#26261f;color:#fff;border-radius:999px;padding:12px 20px;
  font-size:var(--t-small);font-weight:500;box-shadow:var(--shadow-md);
  opacity:0;transform:translateY(8px);transition:opacity .3s ease,transform .3s ease;}
.toast.show{opacity:1;transform:none;}
@media (prefers-reduced-motion:reduce){.toast{transition:none;}}
@media (max-width:560px){.toast{left:16px;right:16px;text-align:center;}}

/* The search field in a page's action slot. It was styled inline on the clients
   page and hard-coded Inter, which had survived the move to DM Sans and was the
   only element in the product still set in the old face. */
.search{font-family:var(--font-ui);font-size:var(--t-body);padding:9px 14px;
  border:1px solid var(--border);border-radius:8px;background:var(--surface);
  color:var(--ink);min-height:40px;min-width:220px;}
.search:focus{outline:2px solid var(--gold);outline-offset:1px;}
.search::placeholder{color:var(--faint);}
@media (max-width:560px){.search{min-width:0;width:100%;}}

/* ---- week calendar -------------------------------------------------------
   Seven columns, one hour a row, drawn as a grid rather than a table so an
   appointment can sit in a cell without a table's row height fighting it.

   The closed day is hatched rather than blank: blank reads as "no bookings",
   hatched reads as "not open", and those are different facts about a salon. */
.weekcal{border:1px solid var(--border);border-radius:10px;overflow:hidden;}
.weekcal-head{display:grid;grid-template-columns:64px repeat(7,1fr);
  background:var(--ground);border-bottom:1px solid var(--border);}
.weekcal-head div{padding:14px 8px;text-align:center;}
.weekcal-head .dow{font-size:var(--t-micro);letter-spacing:.1em;text-transform:uppercase;
  color:var(--faint);font-weight:600;}
.weekcal-head .dom{font-size:var(--t-value);font-weight:600;color:var(--ink);margin-top:4px;
  font-variant-numeric:tabular-nums;}
.weekcal-head .today .dom{background:var(--accent);color:#fff;border-radius:50%;
  width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;}
.weekcal-body{display:grid;grid-template-columns:64px repeat(7,1fr);}
.weekcal-body .hr{grid-column:1;font-size:var(--t-micro);color:var(--faint);padding:10px 8px;
  text-align:right;border-right:1px solid var(--border);border-bottom:1px solid var(--border);
  font-variant-numeric:tabular-nums;}
.weekcal-cell{border-right:1px solid var(--border);border-bottom:1px solid var(--border);
  min-height:56px;padding:4px;}
.weekcal-cell:last-child{border-right:0;}
.weekcal-cell.closed{
  background-image:repeating-linear-gradient(45deg,transparent,transparent 6px,
    var(--surface-hover) 6px,var(--surface-hover) 12px);}
.weekcal-closed{grid-column:2;display:flex;align-items:center;justify-content:center;
  font-size:var(--t-micro);letter-spacing:.1em;text-transform:uppercase;color:var(--faint);}
.appt{display:block;background:var(--gold-tint);border-left:3px solid var(--gold-fill);
  border-radius:4px;padding:6px 8px;margin-bottom:4px;}
.appt.confirmed{background:var(--moss-fill);border-left-color:var(--accent);}
/* Cancelled is struck through and kept, not hidden. A slot that emptied is
   something the owner needs to see; a slot that was never booked is not. */
.appt.cancelled{background:#FBEAE6;border-left-color:var(--danger);}
.appt.cancelled .who,.appt.cancelled .what{text-decoration:line-through;color:var(--faint);}
.appt .who{display:block;font-size:var(--t-small);font-weight:600;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.appt .what{display:block;font-size:var(--t-micro);color:var(--muted);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.appt .when{display:block;font-size:var(--t-micro);color:var(--faint);margin-top:1px;}
.weekcal-nav{display:flex;align-items:center;justify-content:space-between;gap:16px;
  margin-bottom:18px;}
.weekcal-nav .mid{text-align:center;}
.weekcal-nav .mid strong{display:block;font-size:var(--t-head);font-weight:700;letter-spacing:-.01em;}
.weekcal-nav .mid span{display:block;font-size:var(--t-small);color:var(--muted);margin-top:2px;}
@media (max-width:900px){
  .weekcal{overflow-x:auto;}
  .weekcal-head,.weekcal-body{min-width:720px;}
}
/* The week as a list on a phone: a day to a row, its bookings in time order,
   "Closed" on a day with none. The grid above stays for wider screens. */
.weekcal-agenda{display:none;}
@media (max-width:600px){
  .weekcal{display:none;}
  .weekcal-agenda{display:block;border:1px solid var(--border);border-radius:10px;overflow:hidden;}
  .agenda-day{display:grid;grid-template-columns:56px 1fr;gap:12px;padding:12px 14px;
    border-bottom:1px solid var(--border);align-items:start;}
  .agenda-day:last-child{border-bottom:0;}
  .agenda-date{text-align:center;}
  .agenda-date .dow{display:block;font-size:var(--t-micro);letter-spacing:.1em;text-transform:uppercase;
    color:var(--faint);font-weight:600;}
  .agenda-date .dom{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;
    font-size:var(--t-value);font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums;}
  .agenda-day.today .agenda-date .dom{background:var(--accent);color:#fff;border-radius:50%;}
  .agenda-list{min-width:0;}
  .agenda-list .appt{margin-bottom:6px;}
  .agenda-none{display:block;padding-top:8px;font-size:var(--t-micro);letter-spacing:.1em;
    text-transform:uppercase;color:var(--faint);}
  .agenda-day.closed{background-image:repeating-linear-gradient(45deg,transparent,transparent 6px,
    var(--surface-hover) 6px,var(--surface-hover) 12px);}
}

/* ---- source list ----
   A dot in the source's own colour, the name, the count, then a bar. The dot
   and the bar are the same colour, so the eye ties them together without a
   legend. */
.sources{display:flex;flex-wrap:wrap;gap:26px;align-items:center;}
.source{display:flex;align-items:center;gap:9px;font-size:var(--t-small);}
.source i{width:9px;height:9px;border-radius:50%;flex:none;}
.source b{font-weight:600;color:var(--ink);}
.source .n{color:var(--muted);}
.source .bar{width:76px;height:6px;border-radius:3px;background:var(--surface-hover);
  overflow:hidden;}
.source .bar span{display:block;height:100%;border-radius:3px;}

/* ---- panel with a coloured edge ----
   One card in the set carries a green left bar. It is the only card on the page
   about people bringing people, and the bar is what stops it reading as another
   figure card. */
.edged{border-left:3px solid var(--accent);}
.edged.gold{border-left-color:var(--gold-fill);}
.figs{display:flex;flex-wrap:wrap;gap:34px;align-items:flex-start;}
.figs .fig{min-width:120px;}
.figs .fig .v{font-size:var(--t-title);font-weight:700;letter-spacing:-.02em;line-height:1;
  font-variant-numeric:tabular-nums;}
.figs .fig .v.green{color:var(--accent);}
.figs .fig .v.gold{color:var(--gold);}
.figs .fig .k{font-size:var(--t-small);color:var(--muted);margin-top:6px;}

/* ---- inbox preview ----
   Each message is a filled block rather than a row, because a message is a
   thing someone said and a row is a record. */
.msg{background:var(--gold-tint);border-radius:8px;padding:14px 16px;margin-bottom:12px;}
.msg .top{display:flex;align-items:baseline;justify-content:space-between;gap:12px;}
.msg .from{font-size:var(--t-small);font-weight:600;color:var(--ink);}
.msg .ago{font-size:var(--t-micro);color:var(--faint);white-space:nowrap;}
.msg .body{font-size:var(--t-small);color:var(--muted);margin:5px 0 10px;line-height:1.5;}
.msg .ch{display:inline-flex;align-items:center;gap:7px;font-size:var(--t-micro);color:var(--muted);}
.msg .ch svg.ic{width:15px;height:15px;}

/* ---- inbox, two panes ----------------------------------------------------
   The page does not scroll. Each pane scrolls on its own, because a thread and
   a list of threads are two separate places and moving one should not move the
   other.

   Heights are derived from the shell rather than fixed: the main column already
   knows how tall it is, so the panes fill it. */
.inbox{display:grid;grid-template-columns:340px 1fr;gap:0;
  border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);
  box-shadow:var(--shadow);overflow:hidden;height:calc(100vh - var(--topbar-h) - 48px);}
.inbox-list{border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:0;}
.inbox-head{padding:18px 18px 12px;display:flex;align-items:center;
  justify-content:space-between;gap:12px;}
.inbox-head h3{font-size:var(--t-input);font-weight:700;margin:0;letter-spacing:-.01em;}
.inbox-search{padding:0 18px 12px;}
.inbox-threads{flex:1;overflow-y:auto;min-height:0;}

.thread{display:flex;gap:12px;padding:14px 18px;cursor:pointer;
  border-left:3px solid transparent;border-bottom:1px solid var(--border);}
.thread:hover{background:var(--surface-hover);}
.thread.on{background:var(--gold-tint);border-left-color:var(--gold-fill);}
.thread .disc{width:38px;height:38px;border-radius:50%;flex:none;position:relative;
  background:var(--gold-dim);color:var(--gold);border:1px solid var(--border);
  display:flex;align-items:center;justify-content:center;font-size:var(--t-small);font-weight:700;}
/* The unread dot rides the avatar rather than sitting in the row, so it is in
   the same place whatever length the name is. */
.thread .disc .dot{position:absolute;top:-1px;right:-1px;width:10px;height:10px;
  border-radius:50%;background:var(--gold-fill);border:2px solid var(--surface);}
.thread .body{min-width:0;flex:1;}
.thread .top{display:flex;justify-content:space-between;gap:10px;align-items:baseline;}
.thread .nm{font-size:var(--t-small);font-weight:600;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.thread .ago{font-size:var(--t-micro);color:var(--faint);white-space:nowrap;}
.thread .pv{font-size:var(--t-small);color:var(--muted);margin-top:3px;line-height:1.4;
  display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;}
.thread .ch{font-size:var(--t-micro);letter-spacing:.06em;text-transform:uppercase;
  color:var(--faint);margin-top:6px;display:inline-block;}

.inbox-pane{display:flex;flex-direction:column;min-height:0;min-width:0;}
.inbox-bar{display:flex;align-items:center;gap:12px;padding:16px 20px;
  border-bottom:1px solid var(--border);}
.inbox-bar .disc{width:40px;height:40px;border-radius:50%;flex:none;
  background:var(--gold-dim);color:var(--gold);border:1px solid var(--border);
  display:flex;align-items:center;justify-content:center;font-size:var(--t-small);font-weight:700;}
.inbox-bar .nm{font-size:var(--t-input);font-weight:700;letter-spacing:-.01em;}
.inbox-bar .sub{font-size:var(--t-small);color:var(--muted);margin-top:2px;}
.inbox-bar .spacer{flex:1;}

.stream{flex:1;overflow-y:auto;min-height:0;padding:22px 20px;
  display:flex;flex-direction:column;gap:14px;}
.bub{max-width:min(72%,560px);border-radius:12px;padding:12px 15px;}
.bub .t{font-size:var(--t-small);line-height:1.5;}
.bub .m{font-size:var(--t-micro);margin-top:6px;opacity:.72;}
/* In from the client: a light card on the left. Out from the salon: filled
   gold on the right. The side alone would carry it; the fill is what makes a
   long thread readable at a glance. */
.bub.in{align-self:flex-start;background:var(--surface);border:1px solid var(--border);}
.bub.out{align-self:flex-end;background:var(--gold-fill);color:#fff;}
.bub.out .m{color:#fff;}

.composer{border-top:1px solid var(--border);padding:14px 20px;
  display:flex;align-items:center;gap:12px;}
.composer input{flex:1;font-family:var(--font-ui);font-size:var(--t-body);padding:11px 16px;
  border:1px solid var(--border);border-radius:999px;background:var(--surface);
  color:var(--ink);min-height:44px;}
.composer input:disabled{background:var(--ground);color:var(--faint);}
.composer .send{width:44px;height:44px;border-radius:50%;flex:none;border:0;
  background:var(--gold-fill);color:#fff;display:flex;align-items:center;
  justify-content:center;cursor:pointer;}
.composer .send:disabled{background:var(--border);color:var(--faint);cursor:not-allowed;}
.composer-note{font-size:var(--t-small);color:var(--muted);padding:0 20px 14px;}

@media (max-width:900px){
  .inbox{grid-template-columns:1fr;height:auto;}
  .inbox-list{border-right:0;border-bottom:1px solid var(--border);}
  .inbox-threads{max-height:320px;}
  .stream{min-height:320px;}
}

/* ---- month grid ----------------------------------------------------------
   Six rows always. A grid that grows to six rows one month and shrinks to five
   the next makes the page jump as you page through it, which reads as
   instability rather than as information.

   Days outside the month are drawn and greyed rather than left blank: blank
   corners look like a rendering fault, and grey says "this is next month"
   without needing a second label. */
.mgrid{display:grid;grid-template-columns:repeat(7,1fr);
  border:1px solid var(--border);border-radius:10px;overflow:hidden;}
.mhead{background:var(--ground);padding:12px 8px;text-align:center;
  font-size:var(--t-micro);letter-spacing:.1em;text-transform:uppercase;color:var(--faint);
  font-weight:600;border-bottom:1px solid var(--border);}
.mcell{min-height:112px;padding:8px;border-right:1px solid var(--border);
  border-bottom:1px solid var(--border);background:var(--surface);}
.mcell:nth-child(7n+7){border-right:0;}
.mcell.out{background:var(--ground);}
.mcell.out .dn{color:var(--faint);}
.mcell .dn{display:block;font-size:var(--t-small);font-weight:600;color:var(--ink);margin-bottom:6px;
  font-variant-numeric:tabular-nums;}
.mcell.now .dn{background:var(--accent);color:#fff;border-radius:50%;
  width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;}
.mchip{display:block;background:var(--gold-tint);border-radius:4px;padding:3px 6px;
  margin-bottom:3px;font-size:var(--t-micro);color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.mchip.cancelled{background:#FBEAE6;color:var(--faint);text-decoration:line-through;}
.mcell .more{display:block;font-size:var(--t-micro);color:var(--gold);font-weight:600;margin-top:2px;}

.cal-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;
  margin-bottom:18px;flex-wrap:wrap;}
.cal-move{display:flex;align-items:center;gap:14px;}
.cal-move strong{font-size:var(--t-value);font-weight:700;letter-spacing:-.01em;min-width:170px;}
.btn.icon{min-height:38px;padding:0 14px;font-size:var(--t-head);line-height:1;}

@media (max-width:900px){
  .mgrid{min-width:660px;}
  .card > .mgrid{min-width:0;}
  .mcell{min-height:88px;}
}

/* ---- goal bar ----
   A figure against its target, the bar, and the one line that says what to do
   about it. The helper is the point: "on track" is a feeling, "$80 a day" is an
   instruction. */
.goal{padding:4px 0;}
.goal-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin-bottom:10px;}
.goal-top strong{font-size:var(--t-value);font-weight:700;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;}
.goal-top .of{color:var(--faint);font-weight:500;font-size:var(--t-input);}
.goal-track{height:8px;border-radius:4px;background:var(--surface-hover);overflow:hidden;}
.goal-track span{display:block;height:100%;border-radius:4px;}
.goal-foot{display:flex;justify-content:space-between;gap:12px;margin-top:9px;
  font-size:var(--t-small);color:var(--muted);}
.goal-foot .good{color:var(--accent);font-weight:600;}
.row .n.bad{color:var(--danger);}

/* ---- paywall ----
   A locked feature gets a designed page rather than a dead end. The point of it
   is that a customer who cannot use something should still understand what it
   is - otherwise the only people who ever buy it are the ones who already knew. */
.paywall{text-align:center;padding:52px 24px 44px;background:var(--surface);
  border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);}
.paywall-ic{width:72px;height:72px;border-radius:18px;margin:0 auto 22px;
  background:var(--gold-fill);color:#fff;display:flex;align-items:center;justify-content:center;}
.paywall-ic svg.ic{width:30px;height:30px;}
.paywall h2{font-size:clamp(26px,3vw,34px);font-weight:700;letter-spacing:-.025em;margin:0 0 10px;}
.paywall .lede{font-size:var(--t-input);color:var(--muted);max-width:52ch;margin:0 auto;line-height:1.6;}
.paywall .price{font-size:var(--t-display);font-weight:700;color:var(--gold);letter-spacing:-.02em;
  margin:24px 0 18px;}
.feat-ic{width:44px;height:44px;border-radius:12px;margin-bottom:14px;
  background:var(--gold-dim);color:var(--gold);display:flex;align-items:center;justify-content:center;}
.feat-ic svg.ic{width:20px;height:20px;}
.card h4{font-size:var(--t-input);font-weight:700;margin:0 0 8px;letter-spacing:-.01em;}
.card h4 + p{margin-top:0;}
.card p{font-size:var(--t-small);color:var(--muted);line-height:1.6;margin:0 0 8px;}

/* A read-only value beside the button that copies it. */
.copyrow{display:flex;gap:10px;align-items:center;flex-wrap:wrap;}
.copyrow .search{min-width:0;}
@media (max-width:560px){.copyrow .btn{width:100%;}}

/* ---- tabs ----
   Text with an underline, not filled pills. The reference uses both - a gold
   segmented pill on one page and underlined text on another - and Chrome
   flagged that as one of its faults. One tab system, and this is it: an
   underline sits under the thing it names and takes no colour away from the
   content below. */
.tabs{display:flex;gap:26px;border-bottom:1px solid var(--border);margin-bottom:22px;
  overflow-x:auto;}
.tab{background:none;border:0;padding:0 0 13px;font-family:var(--font-ui);font-size:var(--t-body);
  font-weight:500;color:var(--muted);cursor:pointer;white-space:nowrap;
  border-bottom:2px solid transparent;margin-bottom:-1px;
  transition:color .15s ease,border-color .15s ease;}
.tab:hover{color:var(--ink);}
.tab.on{color:var(--gold);border-bottom-color:var(--gold);font-weight:600;}
.tab:focus-visible{outline:2px solid var(--gold);outline-offset:3px;}
.panel > .card:first-child{margin-top:0;}

/* ---- import data ----------------------------------------------------------
   Taken from the reference's import wizard: the three dot rail, the four
   column platform grid, the dashed drop zone and the preview table, at its
   measurements. Its --gold-dim is our gold at a tenth, and its --text3 is our
   --faint, so the borrowed rules are written against our own tokens rather
   than a second palette. */
.import-progress{display:flex;align-items:center;justify-content:center;gap:8px;
  margin:0 0 32px;}
.import-step-dot{width:32px;height:32px;border-radius:50%;border:2px solid var(--border);
  display:flex;align-items:center;justify-content:center;font-size:var(--t-small);font-weight:700;
  color:var(--faint);font-variant-numeric:tabular-nums;transition:all .2s ease;}
.import-step-dot.active{border-color:var(--gold);background:var(--gold);color:#fff;}
.import-step-dot.done{border-color:var(--gold);background:var(--gold-dim);color:var(--gold);}
.import-step-line{width:40px;height:2px;background:var(--border);transition:background .2s ease;}
.import-step-line.done{background:var(--gold);}
.import-step-label{font-size:var(--t-micro);color:var(--faint);text-align:center;margin-top:4px;}

.platform-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:20px;}
.platform-card{background:var(--surface);border:2px solid var(--border);border-radius:var(--radius);
  padding:20px 16px;text-align:center;cursor:pointer;transition:all .15s ease;
  display:flex;flex-direction:column;align-items:center;font-family:var(--font-ui);}
.platform-card:hover{border-color:var(--gold-fill);background:var(--gold-dim);transform:translateY(-2px);}
.platform-card.selected{border-color:var(--gold);background:var(--gold-dim);
  box-shadow:0 0 0 3px rgba(201,169,110,.15);}
.platform-card:focus-visible{outline:2px solid var(--gold);outline-offset:3px;}
.platform-card-icon{width:40px;height:40px;border-radius:10px;margin:0 auto 10px;color:#fff;
  display:flex;align-items:center;justify-content:center;}
.platform-card-icon svg{width:22px;height:22px;}
.platform-card-name{font-weight:600;font-size:var(--t-small);color:var(--ink);}

/* The export steps. Numbered because the content is a sequence that cannot be
   done out of order, which is the only thing that earns a number. */
.wiz-steps{list-style:none;margin:0 0 4px;padding:0;display:flex;flex-direction:column;gap:12px;}
.wiz-steps li{display:flex;gap:12px;align-items:flex-start;font-size:var(--t-small);line-height:1.5;}
.wiz-n{flex:0 0 28px;height:28px;border-radius:50%;background:var(--gold);color:#fff;
  display:grid;place-items:center;font-size:var(--t-small);font-weight:700;font-variant-numeric:tabular-nums;}

.import-upload-zone{border:2px dashed var(--border);border-radius:var(--radius);
  padding:40px 20px;text-align:center;cursor:pointer;transition:all .15s ease;margin-top:16px;}
.import-upload-zone:hover,.import-upload-zone.dragover{border-color:var(--gold);background:var(--gold-dim);}
.import-upload-zone:focus-visible{outline:2px solid var(--gold);outline-offset:3px;}
.import-upload-zone svg{width:36px;height:36px;color:var(--faint);margin-bottom:8px;}
.zone-title{font-weight:600;font-size:var(--t-body);margin-bottom:4px;}
.zone-sub{font-size:var(--t-small);color:var(--faint);}

.import-counts{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:16px;}
.import-counts div{padding:16px;background:var(--gold-dim);border-radius:8px;text-align:center;}
.import-counts strong{display:block;font-weight:700;font-size:var(--t-title);color:var(--gold);
  font-variant-numeric:tabular-nums;}
.import-counts span{font-size:var(--t-small);color:var(--muted);font-weight:600;}

.import-preview-table{width:100%;border-collapse:collapse;font-size:var(--t-small);margin:10px 0 4px;}
.import-preview-table th{text-align:left;padding:10px 12px;background:var(--surface-hover);
  font-weight:600;font-size:var(--t-small);text-transform:uppercase;letter-spacing:.5px;
  color:var(--faint);border-bottom:1px solid var(--border);}
.import-preview-table td{padding:10px 12px;border-bottom:1px solid var(--border);}
.import-preview-table .more{color:var(--faint);}

.maps{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:0 18px;
  margin-bottom:8px;}

/* Back on the left, the action on the right, as the reference has it. */
.wiz-foot{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:24px;}

@media (max-width:768px){
  .platform-grid{grid-template-columns:repeat(2,1fr);}
}
@media (max-width:640px){
  .import-step-line{width:22px;}
  .import-counts{grid-template-columns:1fr;}
}

/* ---- the two platform buttons --------------------------------------------
   Their own colours, on the whole button rather than just the glyph.

   I shipped these as quiet outline buttons with a coloured icon first,
   reasoning that two more brand colours would crowd a page that already has
   its own two. He asked for the colours, and on this screen he is right: these
   are the only two buttons in the product that hand the customer over to
   somebody else's login, and looking like Facebook and Instagram is how a
   salon owner knows that before pressing rather than after.

   Instagram has no flat brand colour - its mark is the corner-lit gradient, so
   that is what is used, at its official stops. Facebook is #1877F2 flat.
   White text on both: 4.6:1 on the Facebook blue and higher across every stop
   of the gradient. */
.btn.brand{display:inline-flex;align-items:center;gap:9px;border:0;color:#fff;
  font-weight:600;}
.btn.brand .brand-ic{width:17px;height:17px;flex:0 0 17px;color:#fff;}
/* Instagram, the way Instagram does it.
 *
 * Two wrong turns before this one. The first was the real gradient copied
 * stop for stop, which put an acid yellow on a calm page and shouted. The
 * second was my fix for that: I deleted the warm end, and the button stopped
 * being Instagram at all - what was left was a flat pink to purple, and pink
 * is banned in this brand outright.
 *
 * The warm end IS the recognition. What cannot happen is white text sitting on
 * it: #FDF497 gives white 1.13:1 and #FD5949 gives 3.13:1, both far under AA.
 *
 * Instagram's own answer, and now ours: put the CENTRE OF THE GRADIENT OUTSIDE
 * THE BUTTON, below and to the left. The warm arc then only reaches the bottom
 * left corner, where nothing is written, and the text crosses magenta into
 * purple, every stop of which clears 5:1. The stops are a shade deeper than
 * Meta's so it sits beside a calm blue without winning the argument.
 *
 * Measured, not eyeballed, on a 200 by 42 button: the warm arc dies at 15 per
 * cent, the icon sits at 20, and the first letter lands at 23. So the orange
 * stop has to close by 9 and magenta hold from 21 - my first attempt let it run
 * to 18, which put the C of Connect on an orange blend at about 3.3:1. The icon
 * keeps the orange, being a graphic, where WCAG asks 3:1 and it has 3.66.
 */
.btn.brand.ig{background:radial-gradient(circle at 12% 135%,
  #F5B942 0%, #E86A3A 9%, #C9276B 21%, #A82A9E 55%, #6A3FBE 92%);}

/* #166FE5, not #1877F2. Facebook's own blue gives white 14px text 4.23:1,
   which fails AA - and it is theirs to fail, since their button carries
   larger type. This is their hover blue, one step down, still unmistakably
   theirs at 4.73:1. */
.btn.brand.fb{background:#166FE5;}
/* The lift is the product's own hover, not a colour change: darkening a brand
   colour on hover makes it look like a different brand for a moment. */
.btn.brand:hover{filter:brightness(1.05);
  transform:translateY(-1px);box-shadow:var(--shadow-md);}
.btn.brand:active{transform:translateY(0);filter:brightness(.97);}
.btn.brand:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
@media (max-width:480px){
  .btn.brand{width:100%;justify-content:center;}
  .btn.brand + .btn.brand{margin:10px 0 0 0 !important;}
}

/* A thing to read BEFORE pressing the button under it. Not a banner: a banner
   reports what happened, and this warns what is about to. The gold rule on the
   left is this product's mark for "act on this knowing that". */
.notice{border-left:3px solid var(--gold-fill);background:var(--gold-dim);
  border-radius:0 var(--radius) var(--radius) 0;padding:14px 16px;
  font-size:var(--t-small);line-height:1.6;color:var(--muted);}
.notice strong{display:block;color:var(--ink);font-weight:600;margin-bottom:3px;}

/* ---- the booking sheet ----------------------------------------------------
   A panel over the page rather than another route: making a booking is a thing
   you do WHILE looking at the diary, and sending someone to a separate page
   loses the week they were reading. */
.sheet-scrim{position:fixed;inset:0;z-index:60;background:rgba(20,20,15,.38);
  display:flex;align-items:center;justify-content:center;padding:24px;}
.sheet{background:var(--surface);border-radius:var(--radius);box-shadow:var(--shadow-md);
  width:100%;max-width:560px;max-height:calc(100dvh - 48px);display:flex;flex-direction:column;}
.sheet-head{display:flex;align-items:center;justify-content:space-between;
  padding:20px 24px;border-bottom:1px solid var(--border);}
.sheet-head h3{font-size:var(--t-head);font-weight:600;}
.sheet-x{background:none;border:0;font-size:var(--t-title);line-height:1;color:var(--faint);
  cursor:pointer;padding:0 4px;}
.sheet-x:hover{color:var(--ink);}
.sheet-body{padding:22px 24px;overflow-y:auto;}
.cal-right{display:flex;align-items:center;gap:14px;flex-wrap:wrap;}
@media (max-width:560px){
  .sheet-scrim{padding:0;align-items:flex-end;}
  .sheet{max-width:none;max-height:92dvh;border-radius:var(--radius) var(--radius) 0 0;}
  /* A sheet still fetching its services has an empty body, and an empty body
     on a bottom-anchored sheet collapses to nothing but a title bar - which is
     what the client photographed. A floor keeps it looking like a sheet while
     it loads, so "wait" and "broken" no longer look identical. */
  .sheet-body{min-height:160px;}
}

/* A textarea in a .field, which until now only ever held an input or a select.
   Same border, same focus ring: a note about a client is not a lesser kind of
   value than their phone number. */
.field textarea{width:100%;font-family:var(--font-ui);font-size:var(--t-input);padding:12px 14px;
  border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);
  color:var(--ink);line-height:1.55;resize:vertical;min-height:76px;}
.field textarea:focus{outline:2px solid var(--accent);outline-offset:1px;}
.field textarea::placeholder{color:var(--faint);}

/* ---- services -------------------------------------------------------------
   A price list, not a card grid. A salon has eight or twelve of these and
   reads them the way a price list has always been read: the thing down the
   left, the money down the right, on one baseline. Twelve cards would put the
   same twelve things in a shape nobody scans.

   The rows are divided rather than boxed. They are one list, and a border
   round each would say they were twelve separate objects. */
.svc-list{border-top:1px solid var(--border);}
.svc{display:flex;align-items:center;gap:18px;padding:16px 4px;
  border-bottom:1px solid var(--border);}
.svc-main{flex:1;min-width:0;}
.svc-name{font-weight:600;font-size:var(--t-body);}
.svc-desc{font-size:var(--t-small);color:var(--muted);margin-top:3px;line-height:1.5;}
/* Gold, because this is the money. Tabular figures so the column lines up. */
.svc-price{font-weight:600;font-size:var(--t-input);color:var(--gold);white-space:nowrap;
  font-variant-numeric:tabular-nums;}
/* A service with no price says so, quietly. Never a zero: a service priced at
   nothing is a statement about their business, not about our data. */
.svc-price.none{color:var(--faint);font-weight:400;font-size:var(--t-small);}
.svc-edit{flex:0 0 auto;}

.svc-form{border:1px solid var(--gold-fill);border-radius:var(--radius);
  padding:20px;margin-bottom:14px;background:var(--gold-dim);}
.svc-form .field:last-of-type{margin-bottom:12px;}
/* Delete arms on the first press and confirms on the second. The armed state
   has to look like what it now does. */
.btn.danger{background:var(--danger);border-color:var(--danger);color:#fff;}
.btn.danger:hover{filter:brightness(1.06);}

@media (max-width:560px){
  .svc{flex-wrap:wrap;gap:8px 14px;}
  .svc-main{flex:1 0 100%;}
  .svc-price{margin-right:auto;}
}

/* ---- the assistant's switch ----------------------------------------------
   First thing on the page, because it is the only control there that decides
   whether anything happens at all. Everything under it is voice.

   Green when it is answering: this product's green means working and healthy,
   and an assistant replying to a client at midnight is exactly that. */
.ai-switch{border:1px solid var(--border);border-radius:var(--radius);
  padding:16px 18px;margin-bottom:20px;background:var(--surface);
  transition:border-color .18s ease,background .18s ease;}
.ai-switch:not(.off){border-color:#CBD9C6;background:#F6F9F4;}
.ai-toggle{display:flex;align-items:center;gap:14px;cursor:pointer;margin:0;}
.ai-toggle input{position:absolute;opacity:0;width:0;height:0;}
.ai-track{position:relative;flex:0 0 42px;width:42px;height:24px;border-radius:12px;
  background:var(--border);transition:background .18s ease;}
.ai-knob{position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;
  background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2);transition:transform .18s ease;}
.ai-toggle input:checked + .ai-track{background:var(--accent);}
.ai-toggle input:checked + .ai-track .ai-knob{transform:translateX(18px);}
.ai-toggle input:focus-visible + .ai-track{outline:2px solid var(--accent);outline-offset:3px;}
.ai-words{display:flex;flex-direction:column;gap:2px;}
.ai-words strong{font-size:var(--t-body);font-weight:600;}
.ai-words span{font-size:var(--t-small);color:var(--muted);}

/* ---- what a phone showed the client, 9 September ---------------------------
   Ms. Ashley opened this on her phone and the right-hand side of every row was
   cut off with no way to reach it. Three faults on top of one another, and the
   first is the one that made the other two invisible rather than merely ugly.

   1 · `.shell` is `overflow:hidden`. That is correct for the desktop layout -
       only `.main` is meant to scroll - but it means anything wider than the
       screen is CLIPPED rather than scrollable. A single element forcing width
       silently amputates the page instead of producing a scrollbar somebody
       could use.

   2 · Nothing guaranteed a grid child could shrink. A CSS grid item's minimum
       size is its content, not zero, so one long word or one `min-width` deep
       inside a card pushes its whole column - and therefore the page - wider
       than the viewport. `min-width:0` is what lets a column give way.

   3 · Two stat tiles side by side on a 390px screen is about 150px each once
       padding is taken out, and the figure inside is set at 28px. It was
       cramped before anything overflowed.

   The height also moves to `dvh`: `100vh` on a phone is the viewport WITHOUT
   the browser's own chrome, so a `100vh` shell is always slightly taller than
   the space it has, and the last row sits under the address bar. */

/* Every grid child may shrink. Without this a grid item's floor is its content
   and one wide thing takes the page with it. */
.grid-4 > *, .grid-3 > *, .grid-2 > *, .grid-60-40 > *,
.maps > *, .mgrid > * { min-width: 0; }
.card, .stat { min-width: 0; }

.shell { height: 100dvh; }

@media (max-width: 560px) {
  /* One figure per row. Two of these on a phone is two cramped columns and a
     third of the screen spent on gaps. */
  .grid-4 { grid-template-columns: 1fr; }
  .stat { min-height: 0; }
  .stat .value { font-size:var(--t-display); }

  /* Wide things scroll inside their own container rather than widening the
     page. The page itself must never scroll sideways. */
  .tablewrap, .weekcal, .import-preview-table { max-width: 100%; overflow-x: auto; }

  /* A full-width action beats a button somebody has to aim at. */
  .actions .btn, .wiz-foot .btn { width: 100%; }
  .wiz-foot { flex-direction: column-reverse; gap: 10px; align-items: stretch; }

  .main { padding: 16px 14px 44px; }
  .card { padding: 16px; }
}

/* The last line of defence. If something still overflows, the page scrolls to
   it instead of hiding it - a visible scrollbar is a bug somebody can report,
   and a clipped page is a bug they can only be angry about. */
@media (max-width: 980px) {
  .shell { overflow-x: auto; }
  .main { overflow-x: hidden; }
}

/* ---- the phone scrolls the page, not a box inside it ----------------------
   Everything above this was built as a desktop application shell: a viewport
   -height frame that never scrolls, with the page scrolling inside `.main`.
   That is right on a laptop and wrong on a phone, for three reasons that all
   arrive at the same complaint - "it does not fit the screen".

     1 · A phone browser only collapses its address bar when the DOCUMENT
         scrolls. Ours never does, so the bar stays down for ever and the app
         permanently runs in the smallest viewport the device offers.
     2 · Pull-to-refresh, the swipe-back gesture and momentum scrolling are all
         attached to the document. In an inner scroller they are simply absent,
         and the page reads as dead rather than as short.
     3 · A frame of a fixed height cannot grow for content, so anything taller
         than the frame is clipped by `overflow:hidden` - the cut-off screen.

   Below 980px the rail is already `position:fixed`, so the flex frame is
   holding nothing up. Take it apart and let the document scroll. The top bar
   becomes sticky so it stays where it was without a frame to pin it. */
@media (max-width: 980px) {
  .shell { height: auto; min-height: 100dvh; overflow: visible; }
  .shell > .body { display: block; }
  .main { height: auto; overflow: visible; scrollbar-gutter: auto; }
  .page-topbar { position: sticky; top: 0; }
}

/* The month calendar is seven columns with a 660px floor, and it has no
   wrapper to scroll inside. On a 390px screen that floor drags the whole page
   sideways. Seven columns of 50-odd pixels is tight; a page that is wider than
   the phone is broken. */
@media (max-width: 560px) {
  .mgrid { min-width: 0; }
  .mgrid .cell { min-height: 62px; font-size:var(--t-small); }
}

/* ---- the top bar fits the device, at every device -------------------------
   Measured, not guessed: at 320px and 360px every page in the app scrolled
   sideways, and the same three elements ran off the right edge on all of them
   - the notification bell and the avatar, pushed out by the wordmark.

   The wordmark is an image about ten times wider than it is tall, and it was
   sized by HEIGHT alone (`height:clamp(22px,2.2vw,30px); width:auto`). Height
   has no idea how much room there is, so at 22px tall the image claimed over
   200px of width, and on a 320px screen the hamburger, the padding and the
   image had already spent the bar before the bell was laid out.

   Sizing by max-height and max-width together lets the image take the smaller
   of the two: full size when the bar is wide, proportionally smaller when it
   is not. Nothing is clipped and nothing is pushed off, at any width. */
.page-topbar > * { min-width: 0; }
.top-actions { flex: none; }
.brandmark { min-width: 0; overflow: hidden; }
.brandmark img {
  max-height: clamp(22px, 2.2vw, 30px);
  max-width: 100%;
  height: auto;
  width: auto;
}

@media (max-width: 420px) {
  /* Below this the bar is mostly padding. Buy the wordmark its width back. */
  .page-topbar { padding: 0 12px; gap: 10px; }
  .top-actions { gap: 8px; }
}

/* ---- the drawer, actually off-canvas --------------------------------------
   THE BUG BEHIND THE CUT-OFF SCREENS. The mobile block above sets the rail to
   `position:fixed` and slides it off to the left. Two hundred lines further
   down - and therefore later in the cascade - the collapse animation set
   `.rail{position:relative}` at the same specificity and with no media query
   around it, so it won an argument it did not know it was having.

   The result on a phone: the drawer never left the flow. It sat in the layout
   at its full 270px, taking two thirds of a 390px screen, and the dashboard
   was squeezed into the strip that was left. That is the white band down the
   left of the screenshots and the reason every card looked clipped.

   Restated here, after everything that could override it. */
@media (max-width: 980px) {
  .rail,
  .rail-collapsed .rail {
    position: fixed; left: 0; top: 0;
    width: 270px; flex-basis: 270px;
    height: 100dvh; z-index: 40;
    transform: translateX(-100%);
    padding: 26px 16px;
  }
  .rail.open, .rail-collapsed .rail.open { transform: translateX(0); }
  /* The drawer STARTS BELOW THE TOP BAR. The bar is sticky at z-index 50 and
     the drawer is 40, which is the right way round - the hamburger that closes
     the drawer has to stay on top of it - but it meant the first 34px of the
     drawer were behind the bar, and what sat there was the greeting. The
     customer's own name, half hidden, at the top of the menu. */
  .rail, .rail-collapsed .rail { padding-top: calc(var(--topbar-h) + 20px); }
  /* The collapse control is a desktop affordance; the drawer opens and closes
     with the hamburger here, and a second control that does something subtly
     different is worse than no control. */
  .rail-collapse { display: none; }
}

/* ---- hidden means hidden ---------------------------------------------------
   THE BUG BEHIND THE EMPTY "NEW BOOKING" PANEL, and it was never about the
   panel.

   The `hidden` attribute is nothing but `display:none` in the browser's own
   stylesheet, so ANY display rule we write beats it. `.sheet-scrim` sets
   `display:flex` to centre the sheet, which meant the scrim - a fixed overlay
   covering the whole viewport at z-index 60, with a 38% black wash - was on
   screen and taking every click from the moment the Bookings page loaded.

   What that looked like to Ms. Ashley: a permanently open, permanently empty
   New booking panel over a greyed-out calendar, and no button on the page
   doing anything. Tapping "New booking" hit the scrim rather than the button,
   and the scrim's own handler read that as "dismiss" - so the form was never
   asked for, and the body it would have filled stayed empty.

   Fixed for every element in the app rather than for this one, because the
   same trap is set wherever CSS gives a display to something JavaScript hides:
   the country-code field, the save row on the AI page, the error lines. This
   is the one place `!important` is right - it is restoring the browser's own
   behaviour, not overriding a decision. */
[hidden] { display: none !important; }

/* ---- a link we generated, beside the button that copies it ----------------
   The booking and review links are ours: provisioning creates them, and the
   only useful thing a salon owner can do with one is copy it into their
   Instagram bio. An editable box invited somebody to paste a different address
   over their own and then wonder why no bookings arrived. */
.linkrow { display: flex; gap: 8px; align-items: center; }
.linkrow input { flex: 1 1 auto; min-width: 0; background: var(--ground); }
.linkrow .btn { flex: none; }
@media (max-width: 560px) {
  .linkrow { flex-direction: column; align-items: stretch; }
  .linkrow .btn { width: 100%; }
}

/* The About paragraph is the one answer worth more than half a row. */
.field.full { grid-column: 1 / -1; }
.field textarea {
  width: 100%; min-height: 96px; resize: vertical;
  font: inherit; color: var(--ink);
  padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface);
}
.field textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* A locked section sitting inside a page the customer already owns, rather
   than a full-page paywall. Softer than .paywall on purpose: the page around
   it is theirs, and this is one card of it they have not bought. */
.paywall-inline {
  background: linear-gradient(180deg, var(--gold-tint), var(--surface) 70%);
  /* --gold-tint and --gold both exist; --moss never did, and a bar painted
     with a variable that is not declared is simply invisible. */
  border-color: var(--gold);
}
.paywall-inline .card-head h3 { display: flex; align-items: center; gap: 8px; }
.paywall-inline .card-head svg.ic { width: 15px; height: 15px; color: var(--gold); }
.paywall-inline ul { margin: 0 0 18px; padding-left: 20px; }
.paywall-inline li { margin-bottom: 6px; font-size:var(--t-body); }

/* The deposit sits under the price, quieter than it, because the price is what
   a client is told and the deposit is what they are asked for to hold the slot.
   Two figures the same size on one line read as a contradiction. */
.svc-dep {
  display: block; margin-top: 3px;
  font-size:var(--t-small); font-weight: 500; color: var(--muted);
  letter-spacing: .01em;
}

/* What a client wrote, under their name. Indented and quieter than the name so
   the list still scans as a list of people, with their words attached - rather
   than a wall of quotations somebody has to read all of to find a name. */
.said {
  margin: 8px 0 0; padding: 0 0 0 12px;
  border-left: 2px solid var(--border);
  font-size:var(--t-small); line-height: 1.55; color: var(--muted);
  overflow-wrap: anywhere;
}

/* Counted words. The number is the evidence, so it never leaves the word. */
.themes { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.theme {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 5px 11px; border-radius: 99px;
  font-size:var(--t-small); border: 1px solid var(--border); background: var(--surface);
}
.theme b { font-variant-numeric: tabular-nums; font-size:var(--t-small); color: var(--muted); }
.theme.good { border-color: var(--accent); background: var(--moss-fill); color: var(--accent); }
.theme.good b { color: var(--accent); }
.theme.bad { border-color: var(--danger); color: var(--danger); }
.theme.bad b { color: var(--danger); }

/* A row that leads somewhere. It has to look pressable without becoming a
   button: these are still a chart, and eight outlined buttons in a column stop
   reading as one. So the affordance is the hover and the focus ring only. */
.barrow {
  border: 0; background: none; color: inherit; font: inherit;
  border-radius: 6px; cursor: pointer; text-decoration: none;
  transition: background .12s ease;
}
.barrow:hover { background: var(--ground); }
.barrow:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.barrow[disabled] { cursor: default; opacity: .55; }
.barrow[disabled]:hover { background: none; }
.barrow.on { background: var(--gold-tint); }

/* A button that reads as part of a sentence, for "show everyone" inside a
   line of running text. */
.linkish {
  border: 0; background: none; padding: 0; font: inherit; color: var(--accent);
  text-decoration: underline; cursor: pointer;
}
.linkish:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A message as a client would see it. Deliberately not styled as a card: it is
   a picture of something arriving on a phone, and a card would make it look
   like another section of the settings above it. */
.preview { margin-top: 24px; }
.preview-bubble {
  margin-top: 8px; max-width: 44ch;
  padding: 13px 16px;
  background: var(--moss-fill);
  border-radius: 16px 16px 16px 4px;
  font-size:var(--t-body); line-height: 1.55; color: var(--ink);
}
.preview-bubble mark {
  background: var(--gold-tint); color: var(--gold);
  padding: 0 3px; border-radius: 3px; font-weight: 600;
}
/* An empty offer leaves a visible hole rather than closing up, because the
   hole is the thing the artist needs to see. */
.preview-bubble mark:empty { display: inline-block; min-width: 4.5em;
  border-bottom: 2px dotted var(--danger); }
.preview-link { color: var(--accent); font-weight: 600; }
.preview .hint { margin-top: 8px; color: var(--danger); max-width: 52ch; }

/* ---- the wordmark on the pages outside the app shell ----------------------
   Login, signup, reset, onboarding, the legal pages and the error page all sat
   outside the shell and still spelled the old name in letterspaced capitals.
   They carry the real logo now, for the reason the shell already did: the
   colour cannot be approximated. A typeface set in gold is not the mark. */
.wordmark-link { display: inline-block; line-height: 0; margin-bottom: 18px; }
.wordmark-img { height: 26px; width: auto; display: block; }
.auth-card .wordmark-link { margin: 0 auto 14px; }
@media (max-width: 420px) { .wordmark-img { height: 22px; } }

/* ===========================================================================
   GLASS
   ===========================================================================
   Asked for directly: "gawin mong parang glass, dapat naka align dun sa kulay
   ng website na pinag gayahan neto pero mas gawin mong maganda ang mga kulay."

   Two things this is NOT, because both are how glass usually goes wrong on a
   product screen:

     - It is not frosting over a photograph. There is no photograph. Here glass
       means depth built from a tinted ground, a translucent surface and one
       hairline of light along the top edge, which is what makes a real pane
       read as a pane.
     - It is not a new palette. Every colour below is the locked evergreen and
       gold already in this file. What changed is how they carry light.

   The whole point of this screen is a salon owner reading their own money at
   seven in the morning, so nothing here is allowed to cost legibility. Text
   sits on opaque-enough surfaces, every fallback below keeps contrast, and the
   blur is a material rather than an effect.
   ========================================================================= */

:root {
  /* A pane, and the light along its top edge. Both are white at low alpha
     rather than a grey, so the surface warms toward the ground beneath it
     instead of going flat. */
  --glass: rgba(255, 255, 255, .68);
  --glass-strong: rgba(255, 255, 255, .86);
  --glass-edge: rgba(255, 255, 255, .9);
  --glass-line: rgba(61, 107, 53, .10);   /* evergreen, not grey */
  /* Shadows tinted to the ground's hue. A pure black shadow on a warm ground
     reads as dirt under the card. */
  --glass-shadow: 0 1px 2px rgba(58, 52, 34, .04), 0 8px 24px -12px rgba(58, 52, 34, .10);
  --glass-shadow-lift: 0 2px 4px rgba(58, 52, 34, .05), 0 18px 40px -18px rgba(58, 52, 34, .16);
  --blur: saturate(1.5) blur(18px);
}

/* The ground. Two very quiet washes - evergreen from the top left, gold from
   the lower right - fixed so they stay put while the page moves under them.
   At these alphas they are almost subliminal, and that is the intent: the
   surfaces above have something to be translucent ABOUT. */
body {
  background:
    radial-gradient(1100px 560px at 6% -6%, rgba(61, 107, 53, .13), transparent 58%),
    radial-gradient(820px 480px at 98% 40%, rgba(201, 169, 110, .16), transparent 58%),
    radial-gradient(900px 620px at 40% 118%, rgba(61, 107, 53, .07), transparent 60%),
    var(--ground);
  background-attachment: fixed;
}

/* The pane itself. `inset 0 1px 0` is the whole trick: one pixel of light along
   the top edge, which is how the eye reads a sheet of glass rather than a
   rectangle with a shadow. */
.card, .stat {
  background: var(--glass);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border-color: var(--glass-line);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-edge);
}
.card:hover {
  background: var(--glass-strong);
  border-color: rgba(61, 107, 53, .18);
  box-shadow: var(--glass-shadow-lift), inset 0 1px 0 var(--glass-edge);
}

/* The two surfaces that sit OVER moving content, so they blur harder and stay
   more opaque. A translucent bar with type scrolling behind it is unreadable,
   and this bar carries the wordmark and the account. */
.page-topbar, .rail {
  background: var(--glass-strong);
  backdrop-filter: saturate(1.6) blur(22px);
  -webkit-backdrop-filter: saturate(1.6) blur(22px);
  border-color: var(--glass-line);
}
.page-topbar { box-shadow: inset 0 -1px 0 var(--glass-line); }

/* Primary action. A shallow vertical gradient and a lit top edge, so the
   button belongs to the same material as the cards. Contrast is unchanged:
   white on evergreen, which clears AA at every size on this screen. */
.btn {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, .16), rgba(255, 255, 255, 0));
  /* THE GLOW IS TINTED WITH THE BUTTON'S OWN COLOUR, 13 September 2026. The
     grey drop shadow here is the one every card uses, and under a green button
     it reads as a card rather than as something to press. Her app puts a soft
     moss shadow under a moss button, which is what makes it look lifted rather
     than pasted on. The inset highlight stays: it is what gives the top edge
     its sheen. Secondary buttons keep the grey, because they sit ON a surface
     rather than above one. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22), 0 4px 14px rgba(61, 107, 53, .22);
}
.btn:hover { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .28), 0 8px 22px rgba(61, 107, 53, .30); }
.btn.secondary, .btn.secondary:hover { background-image: none; }
/* Pressed state you can feel. One pixel, no more: this is a tool, not a toy. */
.btn:active { transform: translateY(1px); }
.btn.secondary {
  background: var(--glass);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border-color: var(--glass-line);
  box-shadow: inset 0 1px 0 var(--glass-edge);
}
.btn.secondary:hover { background: var(--glass-strong); }

/* Inputs are panes too, or the forms float on top of the material instead of
   being set into it. */
.field input, .field select, .field textarea, input.search {
  background: rgba(255, 255, 255, .62);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border-color: var(--glass-line);
}
.field input:focus-visible, .field select:focus-visible,
.field textarea:focus-visible, input.search:focus-visible {
  background: rgba(255, 255, 255, .96);
}

/* Overlays get the treatment properly, because this is the one place glass is
   doing its original job: something is genuinely behind it. */
.sheet, .menu, .helpbox {
  background: var(--glass-strong);
  backdrop-filter: saturate(1.5) blur(24px);
  -webkit-backdrop-filter: saturate(1.5) blur(24px);
  border: 1px solid var(--glass-line);
  box-shadow: var(--glass-shadow-lift), inset 0 1px 0 var(--glass-edge);
}
.sheet-scrim { background: rgba(26, 26, 26, .28); backdrop-filter: blur(3px); }

/* FALLBACK, and it is not decoration.
   Safari on an older machine, Firefox with backdrop-filter off, a browser in a
   low-power mode, and anybody who has asked their system to reduce
   transparency all land here. Every surface goes solid. Nothing about the
   layout moves, and the contrast is the same or better. */
@supports not (backdrop-filter: blur(1px)) {
  .card, .stat, .page-topbar, .rail, .sheet, .menu, .helpbox,
  .btn.secondary { background: var(--surface); }
  .field input, .field select, .field textarea, input.search { background: var(--surface); }
}
@media (prefers-reduced-transparency: reduce) {
  .card, .stat, .page-topbar, .rail, .sheet, .menu, .helpbox,
  .btn.secondary {
    background: var(--surface);
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .field input, .field select, .field textarea, input.search {
    background: var(--surface); backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  body { background: var(--ground); }
}
@media (prefers-reduced-motion: reduce) {
  .card:hover { transform: none; }
  .btn:active { transform: none; }
}

/* ---- the setup wizard's rail ---------------------------------------------
   Setup was one long scroll and is five steps now. The rail is not decoration:
   the only honest answer to "how much of this is left" is a picture of it, and
   a wizard without one is the reason people abandon wizards. */
.ob-rail {
  display: flex; flex-wrap: wrap; gap: 6px 22px;
  list-style: none; margin: 0 0 28px; padding: 0;
}
.ob-step {
  display: flex; align-items: center; gap: 8px;
  font-size:var(--t-small); color: var(--faint);
}
.ob-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  border: 1.5px solid var(--border); background: transparent;
}
.ob-step.done { color: var(--muted); }
.ob-step.done .ob-dot { background: var(--accent); border-color: var(--accent); }
.ob-step.now { color: var(--ink); font-weight: 600; }
.ob-step.now .ob-dot {
  background: var(--gold); border-color: var(--gold);
  box-shadow: 0 0 0 3px var(--gold-tint);
}
/* On a phone the five labels wrap into a paragraph of grey text. The step you
   are on is the only one that has to be readable there. */
@media (max-width: 560px) {
  .ob-rail { gap: 6px 12px; }
  .ob-step:not(.now) .ob-step-label { display: none; }
}
.ob-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.ob-actions .ob-skip { margin-left: auto; }

/* ---- the customer's own page, embedded ------------------------------------
   A phone-shaped window rather than a full-width band: their booking page is
   opened on a phone by almost everybody who ever sees it, so previewing it at
   desktop width would show them a layout their clients will not get. */
.preview-frame {
  width: 100%; max-width: 420px; margin: 0 auto;
  aspect-ratio: 9 / 16;
  border: 1px solid var(--glass-line); border-radius: 18px;
  overflow: hidden; background: var(--surface);
  box-shadow: var(--glass-shadow-lift), inset 0 1px 0 var(--glass-edge);
}
.preview-frame iframe { width: 100%; height: 100%; border: 0; display: block; }
@media (max-width: 480px) { .preview-frame { aspect-ratio: 3 / 5; } }

/* The one true sentence the locked panel is allowed to say for free. Set
   larger than the list under it, because it is the only line on the card that
   is about this salon rather than about the product. */
.suite-obs {
  font-size:var(--t-input); line-height: 1.5; color: var(--ink);
  max-width: 60ch; margin: 0 0 14px;
}
.suite-asked {
  margin: 0; padding: 12px 14px; border-radius: 8px;
  background: var(--moss-fill); color: var(--accent);
  font-size:var(--t-body); font-weight: 600;
}

/* ===========================================================================
   DARK
   ===========================================================================
   Asked for, with a switch beside the date. Three states, not two: an explicit
   choice writes `data-theme` on the root, and the default writes nothing at
   all - so most people are served by the media query and never touch the
   control.

   It is not an inversion. A salon owner reads this at seven in the morning and
   again at eleven at night, and the palette has to mean the same thing in both:
   gold is still money, evergreen is still healthy. What changes is that both
   have to lift OFF a dark ground rather than sit on a light one, so each moves
   up in lightness and down in saturation. A colour that works on cream and is
   simply reused on charcoal reads as mud.

   Every value below is redefined in the same token that carries it in light,
   so nothing downstream needs a second rule. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink:#EDEBE2; --ground:#121309; --surface:#1A1C13; --surface-hover:#22241A;
    --accent:#8FB489; --gold:#D0A75F; --gold-fill:#B99055;
    --gold-tint:#241E10; --moss-fill:#1B2418;
    --gold-dim:rgba(208,167,95,.10); --gold-soft:#B99055;
    --border:#2C2F22; --muted:#A5A899; --faint:#797C6D;
    --danger:#DB8F84; --ok:#8FB489;
    --shadow:0 1px 3px rgba(0,0,0,.30);
    --shadow-md:0 4px 16px rgba(0,0,0,.42);

    --glass:rgba(30,33,22,.66); --glass-strong:rgba(30,33,22,.86);
    /* The lit edge is a highlight in light and has to stay one here: white at
       6% reads as a rim of light, where a grey line would read as a border. */
    --glass-edge:rgba(255,255,255,.06);
    --glass-line:rgba(143,180,137,.16);
    --glass-shadow:0 1px 2px rgba(0,0,0,.34), 0 8px 24px -12px rgba(0,0,0,.5);
    --glass-shadow-lift:0 2px 4px rgba(0,0,0,.4), 0 18px 40px -18px rgba(0,0,0,.62);
  }
  :root:not([data-theme="light"]) body {
    background:
      radial-gradient(1100px 560px at 6% -6%, rgba(143,180,137,.11), transparent 58%),
      radial-gradient(820px 480px at 98% 40%, rgba(208,167,95,.10), transparent 58%),
      var(--ground);
  }
  /* White on evergreen is the light-mode primary. On a dark ground that button
     glares, so it takes dark text on the lighter green instead - which clears
     AA and stops the one saturated element on the page from shouting. */
  /* THE PRIMARY BUTTON ONLY. Written as `.btn` first, which caught the
     secondary as well and painted dark text on a dark pane - the two week
     arrows on the dashboard went almost invisible. A secondary button is a
     pane like any other and keeps the page's own ink. */
  :root:not([data-theme="light"]) .btn:not(.secondary) { color:#12140C; }
  :root:not([data-theme="light"]) .btn.secondary { color:var(--ink); }
  :root:not([data-theme="light"]) .field input,
  :root:not([data-theme="light"]) .field select,
  :root:not([data-theme="light"]) .field textarea,
  :root:not([data-theme="light"]) input.search { background:rgba(255,255,255,.04); }
  :root:not([data-theme="light"]) .field input:focus-visible,
  :root:not([data-theme="light"]) .field select:focus-visible,
  :root:not([data-theme="light"]) .field textarea:focus-visible,
  :root:not([data-theme="light"]) input.search:focus-visible {
    background:rgba(255,255,255,.08);
  }
  :root:not([data-theme="light"]) .sheet-scrim { background:rgba(0,0,0,.55); }
  /* The wordmark is a PNG cut for a light ground. Lifting it slightly stops it
     going flat against charcoal without touching its colour. */
  :root:not([data-theme="light"]) .brandmark img,
  :root:not([data-theme="light"]) .wordmark-img { filter:brightness(1.14); }
}

/* The same set again, for somebody who chose dark while their system is light.
   Duplicated rather than shared because a token defined only inside a media
   query does not exist outside it, which is the classic unreadable-artifact
   bug. */
:root[data-theme="dark"] {
  --ink:#EDEBE2; --ground:#121309; --surface:#1A1C13; --surface-hover:#22241A;
  --accent:#8FB489; --gold:#D0A75F; --gold-fill:#B99055;
  --gold-tint:#241E10; --moss-fill:#1B2418;
  --gold-dim:rgba(208,167,95,.10); --gold-soft:#B99055;
  --border:#2C2F22; --muted:#A5A899; --faint:#797C6D;
  --danger:#DB8F84; --ok:#8FB489;
  --shadow:0 1px 3px rgba(0,0,0,.30);
  --shadow-md:0 4px 16px rgba(0,0,0,.42);
  --glass:rgba(30,33,22,.66); --glass-strong:rgba(30,33,22,.86);
  --glass-edge:rgba(255,255,255,.06);
  --glass-line:rgba(143,180,137,.16);
  --glass-shadow:0 1px 2px rgba(0,0,0,.34), 0 8px 24px -12px rgba(0,0,0,.5);
  --glass-shadow-lift:0 2px 4px rgba(0,0,0,.4), 0 18px 40px -18px rgba(0,0,0,.62);
}
:root[data-theme="dark"] body {
  background:
    radial-gradient(1100px 560px at 6% -6%, rgba(143,180,137,.11), transparent 58%),
    radial-gradient(820px 480px at 98% 40%, rgba(208,167,95,.10), transparent 58%),
    var(--ground);
}
:root[data-theme="dark"] .btn:not(.secondary) { color:#12140C; }
:root[data-theme="dark"] .btn.secondary { color:var(--ink); }
:root[data-theme="dark"] .field input,
:root[data-theme="dark"] .field select,
:root[data-theme="dark"] .field textarea,
:root[data-theme="dark"] input.search { background:rgba(255,255,255,.04); }
:root[data-theme="dark"] .field input:focus-visible,
:root[data-theme="dark"] .field select:focus-visible,
:root[data-theme="dark"] .field textarea:focus-visible,
:root[data-theme="dark"] input.search:focus-visible { background:rgba(255,255,255,.08); }
:root[data-theme="dark"] .sheet-scrim { background:rgba(0,0,0,.55); }
:root[data-theme="dark"] .brandmark img,
:root[data-theme="dark"] .wordmark-img { filter:brightness(1.14); }

/* The switch itself. Same size and shape as the bell beside it, because it is
   the same kind of thing: a control in the bar, not a feature. */
.theme-toggle {
  width:38px; height:38px; flex:none;
  display:flex; align-items:center; justify-content:center;
  border:1px solid var(--glass-line); border-radius:50%;
  background:var(--glass); color:var(--muted);
  cursor:pointer; padding:0;
  transition:color .15s ease, border-color .15s ease, background .15s ease;
}
.theme-toggle:hover { color:var(--ink); border-color:var(--gold); }
.theme-toggle:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.theme-toggle svg { width:17px; height:17px; }
/* Only one of the two is ever drawn, so the button never changes size. */
.theme-toggle .moon { display:none; }
:root[data-theme="dark"] .theme-toggle .moon { display:block; }
:root[data-theme="dark"] .theme-toggle .sun { display:none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .moon { display:block; }
  :root:not([data-theme="light"]) .theme-toggle .sun { display:none; }
}

/* The login card centres its wordmark. It was left-aligned above centred text,
   which reads as a mistake rather than as a choice. */
.auth-card { text-align:center; }
.auth-card .wordmark-link { display:block; margin:0 auto 14px; width:fit-content; }
.auth-card form, .auth-card .field, .auth-card label { text-align:left; }

/* ---- the photo field ------------------------------------------------------
   A square preview beside its controls, because what somebody is choosing is a
   picture and the only useful confirmation is seeing it. Square rather than
   the booking page's own crop: it is a portrait slot, and showing it round
   here would promise a shape the page may not use. */
.photo-field { display: flex; gap: 16px; align-items: flex-start; margin-top: 4px; }
.photo-preview {
  width: 92px; height: 92px; flex: none;
  border: 1px solid var(--glass-line); border-radius: 10px;
  background: var(--glass); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  font-size:var(--t-small); color: var(--faint); text-align: center; padding: 6px;
}
.photo-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; min-width: 0; }
.photo-actions .btn { min-height: 38px; padding: 8px 14px; }
.photo-msg { width: 100%; margin: 0; }
@media (max-width: 480px) {
  .photo-field { flex-direction: column; }
  .photo-actions .btn { flex: 1 1 auto; }
}

/* ---- very wide screens ----------------------------------------------------
   Van's monitor is an ultrawide and nothing above 1440px had ever been looked
   at. There was no overflow - the layout simply kept stretching, so at 3440px
   the content ran to 3192px: a sentence in an empty state floating alone in a
   metre of white, and table columns with a hand's width between them.

   Assuming the wide end is fine because the narrow end now is would be the
   same mistake facing the other way. The first phone test is what broke this
   product in front of the client.

   1600px because that is where this particular page stops improving: four stat
   tiles, a table that still breathes, and reading lines that stay near the 65
   characters prose wants. The ground keeps going, so the screen does not read
   as a narrow app pinned in a void - the tinted washes are fixed to the
   viewport and fill what the content no longer does. */
/* ---- very wide screens, take two -----------------------------------------
   The first attempt capped the app at 1848px and centred it, which put empty
   margins down both sides of an ultrawide monitor. Van's objection was the
   right one: that is letterboxing, not responding. A layout that refuses the
   screen it was given is not adapting to it.

   So nothing is capped. What changes at width is the number of COLUMNS, which
   is what the extra space is actually for. The two things that genuinely
   suffer when stretched are prose and tables, and both are handled where they
   live: an empty state holds its reading measure, a table has its own
   scrolling container.

   1700px is where a 4-across stat row plus two full-width cards starts leaving
   more space than it uses. Above it those cards pair up; above 2400 the pair
   becomes the same shape at a comfortable size rather than two very wide
   boxes. */
@media (min-width: 1700px) {
  /* THE TWO CARDS SHARE A BOTTOM EDGE. `align-items: start` let each one be
     as tall as its own contents, which is invisible while they happen to
     match and obvious the moment they do not: one client in "where they came
     from" against an empty Referrals panel leaves a card ending a hundred
     pixels above its neighbour, and a row of cards that does not line up
     reads as a page that failed to load rather than as a page with less to
     say on one side. Stretching costs nothing - the sources list sits at the
     top of its card and the empty state centres itself in whatever height it
     is given. */
  .dash-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    /* 20px, not 16, so this row's gutter is the same width as the one below
       it. See the note under .grid-60-40 in this block. */
    gap: 20px;
    align-items: stretch;
  }
  .dash-pair > .card.mt { margin-top: 0; }

  /* ONE SEAM DOWN THE PAGE, not two.
     The four cards at the foot are two rows of two, and the rows were split
     differently: this pair evenly, the lists below it 60/40. Measured at
     1901px the top gutter fell at x=1059 and the bottom one at x=1236 - 177px
     apart, with a 16px gap above and a 20px gap below. Four cards, four edges,
     and none of the vertical ones lining up. That is what made the foot of the
     dashboard look disordered even once the heights matched.
     An even split for both rows puts one continuous gutter down the middle.
     This was already the decision above 2200px; there is no reason it starts
     there rather than here, where the room appears. */
  .grid-60-40 { grid-template-columns: 1fr 1fr; }
  /* Six figures across instead of four. The row was built for a laptop and
     there is room for the whole set without any of them shrinking. */
  .grid-4 { grid-template-columns: repeat(4, 1fr); }
}
/* The even split for .grid-60-40 used to start here, at 2200px. It starts at
   1700px now, in the block above, so that it lines up with the pair over it.
   Nothing else changed at this width. */

/* ===========================================================================
   DENSITY
   ===========================================================================
   Measured before it was changed, which is the only way this is honest: every
   card on the dashboard was carrying 68 to 112 pixels of nothing, and the four
   stat tiles were half empty - 191px tall around 79 to 119px of content.

   That is what reads as unfinished. Not the colours and not the type: air
   where information should be. A salon owner opening this between clients
   should see their week without scrolling, and every pixel of padding pushes
   something real below the fold.

   Nothing here removes content or shrinks type. It removes reserved space that
   was never filled. */

/* The tiles stop reserving a height. They still line up, because grid items
   stretch to the tallest in the row - so the row is now as tall as its
   fullest tile rather than as tall as a number somebody once typed. */
.stat { min-height: 0; }
.stat label { margin-bottom: 8px; }
.stat .under { margin-top: 5px; }
.stat-act { padding-top: 12px; }

/* 44px above and below a single sentence is 88px of held breath. An empty
   state should look composed and finished, which is a matter of centring and
   restraint rather than of distance. */
.state { padding: 26px 20px; }
.state-title { margin-bottom: 4px; }
/* And it stops stretching. One sentence across 900px is not a sentence, it is
   a line the eye loses halfway. Set on the block itself, because most of the
   text in here is a bare text node with no element to constrain. */
.state { max-width: 52ch; margin-inline: auto; }

.card { padding: 20px 22px; }
.card-head { margin-bottom: 14px; }
.mt { margin-top: 16px; }

/* The one place air is kept. A figure needs room around it or it stops being
   the thing you read first, and these four are the reason the page exists. */
.stat .value { line-height: 1; }
.statline { gap: 14px; }

@media (max-width: 560px) {
  .card { padding: 16px; }
  .state { padding: 22px 14px; }
}

/* The week, on one line. Left says when, right says what you can do about it -
   which is the order somebody reads a toolbar, and it fits in the height of a
   single button instead of a hundred and fifty pixels. */
.weekbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}
.weekbar-when strong { display: block; font-size:var(--t-input); font-weight: 600; }
.weekbar-when span { display: block; font-size:var(--t-small); color: var(--muted); margin-top: 2px; }
.weekbar-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
@media (max-width: 560px) {
  .weekbar-acts { width: 100%; }
  .weekbar-acts .btn { flex: 1 1 auto; }
}

/* ---- reading measure ------------------------------------------------------
   The layout takes the whole screen, and it should. Prose cannot: on a 3440px
   monitor a single sentence in Settings ran to 3067 pixels, which is a line the
   eye cannot track back from - you finish it and lose which one you were on.

   So the constraint goes on the WORDS, not on the page. Cards, tables, grids
   and toolbars keep every pixel; a paragraph stops at about seventy-five
   characters, which is where prose has stopped for five hundred years.

   Deliberately not applied to table cells, labels or anything inside a control:
   those are scanned, not read, and a cell that refuses to fill its column
   leaves a ragged table. */
/* Written as direct children first, which caught almost nothing: most of these
   sit two or three levels down inside a field or a group. Depth-independent
   now, and safe to be - max-width only ever shrinks a box, so a hint already
   inside a narrow column is untouched. */
.card p,
.hint,
.ob-note,
.onb-group-blurb,
.topbar .sub,
.state,
.notice { max-width: 75ch; }

/* One exception, and it is the reason for the rule: text that is centred in a
   card is centred against the card, so it needs the measure AND the margins or
   it sits off to the left of the space it is meant to be middle of. */
.state { margin-inline: auto; }


/* ---------------------------------------------------------------------------
 * The five client views.
 *
 * Client Management organises into All, New, Loyal, Cold, Inactive and VIP -
 * asked for on 11 September 2026, and the shape the original template uses.
 * All is the table above; these are the person-rows the other five are made
 * of: an initials disc, a name, one line of figures, and at most one button.
 *
 * The disc's colour carries the view's meaning and nothing else - green for
 * somebody new, gold for a regular, warm for gone quiet, red for missed. No
 * pink, no rose: the brand palette is gold and moss.
 * ------------------------------------------------------------------------- */
.vhead{padding:2px 0 16px;border-bottom:1px solid var(--border);margin-bottom:4px;}
.vtitle{font-size:var(--t-head);font-weight:600;color:var(--ink);letter-spacing:-.01em;}
.vsub{margin-top:5px;font-size:var(--t-small);color:var(--muted);display:flex;
  flex-wrap:wrap;align-items:center;gap:7px;}
.vnote{margin:14px 0 4px;font-size:var(--t-small);line-height:1.65;color:var(--faint);
  max-width:72ch;}

.pcard{display:flex;align-items:center;gap:14px;padding:15px 2px;
  border-bottom:1px solid var(--border);}
.pcard:last-child{border-bottom:0;}
.pav{flex:0 0 38px;width:38px;height:38px;border-radius:50%;display:flex;
  align-items:center;justify-content:center;font-size:var(--t-small);font-weight:600;
  letter-spacing:.02em;background:var(--surface-hover);color:var(--muted);}
.pav.green{background:var(--moss-fill);color:var(--accent);}
.pav.gold{background:var(--gold-dim);color:var(--gold);}
.pav.warn{background:var(--gold-tint);color:var(--gold);}
.pav.bad{background:#FBEAE6;color:var(--danger);}
.pbody{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px;}
.pname{font-size:var(--t-body);font-weight:600;color:var(--ink);text-decoration:none;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.pname:hover{color:var(--accent);text-decoration:underline;}
.pmeta{font-size:var(--t-small);color:var(--muted);line-height:1.55;}
.pmeta .sep{color:var(--border);}
.pcard .pact{flex:0 0 auto;white-space:nowrap;}
.pcard .badge{flex:0 0 auto;}

/* The phone. A name, a line of figures and a button do not fit across 390px,
   and the button is the part that must not shrink - so the row becomes two,
   with the action under what it acts on rather than squeezed beside it. The
   same treatment the campaign foot already needed. */
@media (max-width:560px){
  .pcard{align-items:flex-start;flex-wrap:wrap;}
  .pmeta{white-space:normal;}
  /* MEASURED at 375px: `width:100%` plus the 52px indent put the button's
     right edge at 394 on a 344-wide card and pushed the whole document 19px
     sideways. It grows into whatever the indent leaves instead, being the only
     thing on its wrapped line. */
  .pcard .pact{flex:1 1 auto;width:auto;min-width:0;margin-left:52px;
    justify-content:center;}
}

/* THEIR OWN RULES, added 12 September 2026. A repeatable row: heading, the
   words, and a way to take it back out. Stacked on a phone because a heading
   box and a Remove button side by side at 390px leaves neither enough room. */
.policy-row{
  display:grid;
  grid-template-columns:1fr auto;
  gap:0 14px;
  align-items:end;
  padding:16px 0;
  border-top:1px solid var(--border);
}
.policy-row:first-of-type{border-top:0;padding-top:4px;}
.policy-row .field{margin:0;}
.policy-row .field.full{grid-column:1 / -1;margin-top:12px;}
.policy-row .p-remove{flex:none;}
@media (max-width:560px){
  .policy-row{grid-template-columns:1fr;}
  .policy-row .p-remove{margin-top:12px;width:100%;}
}

/* THE LANDING PAGE TAB, 12 September 2026. One row per section of the page, in
   the order a client reads them: a label, what is there, and the way to change
   it. Built to the reference she sent. */
.land-sec{
  display:grid;
  grid-template-columns:130px 1fr auto;
  gap:0 18px;
  align-items:center;
  padding:18px 0;
  border-top:1px solid var(--border);
}
.land-sec:first-of-type{border-top:0;padding-top:2px;}
.land-label{
  font-size:var(--t-micro);letter-spacing:.1em;text-transform:uppercase;
  color:var(--muted);font-weight:600;
}
.land-body{min-width:0;}
.land-body.full{grid-column:2 / 3;}
.land-strong{font-weight:600;overflow-wrap:anywhere;}
.land-body textarea{width:100%;}

/* THE GALLERY IS WHAT THEY HAVE, not eight empty boxes. 13 September 2026.
   auto-fill is right here and wrong on the Suite cards: these are 104px
   thumbnails of a set with no fixed count, so a short last row reads as the end
   of the list rather than as something missing. */
.gallery-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(104px,1fr));
  gap:12px;
}
.gal-item{position:relative;margin:0;aspect-ratio:1;border-radius:10px;
  overflow:hidden;background:var(--surface-hover);border:1px solid var(--border);}
.gal-item img{width:100%;height:100%;object-fit:cover;display:block;}
/* Small, and it stays visible on touch where there is no hover to reveal it. */
.gal-x{position:absolute;top:6px;right:6px;width:24px;height:24px;border-radius:50%;
  border:0;cursor:pointer;font-size:16px;line-height:1;
  background:rgba(20,20,15,.62);color:#fff;}
.gal-x:hover{background:rgba(20,20,15,.85);}
.gal-x:focus-visible{outline:2px solid var(--gold);outline-offset:2px;}
.gal-x:disabled{opacity:.5;cursor:default;}
.gal-add{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:14px;}
.gal-add .hint{margin:0;}
.gallery-slot .photo-field{display:block;}
.gallery-slot .photo-preview{width:100%;aspect-ratio:1;}
.gallery-slot .photo-actions{margin-top:8px;}
.gallery-slot .photo-formats{display:none;}

/* A three-column row with a label and a button does not fit a phone, and the
   label is the part that can move without being missed. */
@media (max-width:620px){
  .land-sec{grid-template-columns:1fr;gap:8px 0;}
  .land-body.full{grid-column:1;}
  .land-sec > .btn{justify-self:start;}
}

/* A LINK WE GENERATE, WHILE IT IS STILL BEING GENERATED. Reported 12 September
   as "your feedback link shows blank": a label with no box under it reads as a
   field that failed to draw. Given a visible edge and a tag so it reads as a
   state of the account rather than a hole in the form. */
.genwait{
  border:1px dashed var(--border);
  border-radius:10px;
  padding:12px 14px;
  background:var(--bg-soft, transparent);
}
.genwait-tag{
  display:inline-block;
  font-size:var(--t-micro);letter-spacing:.08em;text-transform:uppercase;
  font-weight:600;color:var(--muted);
}
.genwait-body{margin-top:6px;font-size:var(--t-small);color:var(--muted);line-height:1.5;}

/* ===========================================================================
   BUTTONS, 12 September 2026.
   Reported: "ang pangit ng mga button sa buong web app na to, di mo ba mas
   mapapaganda at may kulay and interactive?"
   He is right, and the reason is worth naming. Almost every button in this
   product is `.btn.secondary` - Copy, Edit, Preview, Manage, Remove, Win back,
   Send reminder - and that class was `background:transparent` with a grey
   hairline. A page of those reads as unstyled rather than restrained: nothing
   looks pressable, nothing responds, and the one green button on screen has no
   family around it.
   THE LANGUAGE THIS PRODUCT ALREADY SPEAKS, kept rather than replaced:
     moss  = an action. Primary buttons are moss and always have been.
     gold  = a selection. Chips and tabs already mark "on" in gold.
   So the colour arriving here is moss, on interaction, and gold is left to
   mean what it already means. One accent per meaning; no third colour invented.
   Calm at rest, because a salon owner reads this at 7am between clients - the
   colour arrives when they reach for the control, not before.
   =========================================================================== */

/* Every button, including the brand ones, can be reached by keyboard and shows
   it. Only `.btn.brand` and `.tab` had a focus ring before this. */
.btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* A press with two pixels of travel: up on hover, down on press. Small enough
   to feel like a button and not like an animation. */
.btn { transition: background-color .15s ease, border-color .15s ease,
                   color .15s ease, box-shadow .15s ease,
                   transform .12s ease, filter .15s ease; }
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(1px); box-shadow: none; }

.btn.secondary {
  /* A surface, not a hole. Transparent let the card show through, which is why
     these read as outlines of buttons rather than buttons. */
  /* HER SECONDARY BUTTON, COPIED EXACTLY. Read out of her app rather than
     approximated: transparent, gold text, a 1.5px gold border, 8px corners.

     I shipped this once with ink text and a gold border, on the grounds that
     gold text measures 3.51 against white and a label wants 4.5. He asked for
     her colours four times. It is his product and his client's design, so it
     is her button, and the measurement is stated rather than used to quietly
     substitute something else.

     One thing that makes it better than the reference and is worth keeping:
     this gold is #a8833f, not the #C9A96E her page uses for fills. Same family,
     but 3.51 against white instead of 2.24 - so these read as gold and are
     half again as legible as the ones they copy. */
  background: transparent;
  color: var(--gold);
  border: 1.5px solid var(--gold);
  border-radius: 8px;
  box-shadow: none;
}
.btn.secondary:hover {
  /* Her hover too: the gold tint behind it, and the gold stays gold. The moss
     hover that was here turned a gold button green under the cursor, which is
     the palette contradicting itself mid-gesture. */
  background: var(--gold-dim);
  border-color: var(--gold);
  color: var(--gold);
  filter: none;
  box-shadow: none;
}
.btn.secondary:active { background: var(--gold-dim); }

/* #C0564F gives white 14px text 4.48:1, which misses AA by a hair. One step
   darker is 5.18:1 and reads as the same red. */
.btn.danger { background: #B54B44; border-color: #B54B44; color: #fff; }
.btn.danger:hover { background: #A8423B; border-color: #A8423B; filter: none; }

/* Chips and tabs keep gold, because gold already means "this is the one you
   are looking at". What they gain is the same press feedback. */
.chip:active { transform: translateY(1px); }
.chip:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* A disabled or working button must not appear to respond to a press. */
.btn[disabled]:hover, .btn.is-loading:hover,
.btn[disabled]:active, .btn.is-loading:active { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .btn, .chip { transition: background-color .15s ease, color .15s ease; }
  .btn:hover, .btn:active, .chip:active { transform: none; }
}

/* NOTHING FALLS BACK TO THE BROWSER'S OWN SIZE.
   Form controls do not inherit font-size - a button or an input with no rule of
   its own renders at 13.3333px, which is where four of the twenty-two sizes
   came from in the first place: somebody saw an odd size, guessed 13.5px, and
   wrote it down. Named rules below still win; this only catches what nothing
   else claims. */
input, textarea, select, button {
  font-family: var(--font-ui);
  font-size: var(--t-body);
}

/* ===========================================================================
   THE LOCKED MARKETING SUITE, 13 September 2026, built to the original
   template: a lock, the price, what is included, and a close.
   Calm rather than loud - this is a page a salon owner meets between clients,
   and the reference's own restraint is the half worth copying.
   =========================================================================== */
.suite-lock{
  text-align:center;
  padding:44px 24px 40px;
  border:1px solid var(--border);
  border-radius:var(--radius);
  background:var(--surface);
}
.suite-lock-ic{
  display:inline-flex;align-items:center;justify-content:center;
  width:54px;height:54px;border-radius:14px;
  background:var(--gold-tint);color:var(--gold);
}
.suite-lock h2{
  font-family:var(--font-ui);font-weight:600;font-size:var(--t-title);
  margin:18px 0 0;letter-spacing:-.01em;
}
.suite-strap{color:var(--muted);margin:8px auto 0;max-width:46ch;}
.suite-price{
  font-weight:700;font-size:var(--t-display);color:var(--gold);
  letter-spacing:-.02em;margin:22px 0 0;
}
.suite-price span{font-size:var(--t-input);font-weight:500;color:var(--muted);}
.suite-lock .hint{margin-top:4px;}
.suite-lock .btn{margin-top:20px;}

.suite-eyebrow{
  text-align:center;margin:40px 0 16px;
  font-size:var(--t-micro);letter-spacing:.14em;text-transform:uppercase;
  color:var(--gold);font-weight:600;
}
/* SIX CARDS, LAID OUT AS SOMEBODY CHOSE. Rewritten 13 September 2026.
 *
 * This was repeat(auto-fit,minmax(260px,1fr)), which fills the row with as many
 * as fit and leaves whatever is over on its own. Measured across the widths
 * people actually use: 1728 gave FIVE in a row and one orphan underneath, 1440
 * gave four and two. Neither is a layout, and the orphan is what reads as
 * broken - a card sitting alone beside two empty columns looks like something
 * failed to load.
 *
 * Six divides by one, two and three, so those are the only counts allowed. The
 * ladder is 1 on a phone, 2 on a tablet, 3 from a laptop up, and it never goes
 * to four however wide the screen is. Three is also what the reference does.
 *
 * A wider screen makes the CARDS bigger rather than adding columns, which is
 * the right trade for six blocks of reading. */
.suite-grid{
  display:grid;gap:16px;
  grid-template-columns:1fr;
  align-items:stretch;
}
@media (min-width:620px){ .suite-grid{grid-template-columns:1fr 1fr;} }
/* 1180, not 1060, and the rail is why. A media query measures the VIEWPORT,
   but these cards live inside the content column, which is 270px of rail and
   32px of padding narrower. At a 1060px viewport three columns come out at
   236px each, which is too narrow for a heading and five bullets. At 1180 they
   are 274px - measured - and that reads. */
@media (min-width:1180px){ .suite-grid{grid-template-columns:repeat(3,1fr);} }
/* The cards fill their cell, so a short card and a long one in the same row
   have the same edge. The raggedness was measured at 1024: 321px beside 268px. */
.suite-card{padding:22px 22px 20px;margin:0;height:100%;
  display:flex;flex-direction:column;}
.suite-card ul{flex:1 0 auto;}
.suite-ic{
  display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;border-radius:9px;
  background:var(--moss-fill);color:var(--accent);
}
.suite-card h4{
  font-family:var(--font-ui);font-weight:600;font-size:var(--t-head);
  margin:14px 0 10px;
}
.suite-card ul{margin:0;padding-left:18px;color:var(--muted);}
.suite-card li{margin-bottom:6px;line-height:1.5;}
.suite-card-foot{
  margin:12px 0 0;color:var(--gold);font-size:var(--t-small);font-style:italic;
}

/* Built, and empty until somebody supplies figures they can defend. See
   SUITE_RESULTS in marketing.js for why the template's own numbers are not
   here. */
.suite-results{
  display:grid;gap:16px;margin-top:24px;
  grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
}
.suite-res{
  text-align:center;padding:20px 14px;
  border:1px solid var(--border);border-radius:var(--radius);
  background:var(--surface);
}
.suite-res .v{font-weight:700;font-size:var(--t-title);color:var(--accent);}
.suite-res .k{margin-top:4px;color:var(--muted);font-size:var(--t-small);}

.suite-close{text-align:center;padding:32px 24px;margin-top:24px;}
.suite-close h3{font-size:var(--t-head);margin-bottom:6px;}
.suite-close .btn{margin-top:16px;}

/* The locked Create campaign button: a lock, and a press that goes to the
   Suite rather than doing nothing. */

@media (max-width:560px){
  .suite-lock{padding:32px 18px 30px;}
  /* One column is the DEFAULT now, not a phone exception. See .suite-grid. */
  .suite-lock .btn,.suite-close .btn{width:100%;}
}

/* The Suite header's two controls stack on a phone. Measured at 375: "Create
   campaign" with its lock wrapped onto a second line beside "Connect accounts",
   and a button whose label breaks in half reads as a fault rather than a
   tight fit. */
@media (max-width:560px){
  .suite-acts{flex-wrap:wrap;}
  .suite-acts > .btn{flex:1 1 100%;justify-content:center;}
}

/* ---------------------------------------------------------------------------
   WHO THE BOOKING IS FOR, added 13 September 2026.

   A two-way switch inside the New booking sheet, and the list of matches under
   the search box. Reported on the call: booking a client they already have
   meant retyping their name, their email and their phone number, and a typo in
   any of the three left the salon holding two records for one person.

   Gold marks the chosen one, the same as `.tab`, because in this product gold
   is always what is selected and moss green is always what acts.
   --------------------------------------------------------------------------- */
.seg{display:inline-flex;gap:0;border:1px solid var(--border);border-radius:8px;
  overflow:hidden;margin:0 0 14px;background:var(--surface);}
.seg-b{background:none;border:0;padding:9px 16px;cursor:pointer;
  font-family:var(--font-ui);font-size:var(--t-small);color:var(--muted);
  transition:background .15s ease,color .15s ease;}
.seg-b + .seg-b{border-left:1px solid var(--border);}
.seg-b:hover{background:var(--surface-hover);color:var(--ink);}
.seg-b.is-on{background:var(--gold-tint);color:var(--gold);font-weight:600;}
.seg-b:focus-visible{outline:2px solid var(--gold);outline-offset:-2px;}

.nb-found{display:flex;flex-direction:column;margin-top:6px;}
.nb-found:empty{display:none;}
.nb-hit{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  text-align:left;background:none;border:0;border-bottom:1px solid var(--border);
  padding:10px 2px;cursor:pointer;font-family:var(--font-ui);
  font-size:var(--t-small);color:var(--ink);}
.nb-hit:last-child{border-bottom:0;}
.nb-hit:hover{background:var(--surface-hover);}
.nb-hit span{color:var(--muted);font-size:var(--t-micro);}
.nb-hit:focus-visible{outline:2px solid var(--gold);outline-offset:-2px;}

/* ---------------------------------------------------------------------------
   THE TOP ROW ON AN IPAD, fixed 13 September 2026.

   Reported on the call: "on the iPad it's not aligned, the boxes are all over
   the place, and it changes when I zoom."

   Measured rather than guessed. Below 980px the rail becomes an overlay and the
   four figures go two-up, which is right. Above 980px they go four-up - but the
   rail is still 270px of inline column, so on an iPad Pro held upright, at
   1024px, the content area is 744px and each tile gets 159px. Four tall narrow
   slivers 238px high. That is the disorder she is describing, and it runs from
   981px to about 1280px, which is exactly the band every tablet sits in.

   Browser zoom is the same fault seen a second way: zooming changes the CSS
   pixel width of the viewport, so zooming in on a desktop walks the page into
   this same band and the row rearranges. One breakpoint fixes both.

   1280px is where four tiles stop being cramped, with the rail taken off first:
   1280 - 270 rail - 32 padding = 978 of content, less 60 of gaps, is 229px a
   tile. Below that they go two-up, which at 1024 gives two tiles of 351px.
   Both figures are measured in the harness, not calculated and hoped for.
   --------------------------------------------------------------------------- */
@media (min-width:981px) and (max-width:1279px){
  .grid-4{grid-template-columns:1fr 1fr;}
}

/* ---------------------------------------------------------------------------
   WHEN THEY TAKE BOOKINGS, 13 September 2026.

   Asked for on the call: "is there a section where they can put their
   operating hours?" There was one, in the one-time setup flow, and nowhere
   else - so once a salon finished setup their hours could not be changed
   again. It is now on Settings, Booking Page, and these rules are the ones
   from onboarding.html so the two screens cannot drift apart.

   Three columns down the list, aligned, because a salon scans this to find the
   one day that is wrong.
   --------------------------------------------------------------------------- */
.hours{border-top:1px solid var(--border);}
.hrow{display:grid;grid-template-columns:1fr auto auto;align-items:center;
  gap:14px;padding:11px 2px;border-bottom:1px solid var(--border);}
.hday{display:flex;align-items:center;gap:10px;font-size:var(--t-body);cursor:pointer;
  user-select:none;margin:0;font-weight:500;}
.hday input{width:16px;height:16px;accent-color:var(--accent);cursor:pointer;margin:0;}
.htimes{display:flex;align-items:center;gap:8px;}
.htimes input[type=time]{font-family:var(--font-ui);font-size:var(--t-body);padding:7px 9px;
  border:1px solid var(--border);border-radius:8px;background:var(--surface);
  color:var(--ink);width:auto;}
.htimes input[type=time]:focus{outline:2px solid var(--accent);outline-offset:1px;}
.hto{font-size:var(--t-small);color:var(--muted);}
/* A closed day keeps its times, greyed, rather than losing them: a salon that
   shuts on Mondays for a month should not retype nine to five later. */
.hrow.off .htimes{opacity:.35;}
.hclosed{font-size:var(--t-small);color:var(--faint);visibility:hidden;min-width:44px;text-align:right;}
.hrow.off .hclosed{visibility:visible;}
/* A SMALL PHONE PUTS THE TIMES ON THEIR OWN LINE.
   Measured 13 September 2026: a day name, two time boxes and the word "to" need
   about 400px on one row, and an iPhone SE gives 320. Three across pushed the
   second time box past the edge of the screen and took the whole page with it,
   so the page scrolled sideways on every phone. The day and its Closed label
   stay on the first line - that is the pair a person reads together - and the
   times drop underneath with room to breathe. */
@media (max-width:460px){
  .hrow{grid-template-columns:1fr auto;gap:6px 10px;}
  .htimes{grid-column:1 / -1;justify-content:flex-start;}
  .htimes input[type=time]{flex:1 1 0;min-width:0;}
}

/* THIRTY DAYS AS THIRTY BARS. Added 14 September 2026 with the two KPI panels
   asked for on the 13th. Plain elements rather than a chart library: thirty
   bars is not a chart problem, and pulling a library onto the dashboard's
   critical path for two small panels makes the page slower for everybody.
   A day with nothing in it keeps a faint 2% stub rather than vanishing, so the
   run of quiet days is visible AS quiet days. */
.spark{display:flex;align-items:flex-end;gap:2px;height:56px;margin-top:4px;}
.spark-b{flex:1 1 0;min-width:0;background:var(--accent);border-radius:2px 2px 0 0;}
.spark-b.zero{background:var(--border);}
/* A day somebody was unhappy, marked rather than averaged away - the half of
   "trends over time" that a taller bar cannot say on its own. */
.spark-b.bad{background:var(--danger);}
.big{font-size:26px;font-weight:700;line-height:1.1;font-variant-numeric:tabular-nums;}

/* WHAT A CAMPAIGN IS ABOUT TO DO, in figures, directly above its Send button.
   Added 14 September 2026 with the composer. Given weight deliberately: it is
   the one line on that sheet a salon owner must not skim, because the people
   it counts are their own clients. */
.cm-sum{margin-top:20px;padding:14px 16px;border-radius:9px;
  background:var(--gold-dim);border:1px solid var(--gold);
  font-size:var(--t-body);line-height:1.55;}
.cm-sum strong{font-size:1.15em;color:var(--ink);}

/* THE SECOND HALF OF ONE CARD, not a second card.
   Added 14 September 2026 after "the booking and landing page are repetitive":
   a salon's own rules used to live in a card of their own directly beneath the
   card about rules, which read as the same subject asked twice. A rule and a
   divider are enough to separate a subject from its continuation. */
.rule-more{margin-top:26px;padding-top:22px;border-top:1px solid var(--border);}

/* A country code and a number are ONE answer, so they sit on one row. Added
   13 September 2026 with the country list on every phone field in the product.
   The select is given a fixed share rather than sized to its longest option:
   "United Arab Emirates, +971" would otherwise push the number box off a
   phone.

   STACKS ONLY BELOW 360px, not below 420px. At 420 an iPhone at 375 or 390
   split the pair onto two lines - which is the very thing reported on
   14 September 2026, "yung country code number nasa baba ng phone number na
   dapat magkasama", just at a narrower width. A select truncates its label
   gracefully; a code separated from its number does not read as one answer at
   any width. 359px is the genuinely small phone, where two controls on a line
   stop fitting at all. */
.tel-row{display:flex;gap:10px;align-items:stretch;}
.tel-row .tel-cc{flex:0 1 44%;min-width:0;}
.tel-row input{flex:1 1 56%;min-width:0;}
@media (max-width:359px){
  .tel-row{flex-direction:column;}
  .tel-row .tel-cc,.tel-row input{flex:1 1 auto;}
}

/* ---- EMBEDDED IN THE CONSOLE (app.js EMBED) -------------------------------
   The console has its own rail and bar, so these go. The page grows to its
   content rather than scrolling inside a 100vh shell, so the height it
   reports to the console is the real one and there is no scroll in a scroll. */
html[data-embed] .rail,
html[data-embed] .rail-scrim { display: none !important; }
/* The bar stays for one control only: the reporting window, which every
   figure below depends on. The wordmark, menu, theme, bell and avatar belong
   to the console now. */
html[data-embed] .page-topbar {
  background: transparent !important; box-shadow: none !important; border: 0 !important;
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
  height: auto !important; min-height: 0 !important; padding: 0 0 12px !important;
  justify-content: flex-end !important; position: static !important;
}
html[data-embed] .page-topbar .hamburger,
html[data-embed] .page-topbar .brandmark,
html[data-embed] .page-topbar .theme-toggle,
html[data-embed] .page-topbar .bell,
html[data-embed] .page-topbar .avatar { display: none !important; }
html[data-embed] .shell,
html[data-embed] .shell > .body,
html[data-embed] .col,
html[data-embed] .main { height: auto !important; min-height: 0 !important; overflow: visible !important; }
html[data-embed] body { background: transparent !important; }
html[data-embed] .main { padding: 0 0 24px !important; }

/* Campaign performance (Marketing Dashboard, 11 Nov 2026). Built from the
   existing tokens; the only new mark is the warn tint on a row that crossed a
   deliverability line, so a bad send is visible without reading every rate. */
.mkt-bar{flex-wrap:wrap;gap:12px;margin-bottom:8px;}
.mkt-note{margin:4px 0 20px;max-width:72ch;}
.mkt-tiles{margin-bottom:20px;}
.mkt-form{margin-bottom:20px;}
.mkt-alerts ul{margin:6px 0 0;padding-left:18px;}
.mkt-sub{font-weight:400;color:var(--muted);font-size:var(--t-micro);margin-top:2px;}
.mkt-table tr.mkt-warn td{background:var(--gold-tint);}
.mkt-status{display:inline-block;padding:2px 10px;border-radius:999px;font-size:var(--t-micro);border:1px solid var(--border);color:var(--muted);white-space:nowrap;}
.mkt-status.mkt-sent{color:var(--ok);border-color:currentColor;}
.mkt-status.mkt-scheduled,.mkt-status.mkt-approved{color:var(--gold);border-color:currentColor;}
.mkt-err{color:var(--danger);font-size:var(--t-small);margin:8px 0 0;}
.mkt-import{cursor:pointer;}
@media (max-width:900px){.mkt-tiles.grid-4{grid-template-columns:repeat(2,1fr);}}
@media (max-width:520px){.mkt-tiles.grid-4{grid-template-columns:1fr;}}

.mkt-ads{margin-top:20px;}

/* Campaign calendar (11 Nov 2026). A month grid on desktop; below 760px it
   becomes a list of only the days that have a send, because seven columns of
   chips do not fit a phone. */
.mkcal{margin-bottom:20px;}
.mkcal-head{flex-wrap:wrap;gap:12px;}
.mkcal-move{gap:12px;align-items:center;}
.mkcal-move strong{min-width:140px;text-align:center;font-variant-numeric:tabular-nums;}
.mkcal-legend{display:flex;gap:8px;flex-wrap:wrap;margin:4px 0 12px;}
.mkcal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));border-top:1px solid var(--border);border-left:1px solid var(--border);}
.mkcal-h{font-size:var(--t-micro);letter-spacing:.08em;text-transform:uppercase;color:var(--faint);padding:8px;border-right:1px solid var(--border);border-bottom:1px solid var(--border);}
.mkcal-cell{min-height:92px;padding:6px;border-right:1px solid var(--border);border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:4px;min-width:0;}
.mkcal-pad{background:var(--surface-hover);}
.mkcal-cell.today .mkcal-day{color:var(--accent);font-weight:700;}
.mkcal-day{font-size:var(--t-small);color:var(--muted);}
.mkcal-dow{display:none;}
.mkcal-chip{display:block;width:100%;text-align:left;font:inherit;font-size:var(--t-micro);line-height:1.3;padding:4px 6px;border-radius:6px;
  border:1px solid var(--border);background:var(--surface);color:var(--ink);cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.mkcal-chip:hover{background:var(--surface-hover);}
.mkcal-chip:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}
.mkcal-chip.mkt-sent{border-color:var(--ok);}
.mkcal-chip.mkt-approved,.mkcal-chip.mkt-scheduled{border-color:var(--gold);}
.mkcal-chip.mkt-draft{border-style:dashed;}
.mkcal-ch{color:var(--muted);}
.mkcal-undated{margin-top:12px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;}
.mkcal-undated .mkcal-chip{width:auto;}
@media (max-width:760px){
  .mkcal-grid{display:block;border:0;}
  .mkcal-h,.mkcal-pad,.mkcal-cell:not(.has){display:none;}
  .mkcal-cell{min-height:0;border:0;border-bottom:1px solid var(--border);padding:10px 0;}
  .mkcal-dow{display:inline;}
  .mkcal-chip{white-space:normal;}
  .mkcal-move{width:100%;justify-content:space-between;gap:8px;}
  .mkcal-move strong{min-width:0;}
  .mkcal-move .btn{padding-left:12px;padding-right:12px;}
}

/* Approval (11 Nov 2026). */
.mkt-waiting{margin-bottom:20px;}
.mkt-actions{gap:8px;justify-content:flex-end;flex-wrap:nowrap;}
.mkt-approved-line{color:var(--ok);font-size:var(--t-small);margin:0 0 12px;}
.mkt-review{margin-top:20px;padding-top:16px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:10px;}
.mkt-review .row{gap:8px;}

.mkt-report{margin-top:20px;}
.mkt-report .chips{margin-right:8px;}

/* Brand Ambassador Dashboard (12 Nov 2026). */
.mkt-status.amb-complete{color:var(--ok);border-color:currentColor;}
.mkt-status.amb-on_track{color:var(--gold);border-color:currentColor;}
.mkt-status.amb-behind{color:var(--danger);border-color:currentColor;}
.amb-record{margin-bottom:20px;display:flex;flex-direction:column;gap:12px;}
.amb-record h4{margin:12px 0 4px;font-size:var(--t-small);letter-spacing:.06em;text-transform:uppercase;color:var(--muted);}
.amb-crit,.amb-events{margin:0;padding-left:18px;display:flex;flex-direction:column;gap:4px;font-size:var(--t-small);}
.amb-met{color:var(--ok);} .amb-unmet{color:var(--muted);}
.amb-postform{align-items:end;}
#aSearch{min-width:220px;}
@media (max-width:900px){.amb-postform.grid-4{grid-template-columns:repeat(2,1fr);}}
@media (max-width:520px){.amb-postform.grid-4{grid-template-columns:1fr;} .mkt-bar > .row{flex-wrap:wrap;gap:8px;width:100%;} #aSearch{min-width:0;flex:1 1 100%;width:100%;}}

.amb-ask{gap:8px;flex-wrap:wrap;margin-bottom:8px;}
.amb-msg{border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px;display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}
.amb-msg.sent{gap:2px;}
.amb-msg textarea{width:100%;font:inherit;font-size:var(--t-small);line-height:1.5;}

.amb-portal{min-width:280px;max-width:100%;font-size:var(--t-small);}

.amb-views{margin-bottom:16px;}
/* The BeautyRaena wordmark is black; on the dark theme it is drawn white. */
:root[data-theme="dark"] .wordmark-img.br, :root[data-theme="dark"] .brandmark img.br { filter:invert(1); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .wordmark-img.br, :root:not([data-theme="light"]) .brandmark img.br { filter:invert(1); } }
