/* ==========================================================================
   PALABAHAN — shared styles

   Two page layouts share this file:
     <body class="page-auth">  login / register  → centered card
     <body class="page-app">   dashboards        → top bar + content panels

   Design notes
     The app is a laundry service, so the look leans on clean water tones:
     a teal brand, generous whitespace, soft edges, and a very light wash
     behind the content instead of flat grey.

     Every class name used by the PHP pages is kept exactly as it was, so this
     file can be swapped without touching a single page. Nothing here needs a
     web font, a CDN, or JavaScript — it all works offline under XAMPP.

   Contents
     1. Design tokens
     2. Base and typography
     3. Brand mark
     4. Auth pages
     5. App shell
    5b. Sidebar and top navigation
     6. Alerts
     7. Forms
     8. Buttons
     9. Panels and cards
    10. Data display
    11. Status badges
    12. Order tracker (stepper)
   12b. Machine fleet
    13. Utilities
    14. Small screens
    15. Dark mode
    16. Motion and print

   ALWAYS LIGHT (2026-10-18)
     The user asked for every page to share the login pages' soft blue-grey
     ground (#D3D9E1, the --bg / --ice tokens) even on a computer set to dark
     mode. Each dark-theme block is therefore switched off with
     "@media not all" rather than deleted, so the dark palette is still here:
     put "(prefers-color-scheme: dark)" back on those blocks to restore it.
   ========================================================================== */

:root { color-scheme: light; }   /* light form controls and scrollbars too */

/* ==========================================================================
   1. Design tokens
   ========================================================================== */

:root {
    /* Brand — the project's blue, extended into a scale so every shade in the
       app comes from one place rather than being invented per component. */
    --brand:          #2563eb;
    --brand-strong:   #1d4ed8;
    --brand-deep:     #1e3a8a;
    --brand-bright:   #60a5fa;
    --brand-wash:     #dbeafe;
    --brand-mist:     #eff6ff;
    --brand-tint:     rgba(37, 99, 235, .10);
    --brand-line:     rgba(37, 99, 235, .22);
    --brand-ring:     rgba(37, 99, 235, .20);

    /* Ink */
    --text:           #0f172a;
    --text-soft:      #334155;
    --text-muted:     #64748b;
    --text-faint:     #94a3b8;
    --on-brand:       #ffffff;

    /* Surfaces and lines */
    --bg:             #D3D9E1;
    --bg-deep:        #D3D9E1;
    --surface:        #E7ECF3;
    --surface-raised: #E7ECF3;
    --surface-sunken: #E7ECF3;
    --border:         #e2e8f0;
    --border-strong:  #cbd5e1;
    --border-input:   #cbd5e1;
    --topbar-bg:      rgba(255, 255, 255, .82);

    /* Status */
    --success:        #146b47;
    --success-bg:     #e2f3ea;
    --success-line:   rgba(20, 107, 71, .26);
    --danger:         #a51e17;
    --danger-bg:      #fdeae8;
    --danger-line:    rgba(165, 30, 23, .26);
    --warn:           #855000;
    --warn-bg:        #fcf0dc;
    --warn-line:      rgba(133, 80, 0, .26);

    /* Maintenance sits between warn and danger: a machine out of service is
       not an error and not a warning about time, so it keeps its own orange. */
    --maint:          #9c4308;
    --maint-bg:       #fdeade;
    --maint-line:     rgba(156, 67, 8, .26);

    /* App shell metrics. The top bar's height is a token because the sidebar's
       header has to match it exactly — the divider under the wordmark and the
       rule under the top bar are one continuous line across the screen, and
       they only stay that way if both are driven from the same number.
       --control-h is what keeps the bar that height: every control in it (the
       search box, the branch selector, the two round buttons) is this tall, so
       the bar cannot be stretched by whichever one happens to be tallest. */
    /* Sidebar. A navy panel matching the header and footer (it was white
       until 2026-10-18), with the blue --brand as the active pill. Declared
       as its own tokens so the panel can differ from the page around it. */
    --sidebar-bg:       #1A2539;   /* a touch lighter than the header navy (user asked "medyo light", then "dark kaunti") */
    --sidebar-bg-soft:  #283750;
    --sidebar-text:     #cbd5e1;
    --sidebar-text-dim: #94a3b8;
    --sidebar-line:     rgba(255, 255, 255, .10);
    --sidebar-active:   #2563eb;

    /* The wordmark droplet again, drawn light for the dark panel. */
    --mark-light: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.2c4.4 5.3 6.8 8.6 6.8 11.6a6.8 6.8 0 0 1-13.6 0c0-3 2.4-6.3 6.8-11.6z' fill='%23ffffff'/%3E%3Ccircle cx='9.4' cy='14.6' r='1.9' fill='%231e3a8a' opacity='.85'/%3E%3C/svg%3E");

    --topbar-h:       66px;
    --control-h:      38px;

    /* Shape */
    --radius-sm:      9px;
    --radius:         14px;
    --radius-lg:      20px;
    --radius-pill:    999px;

    /* Elevation — a hairline layer plus a soft spread, so edges stay crisp
       while the shadow itself never looks muddy. */
    --shadow-sm:      0 1px 2px rgba(15, 23, 42, .05),
                      0 1px 3px rgba(15, 23, 42, .04);
    --shadow:         0 1px 2px rgba(15, 23, 42, .04),
                      0 10px 28px -10px rgba(15, 23, 42, .18);
    --shadow-lg:      0 2px 4px rgba(15, 23, 42, .04),
                      0 28px 56px -20px rgba(15, 23, 42, .26);

    /* Motion */
    --ease:           cubic-bezier(.32, .72, .32, 1);
    --fast:           .14s;
    --slow:           .26s;

    --font:           "Segoe UI Variable Text", "Segoe UI", Inter, Roboto,
                      -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
    --font-mono:      "Cascadia Mono", Consolas, "Courier New", monospace;

    /* The droplet in the wordmark. Declared as a token so dark mode can swap
       the fill without repeating the path. */
    --mark: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.2c4.4 5.3 6.8 8.6 6.8 11.6a6.8 6.8 0 0 1-13.6 0c0-3 2.4-6.3 6.8-11.6z' fill='%232563eb'/%3E%3Ccircle cx='9.4' cy='14.6' r='1.9' fill='%23ffffff' opacity='.85'/%3E%3C/svg%3E");
}

/* ==========================================================================
   2. Base and typography
   ========================================================================== */

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

html {
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    min-height: 100vh;
    font-family: var(--font);
    font-size: 16px;
    line-height: 1.55;
    color: var(--text);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3 {
    line-height: 1.24;
    letter-spacing: -.014em;
    text-wrap: balance;
}

strong {
    font-weight: 700;
    color: var(--text);
}

a {
    color: var(--brand-strong);
}

/* One focus ring for everything keyboard-reachable, so the app never loses
   the caret on a tab-through. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
    border-radius: 4px;
}

::selection {
    background: var(--brand-tint);
    color: var(--brand-deep);
}

/* ==========================================================================
   3. Brand mark
   ========================================================================== */

.brand {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 8px;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--brand-strong);
}

/* ==========================================================================
   4. Auth pages (login, register)
   ========================================================================== */

.page-auth {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 44px 16px;
    background:
        radial-gradient(720px 420px at 12% 0%, rgba(255, 255, 255, .95) 0%, rgba(255, 255, 255, 0) 60%),
        radial-gradient(680px 420px at 92% 8%, var(--brand-wash) 0%, rgba(219, 234, 254, 0) 62%),
        radial-gradient(520px 320px at 50% 100%, var(--brand-mist) 0%, rgba(239, 246, 255, 0) 70%),
        linear-gradient(165deg, var(--bg-deep) 0%, var(--bg) 52%, var(--surface) 100%);
}

.auth-card {
    position: relative;
    width: 100%;
    max-width: 440px;
    padding: 40px 36px 34px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}

/* A brand rule across the card's top edge — the one flash of colour on an
   otherwise very quiet page. */
.auth-card::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 4px;
    background: linear-gradient(90deg,
        var(--brand-strong) 0%,
        var(--brand-bright) 48%,
        var(--brand-wash) 100%);
}

.auth-card--wide {
    max-width: 560px;
}

/* The welcome screen: one message, one decision, so it is centred and the
   wordmark is allowed to be the largest thing on the page. */
.auth-card--welcome {
    max-width: 470px;
    text-align: center;
}

.auth-card--welcome h1 {
    font-size: 34px;
    letter-spacing: .04em;
}

.auth-card--welcome .subtitle {
    margin-bottom: 24px;
}

.welcome-text {
    margin: 0 0 24px;
    padding: 18px 20px;
    font-size: 15px;
    line-height: 1.6;
    color: var(--text-soft);
    background: var(--brand-mist);
    border: 1px solid var(--brand-line);
    border-radius: var(--radius-sm);
}

.auth-card h1 {
    margin: 0 0 8px;
    font-size: 29px;
    font-weight: 700;
}

.auth-card .subtitle {
    margin: 0 0 28px;
    color: var(--text-muted);
    font-size: 15px;
}

/* The "you are on the Customer door, switch to another" line. Shown to
   everyone identically, so it never hints at which door an account belongs
   to. */
.auth-switch {
    margin: 22px 0 0;
    font-size: 13.5px;
    line-height: 1.7;
    text-align: center;
    color: var(--text-muted);
}

.auth-switch strong {
    color: var(--text-soft);
}

.auth-switch a {
    color: var(--brand);
    font-weight: 650;
    text-decoration: none;
}

.auth-switch a:hover {
    text-decoration: underline;
}

.auth-switch .sep {
    margin: 0 4px;
    color: var(--text-faint);
}

.auth-switch + .auth-footer {
    margin-top: 18px;
}

.auth-footer {
    margin: 26px 0 0;
    padding-top: 20px;
    border-top: 1px solid var(--border);
    text-align: center;
    font-size: 14px;
    color: var(--text-muted);
}

.auth-footer a {
    color: var(--brand);
    font-weight: 700;
    text-decoration: none;
    border-bottom: 1px solid transparent;
    transition: border-color var(--fast) var(--ease), color var(--fast) var(--ease);
}

.auth-footer a:hover {
    color: var(--brand-strong);
    border-bottom-color: currentColor;
}

/* ==========================================================================
   5. App shell (dashboards)
   ========================================================================== */

.page-app {
    background:
        radial-gradient(900px 340px at 50% 0%, var(--brand-mist) 0%, rgba(239, 246, 255, 0) 72%),
        var(--bg);
}

.topbar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 14px 30px;
    background: var(--topbar-bg);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    backdrop-filter: saturate(180%) blur(14px);
    border-bottom: 1px solid var(--border);
}

.topbar .brand {
    margin: 0;
    font-size: 14px;
}

.topbar-user {
    display: flex;
    align-items: center;
    gap: 14px;
    font-size: 14px;
    font-weight: 600;
    color: var(--text-soft);
}

.container {
    max-width: 1200px;   /* was 1040px; widened so the side gaps are smaller (2026-10-18) */
    margin: 0 auto;
    padding: 40px 24px 72px;
}

.page-header {
    margin-bottom: 30px;
}

.page-header h1 {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px;
    margin: 0 0 8px;
    font-size: 30px;
    font-weight: 700;
}

.page-header .subtitle {
    margin: 0;
    max-width: 68ch;
    color: var(--text-muted);
    font-size: 15.5px;
}

.backlink {
    margin: 0 0 20px;
    font-size: 14px;
}

.backlink a {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 5px 12px 5px 10px;
    color: var(--brand-strong);
    font-weight: 600;
    text-decoration: none;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    transition: border-color var(--fast) var(--ease),
                background-color var(--fast) var(--ease);
}

.backlink a::before {
    content: "\2190";
    font-size: 15px;
    line-height: 1;
    transition: transform var(--fast) var(--ease);
}

.backlink a:hover {
    background: var(--brand-wash);
    border-color: var(--brand-line);
}

.backlink a:hover::before {
    transform: translateX(-3px);
}

/* ==========================================================================
   5b. Sidebar and top navigation

   The app shell is two columns: a sticky full-height sidebar and the content
   column, which carries the top bar and <main class="container">.

       body.page-app
         input#app-nav-toggle       (mobile drawer, no JavaScript)
         .app-shell
           .nav-scrim               (tap-to-close, mobile only)
           .sidebar
           .app-main
             .topbar
             .container

   Collapsible sections are <details>/<summary> and the mobile drawer is a
   checkbox, so the whole shell still needs no script — the same promise the
   rest of this file makes. Every colour below comes from the tokens in
   section 1; nothing new is invented here.
   ========================================================================== */

.app-shell {
    display: flex;
    align-items: flex-start;
    min-height: 100vh;
}

.app-main {
    flex: 1;
    min-width: 0;
}

/* --- The sidebar ------------------------------------------------------- */

/* A dark panel, but the app's OWN dark: every colour below is the existing
   teal scale pushed down rather than a new palette, so the sidebar reads as
   the same product as the white content beside it.

   The column does not scroll — it is a fixed frame with the menu scrolling
   inside it, so the wordmark and the account footer stay put however long the
   menu gets. Side padding of 12px, plus the 12px each row carries, puts every
   label in this column on one left edge at 24px. */
.sidebar {
    position: sticky;
    top: 0;
    z-index: 30;
    flex: none;
    display: flex;
    flex-direction: column;
    width: 268px;
    height: 100vh;
    padding: 0 12px 16px;
    overflow: hidden;
    color: var(--sidebar-text);
    background: var(--sidebar-bg);
    border-right: 1px solid var(--sidebar-line);
}

/* Pulled back out to the column's edges so its rule spans the full width and
   meets the top bar's, and held to exactly the top bar's height so the two
   rules are the same line. The 24px padding puts the wordmark on the same left
   edge as the nav labels below it. */
.sidebar-head {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    min-height: var(--topbar-h);
    padding: 0 24px;
    margin: 0 -12px 10px;
    border-bottom: 1px solid var(--sidebar-line);
}

.sidebar-brand {
    margin: 0;
    font-size: 14px;
    color: var(--text);
    text-decoration: none;
}

/* The strip holding the wordmark wears the header's navy, so it lines up with
   the top bar beside it as one band (user request, 2026-10-18); the rest of
   the sidebar keeps its own slightly lighter navy. */
.page-app .sidebar-head {
    background: #0f172a;
}

/* The sidebar wordmark matches the public header's (.portal-mark-name and
   .portal-mark-sub on the navy bar): a bold white name with the pale
   "Multi-Laundry System" line under it. */
.sidebar-brand {
    display: block;
    letter-spacing: 0;
}

.sidebar-brand-name {
    display: block;
    font-size: 20px;
    font-weight: 800;
    letter-spacing: .10em;
    line-height: 1.1;
    color: #ffffff;
}

.sidebar-brand-sub {
    display: block;
    margin-top: 2px;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .19em;
    color: #cbd5e1;
}

/* Any inline SVG in this column that is not a menu glyph — the drawer's close
   button is the one — gets a size here. This is a floor, not decoration: an
   <svg> with no width renders at the browser's default 300x150, so a single
   unstyled icon is enough to tear the layout apart. :not(.nav-icon) keeps it
   from outranking the menu icons' own sizing. */
.sidebar svg:not(.nav-icon) {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* min-height: 0 is load-bearing — without it a flex item refuses to shrink
   below its content, the menu pushes the footer off the bottom, and nothing
   scrolls at all. */
.sidebar-nav {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 2px;
    min-height: 0;
    padding-bottom: 6px;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--sidebar-line) transparent;
}

.sidebar-nav::-webkit-scrollbar {
    width: 8px;
}

.sidebar-nav::-webkit-scrollbar-thumb {
    background: var(--sidebar-line);
    border-radius: var(--radius-pill);
}

/* One row of the menu. Every row is a link now — there is no disabled state,
   because the menu no longer lists anything it cannot open. */
.nav-link {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 12px;
    font-size: 14.5px;
    font-weight: 600;
    text-decoration: none;
    color: var(--sidebar-text);
    border-radius: var(--radius-sm);
    transition: color var(--fast) var(--ease),
                background-color var(--fast) var(--ease);
}

.nav-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nav-icon {
    flex: none;
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity: .82;
}

a.nav-link:hover {
    color: var(--text);
    background: var(--sidebar-bg-soft);
}

a.nav-link:hover .nav-icon {
    opacity: 1;
}

/* The current page, filled with the brand blue so it lifts off the white
   panel the way a selected .chip--on lifts off a card. */
.nav-link.is-active,
a.nav-link.is-active:hover {
    color: var(--on-brand);
    background: var(--sidebar-active);
    box-shadow: var(--shadow-sm);
}

.nav-link.is-active .nav-icon {
    opacity: 1;
}

.sidebar a.nav-link:focus-visible,
.sidebar .nav-group-head:focus-visible {
    outline: 2px solid var(--brand-bright);
    outline-offset: -2px;
}

/* --- Collapsible sections ---------------------------------------------- */

.nav-group {
    margin-top: 6px;
}

/* A real <button>, so it is keyboard-operable and announces its state. The
   button chrome has to be reset because it inherits none of the row styling. */
.nav-group-head {
    display: flex;
    align-items: center;
    gap: 11px;
    width: 100%;
    padding: 9px 12px;
    font: inherit;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-align: left;
    color: var(--sidebar-text-dim);
    background: none;
    border: 0;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: color var(--fast) var(--ease),
                background-color var(--fast) var(--ease);
}

.nav-group-head:hover {
    color: var(--text);
    background: var(--sidebar-bg-soft);
}

.nav-group.is-open > .nav-group-head {
    color: var(--text);
}

/* The caret, drawn in CSS so it takes the row's colour and needs no image. */
.nav-caret {
    flex: none;
    width: 8px;
    height: 8px;
    margin-left: auto;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    border-radius: 1px;
    transform: rotate(45deg) translate(-2px, -2px);
    transition: transform var(--slow) var(--ease);
    opacity: .75;
}

.nav-group.is-open > .nav-group-head .nav-caret {
    transform: rotate(225deg) translate(-2px, -2px);
}

/* The panel animates on height, which is why sidebar.js measures it. Written
   open in the markup and closed by the script, so with no JavaScript every
   submenu is simply expanded and every link is still reachable. */
.nav-sub {
    overflow: hidden;
    transition: height var(--slow) var(--ease);
}

/* The rail sits directly under the first letter of the section heading above
   it, and the children step one clear 18px in from that heading — so the menu
   has two left edges, not four. */
.nav-sub-inner {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 2px 0 4px 6px;
    margin: 2px 0 2px 12px;
    border-left: 1px solid var(--sidebar-line);
}

.nav-sub .nav-link {
    font-size: 14px;
    color: var(--sidebar-text-dim);
}

.nav-sub .nav-link .nav-icon {
    width: 16px;
    height: 16px;
}

.nav-sub a.nav-link:hover,
.nav-sub .nav-link.is-active {
    color: var(--text);
}

/* Held to the bottom of the column, and given the head's own left edge so the
   account line starts where every nav label starts. */
.sidebar-foot {
    flex: none;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 10px 24px 0;
    margin: 0 -12px;
    border-top: 1px solid var(--sidebar-line);
}

.sidebar-user {
    display: flex;
    align-items: center;
    gap: 11px;
    margin: 0;
    padding: 8px 0 4px;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--text);
}

.sidebar-foot .nav-link {
    margin: 0 -12px;
}

.nav-logout:hover {
    color: var(--on-brand);
    background: var(--danger);
}

/* --- The top bar ------------------------------------------------------- */

/* .topbar keeps the sticky, blurred bar from section 5; these rules only add
   room for the controls that now sit in it. */
.topbar {
    gap: 12px;
    flex-wrap: wrap;
}

.topbar svg {
    width: 19px;
    height: 19px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.topbar-page {
    margin: 0;
    padding-left: 14px;
    font-size: 15px;
    font-weight: 700;
    color: var(--text);
    white-space: nowrap;
    border-left: 1px solid var(--border);
}

/* On a wide screen the sidebar's wordmark is already on screen, an inch to the
   left, so the bar does not repeat it. Below the sidebar breakpoint — and on
   the logged-out shell, which has no sidebar — it comes back. */
@media (min-width: 1024px) {
    .app-shell:not(.app-shell--bare) .topbar-mark {
        display: none;
    }

    /* With the wordmark gone the page name leads the bar, so the rule that
       separated the two would be hanging off nothing. */
    .app-shell:not(.app-shell--bare) .topbar-page {
        padding-left: 0;
        border-left: 0;
    }
}

.navsearch {
    flex: 1 1 220px;
    min-width: 0;
    max-width: 400px;
    margin: 0 auto 0 8px;
}

.navsearch input {
    width: 100%;
    height: var(--control-h);
    padding: 0 14px 0 38px;
    font: inherit;
    font-size: 14px;
    color: var(--text);
    background-color: var(--surface);
    background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='6.5'/%3E%3Cpath d='M16 16l4.5 4.5'/%3E%3C/g%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: left 13px center;
    border: 1px solid var(--border-input);
    border-radius: var(--radius-pill);
    transition: border-color var(--fast) var(--ease),
                box-shadow var(--fast) var(--ease);
}

.navsearch input::placeholder {
    color: var(--text-faint);
}

.navsearch input:hover {
    border-color: var(--border-strong);
}

.navsearch input:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-ring);
}

.navsearch input:disabled {
    cursor: not-allowed;
    background-color: var(--surface-sunken);
    border-style: dashed;
}

/* --- Branch selector and account menu ---------------------------------- */

.branchpick,
.usermenu {
    position: relative;
    flex: none;
}

/* The search box that used to stretch across the bar is gone (user request,
   2026-11-06), so the Branch button pushes itself and the icons to the right. */
.topbar .branchpick {
    margin-left: auto;
}

.branchpick > summary {
    display: flex;
    align-items: center;
    gap: 7px;
    height: var(--control-h);
    padding: 0 14px;
    font-size: 13.5px;
    line-height: 1.2;
    font-weight: 650;
    white-space: nowrap;
    color: var(--text-soft);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-pill);
    list-style: none;
    cursor: pointer;
    transition: color var(--fast) var(--ease),
                background-color var(--fast) var(--ease),
                border-color var(--fast) var(--ease);
}

.branchpick > summary::-webkit-details-marker,
.usermenu > summary::-webkit-details-marker {
    display: none;
}

.branchpick > summary::after {
    content: "";
    flex: none;
    width: 12px;
    height: 8px;
    background: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%2364748b' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center;
}

.branchpick > summary:hover,
.branchpick[open] > summary {
    color: var(--brand-strong);
    background: var(--brand-wash);
    border-color: var(--brand);
}

.branchpick-key {
    font-weight: 600;
    color: var(--text-muted);
}

/* A shop is free to have a long name, and the bar is not free to grow a second
   row because of it — that would drop its rule below the sidebar's. The name
   truncates instead; the full list is one click away in the dropdown. */
.branchpick-name {
    overflow: hidden;
    max-width: 16ch;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Round icon buttons: the bell, the account avatar and the mobile menu. */
.iconbtn,
.usermenu > summary,
.navtoggle {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: var(--control-h);
    height: var(--control-h);
    color: var(--text-soft);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: 50%;
    list-style: none;
    cursor: pointer;
    transition: color var(--fast) var(--ease),
                background-color var(--fast) var(--ease),
                border-color var(--fast) var(--ease);
}

.iconbtn:hover,
.usermenu > summary:hover,
.usermenu[open] > summary,
.navtoggle:hover {
    color: var(--brand-strong);
    background: var(--brand-wash);
    border-color: var(--brand);
}

/* The small red number on the shop's bell: new online orders still Pending
   (user request, 2026-11-06). Sits on the bell's top-right edge. */
.iconbtn {
    position: relative;
}

.iconbtn-count {
    position: absolute;
    top: -5px;
    right: -5px;
    box-sizing: border-box;
    min-width: 20px;
    height: 20px;
    padding: 0 4px;
    font-size: 11px;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
    color: #fff;
    background: var(--danger);
    border: 2px solid var(--surface);
    border-radius: 10px;
    pointer-events: none;
}

.iconbtn-count[hidden] {
    display: none;
}

.branchpick-menu,
.usermenu-menu {
    position: absolute;
    top: calc(100% + 10px);
    right: 0;
    z-index: 40;
    min-width: 232px;
    padding: 8px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
}

.branchpick-menu a,
.usermenu-menu a:not(.btn) {
    display: block;
    padding: 9px 12px;
    font-size: 14px;
    font-weight: 600;
    text-decoration: none;
    color: var(--text-soft);
    border-radius: var(--radius-sm);
}

.branchpick-menu a:hover,
.usermenu-menu a:not(.btn):hover {
    color: var(--brand-strong);
    background: var(--brand-wash);
}

.branchpick-menu .note {
    padding: 6px 12px;
    font-size: 13.5px;
}

.usermenu-menu {
    padding: 15px 16px;
}

.usermenu-name {
    margin: 0 0 2px;
    font-size: 14.5px;
    font-weight: 700;
}

.usermenu-menu .note {
    margin-bottom: 13px;
    font-size: 13px;
    overflow-wrap: anywhere;
}

/* --- Mobile drawer ------------------------------------------------------ */

/* The toggles are hidden until the sidebar becomes a drawer. */
.navtoggle,
.navtoggle--close,
.nav-scrim {
    display: none;
}

@media (max-width: 1023px) {
    .navtoggle,
    .navtoggle--close {
        display: inline-flex;
    }

    .navtoggle--close {
        width: 34px;
        height: 34px;
    }

    /* visibility, not just the transform: a drawer parked off-screen is still
       in the tab order, so without this a keyboard lands in an invisible menu.
       It is transitioned so the panel is reachable for the whole slide. */
    .sidebar {
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        z-index: 60;
        width: 284px;
        height: 100%;
        visibility: hidden;
        transform: translateX(-100%);
        box-shadow: var(--shadow-lg);
        transition: transform var(--slow) var(--ease),
                    visibility var(--slow) var(--ease);
    }

    .nav-toggle-box:checked ~ .app-shell .sidebar {
        visibility: visible;
        transform: translateX(0);
    }

    /* While the drawer is open the page behind it holds still (user request,
       2026-11-05: on the phone the page kept scrolling under the open menu, so
       the menu seemed to move with it). Only the menu's own list scrolls, and
       overscroll-behavior stops it handing the swipe on to the page when it
       reaches its end. Plain CSS, so the drawer still needs no script. */
    html:has(.nav-toggle-box:checked),
    body:has(.nav-toggle-box:checked) {
        overflow: hidden;
    }

    .sidebar {
        overscroll-behavior: contain;
    }

    /* A label, not a pseudo-element, so tapping outside closes the drawer. */
    .nav-toggle-box:checked ~ .app-shell .nav-scrim {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 50;
        background: rgba(15, 23, 42, .45);
    }
}

@media (max-width: 900px) {
    .navsearch {
        order: 5;
        flex-basis: 100%;
        max-width: none;
        margin: 0;
    }
}

@media (max-width: 560px) {
    .topbar-page,
    .branchpick-key {
        display: none;
    }

    /* On a phone the bell and account icons wrap onto the topbar's second row,
       at its LEFT edge, while the account menu still opens leftwards from the
       icon's right edge -- so half of it, Log Out included, hung off the
       screen (user report, 2026-10-29). Here the menu is laid against the
       sticky topbar instead of the icon: full width, inset from both sides,
       so it can never leave the screen wherever the icon wraps to. The Branch
       menu does the same on the narrowest phones, where the Branch button
       wraps to the left too, so it gets the same treatment. */
    .branchpick,
    .usermenu {
        position: static;
    }

    .branchpick-menu,
    .usermenu-menu {
        left: 12px;
        right: 12px;
        top: calc(100% + 6px);
        min-width: 0;
    }

    .branchpick-name {
        max-width: 11ch;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
}

@media not all {   /* dark theme switched off -- every page is always light; see the note at the top of this file */
    /* The three chevron/magnifier images have a fixed stroke colour, so they
       get lighter copies here, exactly as .field select does. */
    .nav-group-head::after,
    .branchpick > summary::after {
        background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%2394a3b8' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    }

    .navsearch input {
        background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='6.5'/%3E%3Cpath d='M16 16l4.5 4.5'/%3E%3C/g%3E%3C/svg%3E");
    }

    .nav-toggle-box:checked ~ .app-shell .nav-scrim {
        background: rgba(0, 0, 0, .58);
    }
}

/* ==========================================================================
   6. Alerts
   ========================================================================== */

.alert {
    position: relative;
    margin: 0 0 24px;
    padding: 14px 18px 14px 46px;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    font-size: 14.5px;
    box-shadow: var(--shadow-sm);
}

/* A glyph instead of a bare colour block, so the meaning survives a
   greyscale printout or a colour-blind reader. */
.alert::before {
    position: absolute;
    left: 16px;
    top: 13px;
    width: 18px;
    height: 18px;
    line-height: 18px;
    text-align: center;
    font-size: 12px;
    font-weight: 700;
    border-radius: 50%;
    color: var(--on-brand);
}

.alert--success {
    color: var(--success);
    background: var(--success-bg);
    border-color: var(--success-line);
}

.alert--success::before {
    content: "\2713";
    background: var(--success);
}

.alert--error {
    color: var(--danger);
    background: var(--danger-bg);
    border-color: var(--danger-line);
}

.alert--error::before {
    content: "!";
    background: var(--danger);
}

/* A third weight between the two: nothing has gone wrong, but the reader
   cannot do the thing they came to do. Used where a page has to explain a
   rule rather than report a failure -- book_service.php telling a customer
   they are looking at a shop they are not signed in to. Red would read as an
   error the customer had caused, and green as a success; this is neither.

   Built from the --warn tokens that were already declared for exactly this,
   in both the light and the dark blocks, so it needed no new colour. */
.alert--warn {
    color: var(--warn);
    background: var(--warn-bg);
    border-color: var(--warn-line);
}

.alert--warn::before {
    content: "i";
    background: var(--warn);
}

.alert ul {
    margin: 0;
    padding-left: 18px;
}

.alert li + li {
    margin-top: 5px;
}

/* ==========================================================================
   7. Forms
   ========================================================================== */

.field {
    margin-bottom: 20px;
}

.field label {
    display: block;
    margin-bottom: 7px;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--text-soft);
}

.field input,
.field select {
    width: 100%;
    padding: 12px 14px;
    font: inherit;
    font-size: 15px;
    color: var(--text);
    background: #D2DAE5;              /* a darker white than the card, so the typing box shows (2026-10-18) */
    border: 1px solid #A3B3C8;
    border-radius: var(--radius-sm);
    box-shadow: inset 0 1px 2px rgba(15, 23, 42, .04);
    transition: border-color var(--fast) var(--ease),
                box-shadow var(--fast) var(--ease);
}

.field select {
    appearance: none;
    -webkit-appearance: none;
    padding-right: 40px;
    /* Chevron drawn inline so the app stays self-contained offline. */
    background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%2364748b' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 15px center;
    cursor: pointer;
}

.field input:hover,
.field select:hover {
    border-color: var(--border-strong);
}

.field input:focus,
.field select:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-ring);
}

.field input::placeholder {
    color: var(--text-faint);
}

.field input[type="number"],
.field input[type="date"] {
    font-variant-numeric: tabular-nums;
}

.field .hint {
    margin: 7px 0 0;
    font-size: 13px;
    color: var(--text-muted);
}

/* Two fields side by side on wider screens. */
.field-row {
    display: flex;
    gap: 18px;
}

.field-row[hidden] {
    display: none;
}

.field-row .field {
    flex: 1;
    min-width: 0;
}

@media (max-width: 520px) {
    .field-row {
        display: block;
    }
}

/* A form laid out as one row of controls, e.g. the shop search box. */
.toolbar {
    display: flex;
    align-items: flex-end;
    gap: 12px;
}

.toolbar .field {
    flex: 1;
    margin-bottom: 0;
    min-width: 0;
}

/* ==========================================================================
   8. Buttons
   ========================================================================== */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 11px 20px;
    font: inherit;
    font-size: 15px;
    font-weight: 650;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
    white-space: nowrap;
    color: var(--on-brand);
    background: var(--brand);
    border: 1px solid var(--brand);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-sm);
    cursor: pointer;
    transition: background-color var(--fast) var(--ease),
                border-color var(--fast) var(--ease),
                box-shadow var(--fast) var(--ease),
                transform var(--fast) var(--ease);
}

.btn:hover {
    background: var(--brand-strong);
    border-color: var(--brand-strong);
    box-shadow: var(--shadow);
}

.btn:active {
    transform: translateY(1px);
    box-shadow: var(--shadow-sm);
}

.btn:focus-visible {
    outline: 2px solid var(--brand-strong);
    outline-offset: 2px;
}

.btn--block {
    display: flex;
    width: 100%;
    margin-top: 8px;
    padding-top: 13px;
    padding-bottom: 13px;
    font-size: 16px;
}

.btn--ghost {
    padding: 9px 16px;
    font-size: 14px;
    color: var(--brand-strong);
    background: var(--surface);
    border-color: var(--border-strong);
}

.btn--ghost:hover {
    color: var(--brand-deep);
    background: var(--brand-wash);
    border-color: var(--brand);
}

.btn--sm {
    padding: 6px 13px;
    font-size: 13px;
}

.btn--danger {
    color: var(--danger);
    background: var(--surface);
    border-color: var(--danger-line);
}

.btn--danger:hover {
    color: var(--on-brand);
    background: var(--danger);
    border-color: var(--danger);
}

.button-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 18px 0 0;
}

.button-row form {
    margin: 0;
}

.row-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: flex-end;
}

.row-actions form {
    margin: 0;
}

/* On a <td> the flex above stops it being a table cell, so the action column
   slipped out of the table's box: its row lines no longer met the others and
   the header shading stopped short. As a cell it stays a cell, and the buttons
   sit in a line inside it instead. */
td.row-actions {
    display: table-cell;
    text-align: right;
}

td.row-actions > * {
    display: inline-block;
    margin: 3px 0 3px 6px;
    vertical-align: middle;
}

/* Service List cards: the buttons sit at the foot of each card, so a card with
   a short description still lines its buttons up with its neighbours'. */
.service-grid .row-actions {
    margin-top: auto;
    justify-content: flex-start;
}

/* The fleet board's light blue (see .machine), so a service card stands out
   from the off-white panel the same way a machine card does (user request,
   2026-10-27). */
.service-grid > .card {
    background: #E3EDFC;
    border-color: #BCD3F5;
}

/* ==========================================================================
   9. Panels and cards
   ========================================================================== */

.panel {
    padding: 28px 30px;
    margin-bottom: 22px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

/* Section headings carry a small brand tick, which gives a page of stacked
   panels a scannable left edge instead of five identical grey rules. */
.panel h2 {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 18px;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--border);
    font-size: 16px;
    font-weight: 700;
    letter-spacing: -.005em;
    color: var(--text);
}

.panel h2::before {
    content: "";
    flex: none;
    width: 4px;
    height: 17px;
    background: var(--brand);
    border-radius: 2px;
}

.panel h3 {
    margin: 24px 0 10px;
    font-size: 15px;
    font-weight: 700;
}

.panel > :last-child {
    margin-bottom: 0;
}

/* A panel whose whole job is one call to action reads better with the button
   pushed clear of the copy above it. */
.panel .note + p > .btn {
    margin-top: 4px;
}

/* A heading for a group of cards that sits directly on the page instead of
   inside a panel, e.g. the sections of the public landing page. Carries the
   same brand tick as a panel heading so the two read as one system. */
.section-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 34px 0 16px;
    font-size: 16px;
    font-weight: 700;
    letter-spacing: -.005em;
    color: var(--text);
}

.section-head::before {
    content: "";
    flex: none;
    width: 4px;
    height: 17px;
    background: var(--brand);
    border-radius: 2px;
}

.section-head:first-child {
    margin-top: 0;
}

/* Shop cards on the customer browse page. */
.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
    gap: 20px;
    margin-bottom: 22px;
}

.card {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: 26px 24px 24px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    transition: box-shadow var(--slow) var(--ease),
                transform var(--slow) var(--ease),
                border-color var(--slow) var(--ease);
}

/* An accent edge that fills in on hover, so the pointer has somewhere to land
   without the whole card changing colour. */
.card::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
    background: var(--brand);
    transform: scaleY(0);
    transform-origin: top;
    transition: transform var(--slow) var(--ease);
}

.card:hover {
    transform: translateY(-3px);
    border-color: var(--brand-line);
    box-shadow: var(--shadow);
}

.card:hover::before {
    transform: scaleY(1);
}

.card h3 {
    margin: 0 0 8px;
    font-size: 18px;
    font-weight: 700;
}

.card-desc {
    margin: 0 0 18px;
    font-size: 14px;
    color: var(--text-muted);
}

.card .meta-list {
    margin-bottom: 20px;
    font-size: 14px;
}

.card .btn--block {
    margin-top: auto;
}

/* Location / hours / services lines on a shop chooser card. Each row is an
   icon in a fixed column plus wrapping text, so multi-line addresses stay
   aligned under one another instead of tucking under the icon. */
.shop-facts {
    margin: 0 0 20px;
    padding: 0;
    list-style: none;
    font-size: 14.5px;
}

.shop-facts li {
    display: flex;
    align-items: flex-start;
    gap: 9px;
    padding: 5px 0;
    color: var(--text-soft);
}

.shop-facts-icon {
    flex: none;
    width: 18px;
    text-align: center;
    font-size: 14px;
    line-height: 1.5;
}

/* One card shown as a preview inside a settings panel — Shop Profile draws the
   customer's real shop card so an owner can see what a picture will look like.
   The grid is capped rather than filled, so a single preview card does not
   stretch across the whole panel. */
.card-grid--preview {
    grid-template-columns: minmax(0, 340px);
}

/* A SHOP CARD WITH THE SHOP'S OWN PHOTOGRAPH BEHIND IT.

   The photograph is the card's background and the whole card sits on it — the
   name, the rating, the description, the address, the hours, the prices and
   the button. Nothing is stacked or positioned to make that work: the picture
   simply replaces the card's white surface.

   THE OVERLAY IS PART OF THE SAME DECLARATION, and that is the whole trick.
   Two backgrounds are given at once; the gradient is listed FIRST, so it
   paints on top of the picture underneath it. A page cannot forget it, and
   there is no extra element and no z-index to keep in order — which also means
   the overlay can never end up behind the text it exists to protect.

   The gradient is deliberately heavy (.62 at the top, .88 at the bottom). A
   shop photograph is usually bright — white machines, white towels, a lit
   counter — and a gentler wash leaves 14px grey text unreadable on it. This
   is the version that stays legible over a bright picture, a dark one and a
   busy one alike, which is the only version worth shipping. */
.card--photo {
    background-color: var(--brand-deep);
    background-image: linear-gradient(to bottom,
                                      rgba(9, 17, 34, .66) 0%,
                                      rgba(9, 17, 34, .76) 45%,
                                      rgba(9, 17, 34, .88) 100%),
                      var(--shop-card-image);
    background-size: cover, cover;
    background-position: center, center;
    background-repeat: no-repeat, no-repeat;
    border-color: rgba(255, 255, 255, .18);
    color: #fff;
}

/* Every piece of text on the card, turned white and given a shadow soft enough
   to read as depth rather than as an outline. They are listed one by one
   rather than with a blanket `.card--photo * { color: #fff }`, so a component
   that lands on one of these cards later keeps its own colours until somebody
   decides otherwise. */
.card--photo h3,
.card--photo .card-desc,
.card--photo .shop-facts li,
.card--photo .meta-list dt,
.card--photo .meta-list dd,
.card--photo .rating-score,
.card--photo .rating-count,
.card--photo .rating-none-text,
.card--photo .muted {
    color: #fff;
    text-shadow: 0 1px 3px rgba(0, 0, 0, .55);
}

/* The quieter lines stay quieter, but in white rather than in slate — grey ink
   on a photograph is the one thing that stops being readable.

   The rating line is deliberately NOT in this list. It is the smallest text on
   the card (13px) and it sits at the very top, where the overlay is thinnest:
   dimmed to 86% it measures 4.43:1 over a pure-white photograph, just under the
   4.5:1 that normal text needs, so it keeps solid white and earns 5.7:1. */
.card--photo .card-desc,
.card--photo .meta-list dt,
.card--photo .muted {
    color: rgba(255, 255, 255, .86);
}

.card--photo .shop-facts li {
    color: rgba(255, 255, 255, .94);
}

/* Stars: the earned ones keep the brand's light blue, which reads well on a
   dark ground; the empty ones become a white ghost rather than the pale grey
   they are on a white card, where they would all but vanish. */
.card--photo .star,
.card--photo .rating--none .star {
    fill: rgba(255, 255, 255, .38);
}

.card--photo .star--on {
    fill: var(--brand-bright);
}

/* The logo keeps its light box, which reads as a mark pinned to the
   photograph. A soft white ring separates it from a picture of any colour. */
.card--photo .shop-logo {
    border-color: rgba(255, 255, 255, .5);
    box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
}

/* The card's own button. The primary one is already brand blue on white text
   and needs nothing; the ghost variant would be a white box on a photograph,
   so it becomes the glass version of itself instead. */
.card--photo .btn--ghost {
    color: #fff;
    background: rgba(255, 255, 255, .14);
    border-color: rgba(255, 255, 255, .55);
}

.card--photo .btn--ghost:hover {
    color: #fff;
    background: rgba(255, 255, 255, .24);
    border-color: #fff;
}

/* The accent bar that fills in on hover is drawn on the card's left edge. On a
   photograph it is white rather than brand blue, for the same reason the text
   is. */
.card--photo::before {
    background: rgba(255, 255, 255, .85);
}

.card--photo:hover {
    border-color: rgba(255, 255, 255, .38);
}

/* --------------------------------------------------------------------------
   A picture drawn as a box of its own

   Used on the upload pages: the thumbnail beside each file input, which is
   also the box assets/js/image-preview.js draws a newly chosen picture into.
   The customer-facing cards do not use it — there the picture is the card's
   background, above.
   -------------------------------------------------------------------------- */
.shop-cover {
    position: relative;
    display: block;
    overflow: hidden;
    background: var(--brand-mist);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.shop-cover-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
}

/* No picture yet: the same box in a light panel with the app's own picture
   icon in the middle, so an empty field reads as "nothing uploaded" rather
   than as a broken image. */
.shop-cover--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-faint);
}

.shop-cover--empty .nav-icon {
    width: 26px;
    height: 26px;
    opacity: .55;
}

.shop-cover--thumb {
    width: 116px;
    height: 66px;
    margin: 0 0 8px;
}

/* The business permit (user request, 2026-10-27): the whole sheet shown small
   rather than cropped, in a portrait box because a permit is a page, and the
   box is a link to the full-size file. */
.shop-cover--permit {
    width: 120px;
    height: 156px;
    margin: 0 0 8px;
}

.shop-cover--permit .shop-cover-img {
    object-fit: contain;
}

.permit-link {
    display: inline-block;
    cursor: zoom-in;
}

.permit-link:hover .shop-cover,
.permit-link:focus-visible .shop-cover {
    border-color: var(--brand);
}

/* The shop's own logo, and the row it sits in at the top of a shop card.

   A shop that has not uploaded one is drawn with its initials in the brand
   colour instead, by shop_logo_html() — so the grid never mixes cards that
   have a mark with cards that have an empty box where one should be. Both
   states use the SAME box, which is what keeps every card the same height. */
.shop-card-head {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-bottom: 12px;
}

/* min-width: 0 lets a long shop name wrap inside the flex row instead of
   pushing the logo out of the card. */
.shop-card-head .shop-card-title {
    min-width: 0;
}

.shop-card-head h3 {
    margin: 0;
}

.shop-card-head .card-rating {
    margin: 4px 0 0;
}

.shop-logo {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: 14px;
    overflow: hidden;
    background: var(--brand-mist);
    border: 1px solid var(--border);
    color: var(--brand-strong);
    font-size: 20px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: .5px;
}

/* object-fit: cover so a logo that is not square fills the box and is cropped,
   rather than being squashed into a shape it was never drawn in. */
.shop-logo img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The same mark in a table row, and on a page header. */
.shop-logo--sm {
    width: 38px;
    height: 38px;
    border-radius: 10px;
    font-size: 14px;
}

.shop-logo--lg {
    width: 84px;
    height: 84px;
    border-radius: 18px;
    font-size: 30px;
}

/* ==========================================================================
   10. Data display
   ========================================================================== */

/* Label/value pairs, e.g. "Email: name@example.com" */
.meta-list {
    margin: 0;
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 12px 26px;
    font-size: 15px;
}

.meta-list dt {
    color: var(--text-muted);
    font-size: 13.5px;
    font-weight: 600;
}

.meta-list dd {
    margin: 0;
    color: var(--text);
    word-break: break-word;
}

.feature-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.feature-list li {
    position: relative;
    padding: 12px 0 12px 28px;
    border-top: 1px solid var(--border);
    font-size: 15px;
}

.feature-list li:first-child {
    border-top: none;
}

.feature-list li::before {
    content: "";
    position: absolute;
    left: 6px;
    top: 21px;
    width: 7px;
    height: 7px;
    background: var(--brand);
    border-radius: 50%;
    box-shadow: 0 0 0 3px var(--brand-tint);
}

/* Wide tables scroll inside their own frame instead of stretching the page. */
.table-wrap {
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
}

/* Every table box wears the machine cards' light blue, so it stands out from
   the off-white panel it sits in (user request, 2026-10-18: first the shop's
   Available Services list, then all of them). The header row takes a step
   deeper of the same blue, and a hovered row a step deeper again. */
.table-wrap {
    background: #E3EDFC;
    border-color: #BCD3F5;
}

.table-wrap .table thead th {
    background: #D3E3FA;
    border-bottom-color: #BCD3F5;
}

.table-wrap .table td {
    border-top-color: #CADCF6;
}

/* The first row's top edge is the header's bottom edge; giving both the same
   colour keeps the line unbroken across every column. */
.table-wrap .table tbody tr:first-child td {
    border-top-color: #BCD3F5;
}

.table-wrap .table tbody tr:hover {
    background: #D7E6FB;
}

.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14.5px;
}

.table th,
.table td {
    padding: 14px 16px;
    text-align: left;
    vertical-align: top;
    border-top: 1px solid var(--border);
}

.table thead th {
    border-top: none;
    border-bottom: 1px solid var(--border-strong);
    background: var(--surface-sunken);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
}

.table tbody tr {
    transition: background-color var(--fast) var(--ease);
}

.table tbody tr:hover {
    background: var(--brand-mist);
}

/* A table whose every row is a box of its own, in the machine cards' light
   blue (user request, 2026-10-27: Customer Records). The columns, padding and
   type sizes are the ordinary table's -- only the rows are pulled apart by
   border-spacing and given their own edges -- so nothing on the page changes
   size. The wrapper drops its own box, which would otherwise be a box around
   the boxes. */
.table-wrap--plain {
    background: transparent;
    border: 0;
    border-radius: 0;
}

.table--boxed {
    border-collapse: separate;
    border-spacing: 0 8px;
}

.table--boxed td,
.table-wrap .table--boxed td,
.table-wrap .table--boxed tbody tr:first-child td {
    background: #E3EDFC;
    border-top: 1px solid #BCD3F5;
    border-bottom: 1px solid #BCD3F5;
    transition: background-color var(--fast) var(--ease);
}

.table--boxed td:first-child {
    border-left: 1px solid #BCD3F5;
    border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}

.table--boxed td:last-child {
    border-right: 1px solid #BCD3F5;
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

.table--boxed tbody tr:hover,
.table-wrap .table--boxed tbody tr:hover {
    background: transparent;
}

.table--boxed tbody tr:hover td {
    background: #D7E6FB;
}

.table .num {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* Secondary line under the main value in a table cell. */
.cell-sub {
    display: block;
    margin-top: 3px;
    font-size: 13px;
    font-weight: 400;
    color: var(--text-muted);
}

code {
    padding: 2px 7px;
    font-family: var(--font-mono);
    font-size: .88em;
    color: var(--brand-deep);
    background: #C9DCF9;   /* a step deeper, so the small box reads on its card */
    border: 1px solid var(--brand-line);
    border-radius: 5px;
}

/* ==========================================================================
   11. Status badges and chips
   ========================================================================== */

/* Shared by order status, SMS results and the role label in the page header.
   The leading dot carries the same meaning as the colour, so the state is
   still readable in greyscale. */
.badge {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 5px 12px 5px 10px;
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--brand-deep);
    background: #C9DCF9;              /* a step deeper than --brand-wash, so it reads on the light cards */
    border: 1px solid #8FB3EC;
    border-radius: var(--radius-pill);
    vertical-align: middle;
}

.badge::before {
    content: "";
    flex: none;
    width: 6px;
    height: 6px;
    background: currentColor;
    border-radius: 50%;
}

.badge--muted {
    color: #475569;
    background: #D6DEE9;              /* darker than the off-white cards it sits on */
    border-color: #A3B3C8;
}

.badge--ready {
    color: var(--warn);
    background: #F7DFB4;
    border-color: rgba(133, 80, 0, .42);
}

.badge--done {
    color: var(--success);
    background: #C9EAD7;
    border-color: rgba(20, 107, 71, .42);
}

.badge--cancelled {
    color: var(--danger);
    background: #F9D3CF;
    border-color: rgba(165, 30, 23, .42);
}

.status-line {
    margin: 0 0 20px;
}

.status-line .badge {
    padding: 8px 18px 8px 15px;
    font-size: 13px;
}

.status-line .badge::before {
    width: 8px;
    height: 8px;
}

/* Shop Profile: "Different Hours on Some Days", one row per weekday --
   the day's name, then its opening and closing time (2026-11-05). */
.day-hours {
    display: grid;
    grid-template-columns: minmax(84px, auto) 1fr 1fr;
    gap: 8px 10px;
    align-items: center;
}

.day-hours__day {
    font-size: 14px;
    font-weight: 600;
    color: var(--text);
}

/* Filter chips above an order list. */
.chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

/* My Orders' Filter: a button with an arrow that shows the chips when pressed
   (user request, 2026-10-30). Same look as the Branch selector's button. */
.filterpick > summary {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 0 16px;
    font-size: 14px;
    font-weight: 650;
    color: var(--text-soft);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-pill);
    list-style: none;
    cursor: pointer;
}

.filterpick > summary::-webkit-details-marker {
    display: none;
}

.filterpick > summary::after {
    content: "";
    flex: none;
    width: 12px;
    height: 8px;
    background: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%2364748b' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center;
    transition: transform var(--fast) var(--ease);
}

.filterpick[open] > summary::after {
    transform: rotate(180deg);
}

.filterpick > summary:hover,
.filterpick[open] > summary {
    color: var(--brand-strong);
    background: var(--brand-wash);
    border-color: var(--brand);
}

.filterpick-key {
    font-weight: 600;
    color: var(--text-muted);
}

.filterpick > .chip-row {
    margin-top: 14px;
}

.chip {
    display: inline-block;
    padding: 8px 16px;
    font-size: 13px;
    font-weight: 650;
    text-decoration: none;
    color: var(--text-soft);
    background: #DCE5F1;              /* darker than the off-white panel, so each chip is visible */
    border: 1px solid #A3B3C8;
    border-radius: var(--radius-pill);
    transition: color var(--fast) var(--ease),
                background-color var(--fast) var(--ease),
                border-color var(--fast) var(--ease);
}

.chip:hover {
    color: var(--brand-strong);
    background: var(--brand-wash);
    border-color: var(--brand);
}

/* A chip that holds a real checkbox rather than being a link. Used where a
   choice is a set rather than a filter -- the stages a service includes -- so
   the same pill vocabulary covers both without a second control appearing. */
.chip--check {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
}

.chip--check input {
    margin: 0;
    accent-color: var(--brand);
}

.chip--on {
    color: var(--on-brand);
    background: var(--brand);
    border-color: var(--brand);
    box-shadow: var(--shadow-sm);
}

.chip--on:hover {
    color: var(--on-brand);
    background: var(--brand-strong);
    border-color: var(--brand-strong);
}

/* ==========================================================================
   12. Order tracker (stepper)

   The markup is an <ol class="tracker"> of <li class="tracker-step">, with
   .is-done on every step reached and .is-current on the live one. The list
   already carried counter-reset: step, so the step numbers are generated
   here rather than being written into the PHP.
   ========================================================================== */

.tracker {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin: 0 0 24px;
    padding: 0;
    list-style: none;
    counter-reset: step;
}

.tracker-step {
    position: relative;
    flex: 1 1 110px;
    counter-increment: step;
    padding: 44px 10px 4px;
    text-align: center;
    background: none;
    border: none;
}

/* The numbered node. */
.tracker-step::before {
    content: counter(step);
    position: absolute;
    top: 0;
    left: 50%;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-left: -16px;
    font-size: 13px;
    font-weight: 700;
    color: var(--text-faint);
    background: var(--surface);
    border: 2px solid var(--border-strong);
    border-radius: 50%;
    transition: color var(--slow) var(--ease),
                background-color var(--slow) var(--ease),
                border-color var(--slow) var(--ease);
}

/* The rail between one node and the next. Sits behind the nodes and stops at
   the last step so the line never dangles off the end. */
.tracker-step::after {
    content: "";
    position: absolute;
    top: 15px;
    left: 50%;
    /* Reaches exactly the next node's centre: one step width plus the gap. */
    width: calc(100% + 4px);
    height: 2px;
    background: var(--border-strong);
    transition: background-color var(--slow) var(--ease);
}

.tracker-step:last-child::after {
    display: none;
}

/* Reached: filled node, brand-coloured rail, and a tick in place of the
   number so a completed step reads at a glance. */
.tracker-step.is-done::before {
    content: "\2713";
    font-size: 15px;
    color: var(--on-brand);
    background: var(--brand);
    border-color: var(--brand);
}

.tracker-step.is-done::after {
    background: var(--brand);
}

/* The live step keeps its number and gets a halo. */
.tracker-step.is-current::before {
    content: counter(step);
    color: var(--on-brand);
    background: var(--brand-strong);
    border-color: var(--brand-strong);
    box-shadow: 0 0 0 4px var(--brand-ring);
}

/* Everything after the live step is still to come, so its rail stays grey. */
.tracker-step.is-current::after {
    background: var(--border-strong);
}

.tracker-label {
    display: block;
    font-size: 13.5px;
    font-weight: 650;
    color: var(--text);
}

.tracker-step:not(.is-done) .tracker-label {
    color: var(--text-muted);
}

.tracker-time {
    display: block;
    margin-top: 3px;
    font-size: 11.5px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}


/* ==========================================================================
   12b. Machine fleet

   The cards on the shop dashboard's Machine Fleet Status section. Built on
   .card-grid / .card, and the cycle stages reuse the order .tracker outright —
   a compact modifier is the only thing added to it, because a wash going
   Washing → Rinsing → Spinning is the same idea as an order going Confirmed →
   Processing → Ready, and it should look like it.

   Status colours, as agreed:
     green running · yellow near completion · orange maintenance
     red error · grey available · brand-teal completed
   ========================================================================== */

/* Status pills. Each one is a .badge, so the dot, the shape and the type are
   already right; only the three colours change. */
.badge--run {
    color: var(--success);
    background: #C9EAD7;
    border-color: rgba(20, 107, 71, .42);
}

.badge--near {
    color: var(--warn);
    background: #F7DFB4;
    border-color: rgba(133, 80, 0, .42);
}

.badge--maint {
    color: var(--maint);
    background: #F9D5BE;
    border-color: rgba(156, 67, 8, .42);
}

.badge--complete {
    color: var(--brand-deep);
    background: #C9DCF9;
    border-color: #8FB3EC;
}

.badge--paused {
    color: #475569;
    background: #D6DEE9;
    border-color: #A3B3C8;
}

.fleet-summary {
    margin: 0 0 20px;
}

/* Completion notices are appended here by the countdown. Empty until one
   fires, so it must not reserve space. */
.fleet-notices:empty {
    display: none;
}

.fleet-notices .alert:last-child {
    margin-bottom: 20px;
}

/* The fleet board is the one card grid that is NOT a grid.

   Every card here is a different height on purpose: a running drum carries a
   clock, a progress bar and a stage tracker, an idle one carries neither. A
   real grid stretches every cell to the tallest in its row, which made an idle
   machine beside a running one grow to match and read as busy -- and the whole
   job of this board is to show at a glance which drums are working.

   Letting the cards keep their own height fixed that, but left a tall card
   holding a column of empty space open beside the short ones until the next
   row began. Columns close it: a short card flows up under its neighbour
   instead of waiting for a new row, so the board packs itself.

   WHY COLUMNS AND NOT GRID

   grid-template-rows: masonry is what this wants and it is not usable yet --
   Firefox only, behind a flag. A multi-column container does the same packing
   in every browser PALABAHAN runs in, with no JavaScript and no library, which
   is the rest of this project's rule anyway.

   THE ONE THING COLUMNS CHANGE

   Cards read DOWN a column and then across, not left to right. That is fine
   for a fleet board -- each card carries its own machine code and nothing here
   is a sequence -- but it is why this is not used for the other card grids,
   where reading order carries meaning.

   column-width rather than column-count keeps it responsive on its own: the
   browser fits as many 310px columns as the screen allows, down to one on a
   phone. The gap comes from .card-grid above, where `gap` sets column-gap. */
.machine-grid {
    display: block;
    column-width: 310px;
}

/* A card must never be sliced across a column break. The margin is what the
   grid's row-gap used to provide; a multi-column container has no row gap. */
.fleet-page > .machine {
    -webkit-column-break-inside: avoid;
    break-inside: avoid;
    margin-bottom: 20px;
}

/* The swipe pages of includes/machine_fleet.php. On a computer they are not
   boxes at all, so every card still sits straight in the columns above; only
   the phone swipe below turns them into pages. */
.fleet-page {
    display: contents;
}

.machine {
    gap: 18px;
    padding: 22px 22px 20px;
    /* Light blue, so each machine stands out from the off-white panel it sits
       in (the user picked this, 2026-10-18). The coloured left edge still
       carries the state -- running, near completion, out of service. */
    background: #E3EDFC;
    border-color: #BCD3F5;
}

/* A fleet card is a readout, not a link. Lifting a card whose numbers are
   moving reads as a glitch, so the hover animation is dropped and the accent
   edge is used for the machine's state instead. */
.machine:hover {
    transform: none;
    border-color: #BCD3F5;
    box-shadow: var(--shadow-sm);
}

.machine::before,
.machine:hover::before {
    transform: scaleY(1);
    background: var(--border-strong);
}

.machine[data-status="running"]::before {
    background: var(--success);
}

.machine[data-status="paused"]::before {
    background: var(--text-faint);
}

.machine[data-status="maintenance"]::before {
    background: var(--maint);
}

.machine[data-status="error"]::before {
    background: var(--danger);
}

.machine[data-status="completed"]::before,
.machine.is-complete::before {
    background: var(--brand);
}

.machine.is-near::before {
    background: var(--warn);
}

.fleet-intro {
    margin: -6px 0 18px;
}

/* The fleet board's waiting list: the round feed icon carries the place in
   the queue instead of a picture. */
.feed.fleet-queue {
    margin-bottom: 22px;    /* .feed, later in the file, zeroes the margin */
}

.fleet-queue .feed-icon {
    font-weight: 800;
    font-size: 15px;
}

/* Machine identity: the icon, the code and the type as one block, so the
   status pill on the right always has a single thing to sit opposite. */
.machine-id {
    display: flex;
    align-items: flex-start;
    gap: 11px;
    min-width: 0;
}

.machine-icon {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    color: var(--brand-strong);
    background: #C3D8F8;              /* deeper than the light blue machine card */
    border: 1px solid #8FB3EC;
    border-radius: var(--radius-sm);
}

.machine-icon svg {
    width: 19px;
    height: 19px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.machine[data-status="running"] .machine-icon {
    color: var(--success);
    background: var(--success-bg);
    border-color: var(--success-line);
}

.machine[data-status="maintenance"] .machine-icon,
.machine[data-status="error"] .machine-icon {
    color: var(--maint);
    background: var(--maint-bg);
    border-color: var(--maint-line);
}

.machine.is-near .machine-icon {
    color: var(--warn);
    background: var(--warn-bg);
    border-color: var(--warn-line);
}

.machine-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
}

.machine-code {
    margin: 0;
    font-size: 19px;
    font-weight: 700;
    letter-spacing: .01em;
    font-variant-numeric: tabular-nums;
}

.machine-head .card-desc {
    margin: 2px 0 0;
}

.machine-facts {
    margin: 0;
}

.machine-facts dd {
    font-variant-numeric: tabular-nums;
}

/* The two counters that tick. Tabular figures keep the card from twitching
   sideways every time a digit changes width. */
.machine-clock {
    display: flex;
    gap: 10px;
    margin: 0;
    padding: 14px 16px;
    background: #D3E3FA;   /* a step deeper, so the small box reads on its card */
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.machine-clock p {
    flex: 1;
    margin: 0;
    min-width: 0;
}

.machine-clock strong {
    display: block;
    font-size: 20px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    letter-spacing: -.01em;
}

.machine-clock-key {
    display: block;
    margin-bottom: 2px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Under a quarter of an hour to go: the number a staff member is actually
   waiting on, so it gets the warn colour rather than a second badge. */
.machine.is-near .machine-clock {
    background: var(--warn-bg);
    border-color: var(--warn-line);
}

.machine.is-near [data-machine-remaining] {
    color: var(--warn);
}

.machine.is-complete .machine-clock {
    background: #C9DCF9;   /* a step deeper, so the small box reads on its card */
    border-color: var(--brand-line);
}

.machine-finish {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin: 0;
}

.machine-finish strong {
    font-size: 15px;
    font-variant-numeric: tabular-nums;
}

/* --- Cycle progress bar ------------------------------------------------- */

.cycle {
    margin: 0;
}

.cycle-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin: 0 0 8px;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--text-soft);
}

.cycle-head span:last-child {
    font-variant-numeric: tabular-nums;
    color: var(--brand-strong);
}

.cycle-bar {
    overflow: hidden;
    height: 10px;
    background: #C3D3EA;   /* a step deeper, so the small box reads on its card */
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
}

.cycle-bar-fill {
    display: block;
    height: 100%;
    background: linear-gradient(90deg, var(--brand) 0%, var(--brand-bright) 100%);
    border-radius: inherit;
    /* Matches the one second tick, so the bar glides instead of stepping. */
    transition: width 1s linear;
}

.machine.is-near .cycle-bar-fill {
    background: linear-gradient(90deg, var(--warn) 0%, var(--warn) 100%);
}

.machine.is-complete .cycle-bar-fill,
.machine[data-status="completed"] .cycle-bar-fill {
    background: var(--brand);
}

.cycle-foot {
    margin: 8px 0 0;
    font-size: 12.5px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* --- Cycle stages ------------------------------------------------------- */

/* The order tracker, shrunk to fit inside a card: the rail runs down the left
   the way it already does on a narrow screen, with smaller nodes. */
.tracker--compact {
    display: block;
    margin: 0;
}

.tracker--compact .tracker-step {
    flex: none;
    padding: 0 0 14px 30px;
    text-align: left;
}

.tracker--compact .tracker-step:last-child {
    padding-bottom: 0;
}

.tracker--compact .tracker-step::before {
    top: 0;
    left: 0;
    width: 21px;
    height: 21px;
    margin-left: 0;
    font-size: 10.5px;
    border-width: 2px;
}

.tracker--compact .tracker-step.is-done::before {
    font-size: 11px;
}

.tracker--compact .tracker-step.is-current::before {
    box-shadow: 0 0 0 3px var(--brand-ring);
}

.tracker--compact .tracker-step::after {
    top: 21px;
    left: 10px;
    width: 2px;
    height: calc(100% - 21px);
}

.tracker--compact .tracker-label {
    font-size: 13px;
    line-height: 21px;
}

/* --- The order in the drum ---------------------------------------------- */

.machine-order {
    padding: 12px 14px;
    background: #D3E3FA;   /* a step deeper, so the small box reads on its card */
    border: 1px solid var(--brand-line);
    border-radius: var(--radius-sm);
}

/* Another customer's load: the neutral, unnamed version of the block above,
   so an anonymous machine never looks like a card with missing data. */
.machine-order--other {
    background: #D6DEE9;   /* a step deeper, so the small box reads on its card */
    border-color: var(--border);
}

.machine-order--other .note {
    color: var(--text-muted);
}

/* The viewer's own laundry, picked out of a row of otherwise anonymous
   machines. A ring rather than a fill, so it reads as "this one is yours"
   without competing with the status colour the card already carries. */
.machine.is-mine {
    border-color: var(--brand);
    box-shadow: 0 0 0 1px var(--brand-line), var(--shadow);
}

.machine.is-mine:hover {
    border-color: var(--brand);
    box-shadow: 0 0 0 1px var(--brand-line), var(--shadow);
}

.machine-order-ref {
    margin: 0 0 2px;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--brand-deep);
}

.machine-order .note {
    font-size: 13px;
}

.machine-note {
    font-size: 13.5px;
}

/* --- Controls ----------------------------------------------------------- */

/* Every control is its own POST form, so the row has to lay the forms out
   rather than the buttons. */
.machine-controls {
    margin-top: auto;
    padding-top: 16px;
    border-top: 1px solid var(--border);
}

.machine-controls .button-row {
    margin: 0;
}

.machine-controls .field {
    margin-bottom: 12px;
}

.machine-controls .field label {
    font-size: 12px;
}

.machine-controls .field select {
    padding: 9px 12px;
    padding-right: 36px;
    font-size: 14px;
}

.machine-start .button-row {
    margin-top: 14px;
}

.machine-secondary {
    margin-top: 10px;
}

.machine-controls .field input {
    padding: 9px 12px;
    font-size: 14px;
}

/* The branch's one Washer time, above the machine cards (2026-11-05). */
.fleet-type-times {
    display: flex;
    flex-wrap: wrap;          /* side by side; stacks when there is no room */
    gap: 12px 24px;
    margin: 18px 0;
}

.machine-set-time.fleet-washer-time {
    flex: 1 1 260px;
    max-width: 420px;
    margin: 0;
}

/* Idle machine: Time + Done, then the Cycle it set (2026-11-05). */
.machine-cycle-set {
    margin: 14px 0 12px;
}

.machine-start .btn:disabled {
    opacity: .55;
    cursor: not-allowed;
}

/* Adjust time on a running or paused machine (2026-11-05). */
.machine-time {
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px dashed var(--border);
}

.machine-time .button-row {
    gap: 8px;
    margin-top: 6px;
}

.machine-set-time,
.machine-link {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    margin-top: 10px;
}

.machine-set-time .field,
.machine-link .field {
    flex: 1;
    min-width: 0;
    margin-bottom: 0;
}

.machine-set-time .btn,
.machine-link .btn {
    flex: none;
    padding-top: 9px;
    padding-bottom: 9px;
}

@media (prefers-reduced-motion: reduce) {
    .cycle-bar-fill {
        transition: none;
    }
}
/* ==========================================================================
   13. Utilities
   ========================================================================== */

.muted {
    color: var(--text-muted);
}

.note {
    margin: 0;
    font-size: 14.5px;
    line-height: 1.65;
    color: var(--text-muted);
    max-width: 72ch;
}

.feature-list + .note {
    margin-top: 16px;
}

/* Empty-state line inside a panel, e.g. "No orders yet". */
.empty {
    margin: 0;
    padding: 36px 20px;
    color: var(--text-muted);
    font-size: 15px;
    text-align: center;
    background: #E3EAF3;   /* a step deeper, so the small box reads on its card */
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm);
}

/* Visible to screen readers only. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ==========================================================================
   14. Small screens
   ========================================================================== */

@media (max-width: 680px) {
    /* The stepper turns vertical, with the rail running down the left. This
       avoids five nodes wrapping into a broken second row. */
    .tracker {
        display: block;
    }

    .tracker-step {
        padding: 4px 0 20px 46px;
        text-align: left;
    }

    .tracker-step:last-child {
        padding-bottom: 0;
    }

    .tracker-step::before {
        top: 0;
        left: 0;
        margin-left: 0;
    }

    .tracker-step::after {
        top: 32px;
        left: 15px;
        width: 2px;
        height: calc(100% - 32px);
    }
}

@media (max-width: 620px) {
    .toolbar {
        flex-wrap: wrap;
    }

    .toolbar .field {
        flex-basis: 100%;
    }

    .row-actions {
        justify-content: flex-start;
    }

    td.row-actions {
        text-align: left;
    }
}

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

    .page-auth {
        padding: 20px 12px;
        align-items: flex-start;
    }

    .auth-card {
        padding: 32px 22px 26px;
    }

    .auth-card h1 {
        font-size: 25px;
    }

    /* The bar now holds a menu button, a branch selector, a bell and an
       account menu, so it wraps its row instead of stacking into a column —
       stacking would push the page content most of a screen down. */
    .topbar {
        gap: 10px;
        padding: 12px 14px;
    }

    .sidebar {
        width: min(300px, 86vw);
    }

    .container {
        padding: 28px 16px 52px;
    }

    .page-header h1 {
        font-size: 25px;
    }

    .panel {
        padding: 22px 18px;
    }

    .card {
        padding: 22px 18px;
    }

    /* One upload field per row at this width, so its thumbnail can afford to
       be a little larger. */
    .shop-cover--thumb {
        width: 132px;
        height: 74px;
    }

    .meta-list {
        grid-template-columns: 1fr;
        gap: 2px;
    }

    .meta-list dd + dt {
        margin-top: 14px;
    }

    .btn {
        white-space: normal;
    }

    .button-row .btn {
        flex: 1 1 auto;
    }
}

/* ==========================================================================
   15. Dark mode

   Only the tokens are redefined, so every component follows automatically.
   Nothing here changes layout.
   ========================================================================== */

@media not all {   /* dark theme switched off -- every page is always light; see the note at the top of this file */
    :root {
        --brand:          #60a5fa;
        --brand-strong:   #93c5fd;
        --brand-deep:     #bfdbfe;
        --brand-bright:   #93c5fd;
        --brand-wash:     #16243d;
        --brand-mist:     #131f33;
        --brand-tint:     rgba(96, 165, 250, .14);
        --brand-line:     rgba(96, 165, 250, .30);
        --brand-ring:     rgba(96, 165, 250, .26);

        --text:           #e8eef7;
        --text-soft:      #c2cfe0;
        --text-muted:     #94a3b8;
        --text-faint:     #64748b;
        --on-brand:       #06203f;

        --bg:             #0b1220;
        --bg-deep:        #070d18;
        --surface:        #131c2e;
        --surface-raised: #182236;
        --surface-sunken: #0f1727;
        --border:         #24314a;
        --border-strong:  #33425f;
        --border-input:   #33425f;
        --topbar-bg:      rgba(19, 28, 46, .82);

        /* Deeper still in dark mode, so the panel stays distinct from the
           page behind it instead of merging with it. */
        --sidebar-bg:       #070f1e;
        --sidebar-bg-soft:  #16264a;
        --sidebar-text:     #cbd9ee;
        --sidebar-text-dim: #8296b5;
        --sidebar-line:     rgba(255, 255, 255, .09);
        --sidebar-active:   #2563eb;

        --success:        #57c295;
        --success-bg:     #102e24;
        --success-line:   rgba(87, 194, 149, .28);
        --danger:         #f08a80;
        --danger-bg:      #331613;
        --danger-line:    rgba(240, 138, 128, .28);
        --warn:           #e0aa5c;
        --warn-bg:        #33260f;
        --warn-line:      rgba(224, 170, 92, .28);

        --maint:          #f0a173;
        --maint-bg:       #3a1f10;
        --maint-line:     rgba(240, 161, 115, .28);

        --shadow-sm:      0 1px 2px rgba(0, 0, 0, .40);
        --shadow:         0 1px 2px rgba(0, 0, 0, .35),
                          0 10px 28px -10px rgba(0, 0, 0, .60);
        --shadow-lg:      0 2px 4px rgba(0, 0, 0, .35),
                          0 28px 56px -20px rgba(0, 0, 0, .70);

        --mark: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.2c4.4 5.3 6.8 8.6 6.8 11.6a6.8 6.8 0 0 1-13.6 0c0-3 2.4-6.3 6.8-11.6z' fill='%2360a5fa'/%3E%3Ccircle cx='9.4' cy='14.6' r='1.9' fill='%2306203f' opacity='.8'/%3E%3C/svg%3E");
    }

    /* The two page backgrounds paint white directly, so they are restated
       rather than left to the tokens. */
    .page-auth {
        background:
            radial-gradient(720px 420px at 12% 0%, rgba(15, 23, 42, .55) 0%, rgba(15, 23, 42, 0) 60%),
            radial-gradient(680px 420px at 92% 8%, var(--brand-wash) 0%, rgba(19, 28, 46, 0) 62%),
            linear-gradient(165deg, var(--bg-deep) 0%, var(--bg) 60%, #131c2e 100%);
    }

    .page-app {
        background:
            radial-gradient(900px 340px at 50% 0%, var(--brand-mist) 0%, rgba(15, 23, 42, 0) 72%),
            var(--bg);
    }

    /* The select chevron is a fixed-colour image, so it gets a lighter one. */
    .field select {
        background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%2394a3b8' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    }

    .field input,
    .field select {
        box-shadow: none;
    }
}

/* ==========================================================================
   16. Motion and print
   ========================================================================== */

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

    .card:hover {
        transform: none;
    }
}

/* Order pages get printed at the shop counter. */
@media print {
    body {
        background: #fff;
        color: #000;
    }

    .topbar,
    .sidebar,
    .nav-scrim,
    .button-row,
    .row-actions,
    .chip-row,
    .backlink,
    .btn {
        display: none !important;
    }

    /* Without the sidebar there is no second column to lay out. */
    .app-shell {
        display: block;
    }

    .container {
        max-width: none;
        padding: 0;
    }

    .panel,
    .card,
    .table-wrap {
        box-shadow: none;
        border-color: #999;
        break-inside: avoid;
    }

    /* A photo card prints as a plain one. Browsers drop background images from
       a printout by default, which would leave white text on white paper. */
    .card--photo {
        background: #fff;
        color: #000;
    }

    .card--photo h3,
    .card--photo .card-desc,
    .card--photo .shop-facts li,
    .card--photo .meta-list dt,
    .card--photo .meta-list dd,
    .card--photo .rating-score,
    .card--photo .rating-count,
    .card--photo .rating-none-text,
    .card--photo .muted {
        color: #000;
        text-shadow: none;
    }

    .tracker-step::before {
        color: #000;
        background: #fff;
        border-color: #000;
    }
}

/* ==========================================================================
   17. Public portal (PALABAHAN brand palette)

   Scope
     Everything in this section is scoped to the two PUBLIC body classes:

       <body class="page-portal">   index.php - the portal chooser
       <body class="page-auth">     login / register / forgot password

     The signed-in dashboards (<body class="page-app">) are deliberately NOT
     touched. That was a deliberate call: the brand palette below is what the
     PALABAHAN reference design asks for on the doors of the system, while the
     teal in section 1 is what every dashboard screen already uses. Scoping the
     override to a body class means one file still holds both, and rolling the
     palette across the whole app later is a matter of moving these token
     values up into :root rather than rewriting a single component.

   How the override works
     Only the BRAND tokens and the two page washes are redefined. Every
     component below - .btn, .card, .field, .alert, .feature-list, .chip - is
     reused untouched from the sections above and simply follows the new
     values, so there is no second copy of any component to keep in step.
   ========================================================================== */

.page-portal,
.page-auth {
    /* PALABAHAN brand: orange #2563eb, yellow #60a5fa, dark #0f172a. */
    --brand:          #1d4ed8;   /* the blue at text/link weight, for contrast */
    --brand-strong:   #1e40af;
    --brand-deep:     #0f172a;
    --brand-bright:   #60a5fa;
    --brand-wash:     #dbeafe;
    --brand-mist:     #f0f7ff;
    --brand-tint:     rgba(37, 99, 235, .12);
    --brand-line:     rgba(37, 99, 235, .28);
    --brand-ring:     rgba(37, 99, 235, .24);

    /* The blue at full strength, for fills where contrast is carried by
       white text rather than by the colour itself. */
    --palabahan-orange: #2563eb;
    --palabahan-yellow: #60a5fa;
    --palabahan-dark:   #0f172a;
    --palabahan-gray:   #8B8B8B;

    /* Ice blue: the ground the orange sits on.
       A laundry is water and cold clean linen, so the page itself reads cool
       and the brand blue stays the accent on top of it -- buttons, ticks,
       the card edge. Held in tokens of its own rather than written into each
       gradient, because the portal page and the login pages paint two
       different washes out of the same three values. */
    --ice:            #D3D9E1;   /* the page ground: soft blue-grey, so white cards stand out */
    --ice-deep:       #D3D9E1;
    --ice-mist:       #E7ECF3;

    /* How much of the photograph is painted out.
       assets/img/portal-bg.jpg sits at the BOTTOM of the background stack on
       BOTH .page-portal and .page-auth, and these two are the alpha of the
       wash laid over it: the -mid one is deliberately lighter so the middle
       of the page, where the cards sit, is the clearest part of the picture.

       One pair of tokens for both, deliberately. The portal chooser, the shop
       chooser and the shop login are seen one straight after the other -- and
       if their washes were tuned separately the picture would visibly jump
       brightness at each hand-off, which reads as a loading glitch rather
       than as a design. Tune here and the whole front door moves together.

       They are tokens rather than literals because tuning a photographic
       background is a matter of turning one dial until the cards read, and
       that dial should not be buried inside a five-layer shorthand. */
    --photo-wash:     .32;
    --photo-wash-mid: .20;

    --bg:             var(--ice);
    --bg-deep:        var(--ice-deep);

    /* Kept a step darker than --ice, so a white card still has a visible edge
       against the ground. These have to move whenever the ice does: at the
       paler setting they were only just darker than the page, and deepening
       the ground without them would have hidden every card border. */
    --border:         #e2e8f0;
    --border-strong:  #cbd5e1;
    --border-input:   #cbd5e1;

    /* The wordmark droplet, redrawn in the brand blue. */
    --mark: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.2c4.4 5.3 6.8 8.6 6.8 11.6a6.8 6.8 0 0 1-13.6 0c0-3 2.4-6.3 6.8-11.6z' fill='%232563eb'/%3E%3Ccircle cx='9.4' cy='14.6' r='1.9' fill='%23ffffff' opacity='.9'/%3E%3C/svg%3E");
}

/* Primary buttons on the public pages take the full-strength orange. .btn
   itself is untouched - only the fill it paints with. */
.page-portal .btn,
.page-auth .btn {
    background: linear-gradient(180deg, #3b82f6 0%, var(--palabahan-orange) 100%);
    border-color: #1d4ed8;
}

.page-portal .btn:hover,
.page-auth .btn:hover {
    background: linear-gradient(180deg, var(--palabahan-orange) 0%, #1d4ed8 100%);
}

/* .btn--ghost stays an outline button; it only needs the orange edge. */
.page-portal .btn--ghost,
.page-auth .btn--ghost {
    background: var(--surface);
    border-color: var(--brand-line);
    color: var(--brand-strong);
}

.page-portal .btn--ghost:hover,
.page-auth .btn--ghost:hover {
    background: var(--brand-mist);
    border-color: var(--palabahan-orange);
}

/* --------------------------------------------------------------------------
   17a. Portal page frame

   A column: header, content that grows, dark footer pinned to the bottom of
   the viewport on short pages.
   -------------------------------------------------------------------------- */

.page-portal {
    display: flex;
    flex-direction: column;
    min-height: 100vh;

    /* The photograph is the LAST layer in the stack, which in CSS means the
       bottom-most one: everything above it -- the bubbles and the four washes
       -- now composites over it instead of hiding it. That is also why the
       washes stopped being solid --ice tokens and became rgba(): a solid
       white layer would paint the picture out completely.

       --photo-wash (section 17) is the one number to turn when
       the picture fights the text. Lower it to let more photograph through,
       raise it if the portal cards stop reading cleanly against it. */
    background:
        url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220' viewBox='0 0 220 220'%3E%3Cg fill='none' stroke='%2393b4e8' stroke-opacity='.20'%3E%3Ccircle cx='34' cy='42' r='16'/%3E%3Ccircle cx='150' cy='28' r='9'/%3E%3Ccircle cx='192' cy='96' r='20'/%3E%3Ccircle cx='96' cy='120' r='12'/%3E%3Ccircle cx='28' cy='168' r='11'/%3E%3Ccircle cx='140' cy='186' r='17'/%3E%3C/g%3E%3C/svg%3E"),
        radial-gradient(760px 420px at 10% -6%, rgba(255, 255, 255, .58) 0%, rgba(255, 255, 255, 0) 62%),
        radial-gradient(700px 420px at 94% 4%, rgba(255, 255, 255, .40) 0%, rgba(255, 255, 255, 0) 64%),
        /* One low brand glow along the bottom edge, so the blue is present in
           the page and not only in the buttons sitting on it. */
        radial-gradient(560px 340px at 50% 104%, rgba(37, 99, 235, .16) 0%, rgba(37, 99, 235, 0) 70%),
        linear-gradient(168deg,
            rgba(255, 255, 255, var(--photo-wash)) 0%,
            rgba(255, 255, 255, var(--photo-wash-mid)) 48%,
            rgba(219, 234, 254, var(--photo-wash)) 100%),
        url("../img/portal-bg.jpg");
    background-color: var(--ice);
    background-size:       220px 220px, cover, cover, cover, cover, cover;
    background-position:   0 0, center, center, center, center, center;
    background-repeat:     repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
    background-attachment: scroll;
}

/* The white veil that keeps the text readable over the picture above.
   It is the other half of how deep the ice reads, and the reason it is this
   low: the veil composites over the whole page, so at .58 the ice washed out
   to near-white however deep the tokens were set. Kept at .32 so the portal
   lands close to the login pages, which carry no veil at all -- the two are
   seen one after the other and a visible jump between them would look like a
   loading glitch rather than a design. The bubble pattern is only a .20 stroke
   to begin with, so this is still plenty to hold text legible over it. */
.page-portal::before {
    content: "";
    position: fixed;
    inset: 0;
    background: rgba(255, 255, 255, .10);
    pointer-events: none;
    z-index: 0;
}

.page-portal > * {
    position: relative;
    z-index: 1;
}

/* The login pages get the same ice ground, restated rather than left to the
   tokens.
   Section 4 builds this wash out of --brand-wash and --brand-mist, and those
   two are now the ORANGE tints the components use (the illustration band, the
   help pill, the ghost button's hover). Letting the page keep painting itself
   from them would put an orange haze behind every login card. So the page
   names the ice tokens directly, and the brand tints stay component tints. */
/* The same photograph the portal carries, for the same reason it carries it:
   pressing Customer Login goes portal -> shop chooser -> this form in about
   two clicks, and a picture that vanished on the third screen would read as
   the page having failed to load rather than as a different page.

   The angle and the stops are the login page's own, not copied from the
   portal -- this page centres a single card instead of laying out two, so the
   clear part of the picture is nudged to where the card is not. What IS
   shared is --photo-wash, so the two screens are the same brightness.

   This is section 4's .page-auth restated, and it is deliberately the one
   that wins: same specificity, later in the file. Section 4 keeps painting
   itself from --brand-wash / --brand-mist, which are the BLUE component
   tints; letting those through here would put a coloured haze over the
   photograph. */
.page-auth {
    background:
        radial-gradient(720px 420px at 12% 0%, rgba(255, 255, 255, .52) 0%, rgba(255, 255, 255, 0) 60%),
        radial-gradient(680px 420px at 92% 8%, rgba(255, 255, 255, .38) 0%, rgba(230, 237, 245, 0) 62%),
        radial-gradient(520px 320px at 50% 100%, rgba(37, 99, 235, .14) 0%, rgba(37, 99, 235, 0) 70%),
        linear-gradient(165deg,
            rgba(255, 255, 255, var(--photo-wash)) 0%,
            rgba(255, 255, 255, var(--photo-wash-mid)) 52%,
            rgba(219, 234, 254, var(--photo-wash)) 100%),
        url("../img/portal-bg.jpg");
    background-color: var(--ice);
    background-size:       cover, cover, cover, cover, cover;
    background-position:   center, center, center, center, center;
    background-repeat:     no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
    background-attachment: scroll;
}

/* The veil, exactly as .page-portal carries it. The auth card itself is
   opaque and would read over anything, but the wordmark, the footer links and
   the "back to shops" line all sit directly on the picture, and those are the
   ones that need the ground held still behind them. Fixed rather than
   absolute, so it does not become a flex item of .page-auth--framed and
   disturb the card's auto margins. */
.page-auth::before {
    content: "";
    position: fixed;
    inset: 0;
    background: rgba(255, 255, 255, .10);
    pointer-events: none;
    z-index: 0;
}

.page-auth > * {
    position: relative;
    z-index: 1;
}

/* --------------------------------------------------------------------------
   17b. Public header - wordmark left, help line right
   -------------------------------------------------------------------------- */

.portal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18px;
    flex-wrap: wrap;
    padding: 16px clamp(16px, 4vw, 40px);
    background: rgba(255, 255, 255, .88);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-sm);
}

.portal-mark {
    display: flex;
    align-items: center;
    gap: 12px;
    text-decoration: none;
    color: inherit;
}

/* The washing machine, inline SVG for the same reason as every other mark in
   this file: no image file to lose, no CDN to fail. */
.portal-mark-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 46px;
    height: 46px;
    border-radius: 13px;
    background: linear-gradient(160deg, var(--palabahan-orange) 0%, #1d4ed8 100%);
    box-shadow: 0 6px 16px -8px rgba(37, 99, 235, .9);
}

.portal-mark-icon svg {
    width: 27px;
    height: 27px;
    fill: none;
    stroke: #fff;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.portal-mark-name {
    display: block;
    font-size: 20px;
    font-weight: 800;
    letter-spacing: .10em;
    line-height: 1.1;
    color: var(--palabahan-dark);
}

.portal-mark-sub {
    display: block;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .19em;
    text-transform: uppercase;
    color: var(--palabahan-gray);
}

.portal-help {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 8px 15px;
    background: var(--brand-mist);
    border: 1px solid var(--brand-line);
    border-radius: var(--radius-pill);
    text-decoration: none;
}

.portal-help svg {
    flex: none;
    width: 19px;
    height: 19px;
    fill: none;
    stroke: var(--palabahan-orange);
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.portal-help-key {
    display: block;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--palabahan-gray);
}

.portal-help-num {
    display: block;
    font-size: 15.5px;
    font-weight: 800;
    color: var(--palabahan-dark);
    white-space: nowrap;
}

/* --------------------------------------------------------------------------
   17c. Hero and the two portal cards
   -------------------------------------------------------------------------- */

.portal-main {
    flex: 1;
    width: 100%;
    max-width: 1080px;
    margin: 0 auto;
    padding: clamp(28px, 5vw, 52px) clamp(16px, 4vw, 32px) 56px;
}

.portal-hero {
    text-align: center;
    margin-bottom: clamp(26px, 4vw, 40px);
}

.portal-hero h1 {
    margin: 0 0 8px;
    font-size: clamp(28px, 5vw, 40px);
    font-weight: 800;
    color: var(--palabahan-dark);
}

.portal-hero .subtitle {
    margin: 0;
    font-size: clamp(15px, 2vw, 17px);
    color: var(--text-muted);
}

/* An accent rule under the heading, orange into yellow. */
.portal-hero h1::after {
    content: "";
    display: block;
    width: 74px;
    height: 4px;
    margin: 14px auto 0;
    border-radius: var(--radius-pill);
    background: linear-gradient(90deg, var(--palabahan-orange), var(--palabahan-yellow));
}

/* Desktop: card | OR | card. The middle column is only as wide as the badge,
   so the two cards stay equal. */
.portal-choice {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: stretch;
    gap: clamp(14px, 2.5vw, 26px);
}

.portal-card {
    display: flex;
    flex-direction: column;
    position: relative;
    padding: 30px 28px 26px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-top: 4px solid var(--palabahan-orange);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    transition: box-shadow var(--slow) var(--ease),
                transform var(--slow) var(--ease);
}

.portal-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-lg);
}

/* The management card wears the dark accent instead, so the two doors read as
   different places before a single word is read. */
.portal-card--admin {
    border-top-color: var(--palabahan-dark);
}

.portal-eyebrow {
    display: inline-flex;
    align-self: flex-start;
    align-items: center;
    gap: 7px;
    margin: 0 0 14px;
    padding: 5px 12px;
    font-size: 11.5px;
    font-weight: 800;
    letter-spacing: .12em;
    text-transform: uppercase;
    border-radius: var(--radius-pill);
    background: var(--brand-tint);
    color: var(--brand-strong);
}

.portal-card--admin .portal-eyebrow {
    background: rgba(15, 23, 42, .09);
    color: var(--palabahan-dark);
}

.portal-card h2 {
    margin: 0 0 8px;
    font-size: 23px;
    font-weight: 800;
    color: var(--palabahan-dark);
}

.portal-card-desc {
    margin: 0 0 20px;
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--text-muted);
}

/* The illustration band. Kept short so the buttons stay above the fold on a
   laptop, and shorter still on a phone. */
.portal-art {
    display: grid;
    place-items: center;
    height: 132px;
    margin-bottom: 22px;
    border-radius: var(--radius);
    background: linear-gradient(160deg, var(--brand-mist) 0%, var(--brand-wash) 100%);
    border: 1px solid var(--brand-line);
}

.portal-card--admin .portal-art {
    background: linear-gradient(160deg, #f8fafc 0%, #e2e8f0 100%);
    border-color: rgba(15, 23, 42, .14);
}

.portal-art svg {
    width: 118px;
    height: 100px;
}

.portal-actions {
    display: grid;
    gap: 10px;
    margin-bottom: 22px;
}

/* The feature list is the shared .feature-list from section 10; only the
   tick's colour differs per card. */
.portal-card .feature-list {
    margin-top: auto;
    font-size: 14.5px;
}

.portal-card .feature-list li::before {
    background: var(--palabahan-orange);
    box-shadow: 0 0 0 3px var(--brand-tint);
}

.portal-card--admin .feature-list li::before {
    background: var(--palabahan-dark);
    box-shadow: 0 0 0 3px rgba(15, 23, 42, .10);
}

/* --------------------------------------------------------------------------
   17d. The OR separator

   A badge with a hairline running through it. On desktop the line is vertical
   and fills the middle column; on mobile it turns horizontal between the two
   stacked cards.
   -------------------------------------------------------------------------- */

.portal-or {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    align-self: stretch;
}

.portal-or::before,
.portal-or::after {
    content: "";
    flex: 1;
    width: 2px;
    min-height: 20px;
    background: linear-gradient(180deg, rgba(148, 163, 184, 0), rgba(148, 163, 184, .38));
}

.portal-or::after {
    background: linear-gradient(180deg, rgba(148, 163, 184, .38), rgba(148, 163, 184, 0));
}

.portal-or span {
    display: grid;
    place-items: center;
    flex: none;
    width: 54px;
    height: 54px;
    border-radius: 50%;
    background: var(--surface);
    border: 2px solid var(--palabahan-yellow);
    box-shadow: var(--shadow-sm);
    font-size: 14px;
    font-weight: 800;
    letter-spacing: .10em;
    color: var(--palabahan-dark);
}

/* --------------------------------------------------------------------------
   17e. Secure-login reassurance line
   -------------------------------------------------------------------------- */

.portal-secure {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    margin: 28px 0 0;
    font-size: 13.5px;
    color: var(--text-muted);
    text-align: center;
}

.portal-secure svg {
    flex: none;
    width: 16px;
    height: 16px;
    fill: none;
    stroke: var(--success);
    stroke-width: 1.9;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* --------------------------------------------------------------------------
   17f. Dark footer
   -------------------------------------------------------------------------- */

.portal-foot {
    padding: 26px clamp(16px, 4vw, 40px);
    background: var(--palabahan-dark);
    color: #cbd5e1;
    text-align: center;
    font-size: 13.5px;
}

.portal-foot p {
    margin: 0;
}

.portal-foot-links {
    margin-top: 10px !important;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 4px 12px;
}

.portal-foot-links a {
    color: var(--palabahan-yellow);
    text-decoration: none;
}

.portal-foot-links a:hover {
    text-decoration: underline;
}

.portal-foot-links .sep {
    color: #64748b;
}

/* --------------------------------------------------------------------------
   17g. Public login pages

   These use the .auth-card shell from section 4 unchanged. The only additions
   are the management card's dark treatment and the row under the password
   field.
   -------------------------------------------------------------------------- */

/* A login page that carries the portal's header and footer is a COLUMN --
   header, card, footer -- not the single centred card .page-auth lays out by
   default. Both bars stay in normal flow rather than being fixed: a fixed bar
   would have to be paid for with a hand-tuned padding on the body, and the one
   screen where that always goes wrong is a phone in landscape, where a tall
   form ends up sliding under a footer that is covering a third of it. */
.page-auth--framed {
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    padding: 0;
}

/* auto side margins centre the card inside the column; the vertical margin is
   the breathing room the body's padding used to give it. */
.page-auth--framed > .auth-card {
    margin: clamp(26px, 5vw, 52px) auto;
    max-width: min(440px, calc(100% - 32px));
}

.page-auth--framed > .auth-card--wide {
    max-width: min(560px, calc(100% - 32px));
}

/* Pushes the footer to the bottom of the viewport on a short page, and leaves
   it directly under the card on a tall one. */
.page-auth--framed > .portal-foot {
    margin-top: auto;
}

/* The management door drops the auth card's orange top rule for a dark one,
   matching the portal card the visitor clicked to get here. */
.auth-card--admin::before {
    background: linear-gradient(90deg,
        var(--palabahan-dark) 0%,
        #334155 55%,
        var(--palabahan-yellow) 100%);
}

.auth-card--admin .btn {
    background: linear-gradient(180deg, #1e293b 0%, var(--palabahan-dark) 100%);
    border-color: #0b1220;
}

.auth-card--admin .btn:hover {
    background: linear-gradient(180deg, var(--palabahan-dark) 0%, #070d18 100%);
}

/* A one-line note naming which roles a door admits. */
.auth-note {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 0 0 22px;
    padding: 13px 15px;
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--text-soft);
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-left: 3px solid var(--palabahan-orange);
    border-radius: var(--radius-sm);
}

.auth-card--admin .auth-note {
    border-left-color: var(--palabahan-dark);
}

.auth-note svg {
    flex: none;
    width: 17px;
    height: 17px;
    margin-top: 1px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity: .7;
}

/* The "Forgot password?" row under the password field. */
.field-aside {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 14px;
    margin: -6px 0 20px;
    font-size: 13.5px;
}

.field-aside a {
    color: var(--brand-strong);
    font-weight: 650;
    text-decoration: none;
}

.field-aside a:hover {
    text-decoration: underline;
}

/* --------------------------------------------------------------------------
   17h. Small screens

   The cards stack and the OR turns horizontal. Nothing is allowed to force a
   sideways scroll, which is why the grid drops to a single column rather than
   letting the three-column track shrink.
   -------------------------------------------------------------------------- */

@media (max-width: 860px) {
    .portal-choice {
        grid-template-columns: 1fr;
    }

    .portal-or {
        flex-direction: row;
        padding: 2px 0;
    }

    .portal-or::before,
    .portal-or::after {
        width: auto;
        height: 2px;
        min-height: 0;
        min-width: 20px;
    }

    .portal-or::before {
        background: linear-gradient(90deg, rgba(148, 163, 184, 0), rgba(148, 163, 184, .38));
    }

    .portal-or::after {
        background: linear-gradient(90deg, rgba(148, 163, 184, .38), rgba(148, 163, 184, 0));
    }
}

@media (max-width: 560px) {
    .portal-header {
        justify-content: center;
        text-align: center;
    }

    .portal-help {
        width: 100%;
        justify-content: center;
    }

    .portal-card {
        padding: 24px 20px 22px;
    }

    .portal-art {
        height: 104px;
    }

    .portal-art svg {
        width: 96px;
        height: 82px;
    }
}

/* --------------------------------------------------------------------------
   17i. Dark mode

   Only the brand tokens are lightened. The surfaces, ink and shadows keep
   coming from section 15, so the public pages follow the same dark theme as
   the rest of the app rather than staying stubbornly cream.
   -------------------------------------------------------------------------- */

@media not all {   /* dark theme switched off -- every page is always light; see the note at the top of this file */
    .page-portal,
    .page-auth {
        --brand:          #60a5fa;
        --brand-strong:   #93c5fd;
        --brand-deep:     #bfdbfe;
        --brand-bright:   #60a5fa;
        --brand-wash:     #16243d;
        --brand-mist:     #131f33;
        --brand-tint:     rgba(37, 99, 235, .18);
        --brand-line:     rgba(37, 99, 235, .34);
        --brand-ring:     rgba(37, 99, 235, .28);

        /* Ice blue after dark is a deep slate, not a warm brown. Cooling these
           with the light ones is what stops the page changing character
           between the two themes. */
        /* Deepened alongside the light ones, but by SATURATION rather than by
           lightness. A dark theme that is asked for "deeper" wants more blue
           in the slate, not a blacker page -- taking the lightness down here
           would only cost contrast against the text. */
        --ice:            #0f1a2b;
        --ice-deep:       #080f1c;
        --ice-mist:       #16233a;

        --bg:             var(--ice);
        --bg-deep:        var(--ice-deep);
        --border:         #24314a;
        --border-strong:  #33425f;
        --border-input:   #33425f;
    }

    /* The same photograph, under a dark wash instead of a white one. The
       picture is not swapped for a night-time one -- one file is what the
       shop uploads -- so after dark it is simply held further back, which is
       why these alphas are higher than the light theme's. */
    .page-portal {
        background:
            radial-gradient(700px 420px at 94% 4%, rgba(22, 35, 58, .55) 0%, rgba(15, 23, 42, 0) 64%),
            radial-gradient(560px 340px at 50% 104%, rgba(37, 99, 235, .16) 0%, rgba(37, 99, 235, 0) 70%),
            linear-gradient(168deg, rgba(8, 15, 28, .76) 0%, rgba(15, 26, 43, .66) 60%, rgba(15, 23, 42, .84) 100%),
            url("../img/portal-bg.jpg");
        background-color: var(--ice);
        background-size:       cover, cover, cover, cover;
        background-position:   center, center, center, center;
        background-repeat:     no-repeat, no-repeat, no-repeat, no-repeat;
        background-attachment: scroll;
    }

    /* A veil, not a wash: over a dark ground the job is to hold the
       photograph back, so it darkens instead of lightening. */
    .page-portal::before {
        background: rgba(0, 0, 0, .22);
    }

    /* The photograph after dark, held back the same amount the portal holds
       it back so the two screens still match in the dark theme. */
    .page-auth {
        background:
            radial-gradient(700px 420px at 92% 6%, rgba(22, 35, 58, .52) 0%, rgba(15, 23, 42, 0) 62%),
            radial-gradient(520px 320px at 50% 100%, rgba(37, 99, 235, .14) 0%, rgba(37, 99, 235, 0) 70%),
            linear-gradient(165deg, rgba(8, 15, 28, .76) 0%, rgba(15, 26, 43, .66) 60%, rgba(15, 23, 42, .84) 100%),
            url("../img/portal-bg.jpg");
        background-color: var(--ice);
        background-size:       cover, cover, cover, cover;
        background-position:   center, center, center, center;
        background-repeat:     no-repeat, no-repeat, no-repeat, no-repeat;
        background-attachment: scroll;
    }

    .page-auth::before {
        background: rgba(0, 0, 0, .22);
    }

    .portal-header {
        background: rgba(11, 18, 32, .92);
    }

    .portal-mark-name,
    .portal-help-num,
    .portal-card h2,
    .portal-hero h1,
    .portal-or span {
        color: #eff6ff;
    }

    .portal-card--admin .portal-art {
        background: linear-gradient(160deg, #24314a 0%, #131c2e 100%);
        border-color: rgba(255, 255, 255, .10);
    }

    .portal-card--admin .portal-eyebrow {
        background: rgba(255, 255, 255, .09);
        color: #dbeafe;
    }

    .portal-card--admin .feature-list li::before {
        background: #60a5fa;
        box-shadow: 0 0 0 3px rgba(96, 165, 250, .16);
    }

    .auth-card--admin .btn {
        background: linear-gradient(180deg, #64748b 0%, #334155 100%);
        border-color: #1e293b;
    }
}

/* ==========================================================================
   18. Customer website (the public pages beyond the portal chooser)

   Scope
     Everything here is scoped to <body class="page-portal">, the same body
     class index.php already used, so the signed-in dashboards are untouched.
     Section 17 above still supplies the palette; this section only adds the
     layouts the public pages need and that nothing else in the app has:
     a top nav, a split hero, a step list and a stat tile.

   Reuse first
     .card, .card-grid, .panel, .table, .btn, .field, .shop-facts, .tracker,
     .badge and .section-head are all reused untouched from the sections above.
     A new class appears below only where there was genuinely nothing to reuse.
   ========================================================================== */

/* --------------------------------------------------------------------------
   18a. Public navigation

   Collapses behind a hamburger with no JavaScript: the checkbox sits just
   before the header, so a sibling selector can open the menu. With CSS off the
   nav is a plain list of links, which is still a usable menu.
   -------------------------------------------------------------------------- */

.portal-nav {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px 6px;
    margin-left: auto;
}

.portal-nav-link {
    padding: 8px 12px;
    border-radius: var(--radius-pill);
    color: var(--text-soft);
    font-size: 14.5px;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.portal-nav-link:hover {
    background: var(--brand-mist);
    color: var(--brand-strong);
    text-decoration: none;
}

/* The current page keeps the tint as well as the colour, so it reads at a
   glance on a row of six links. */
.portal-nav-link.is-current {
    background: var(--brand-tint);
    color: var(--brand-strong);
}

.portal-nav-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-left: 10px;
    padding-left: 14px;
    border-left: 1px solid var(--border);
}

/* Hidden on wide screens, where the nav is already visible. */
.portal-navtoggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    margin-left: auto;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    cursor: pointer;
}

.portal-navtoggle svg {
    width: 22px;
    height: 22px;
    fill: none;
    stroke: var(--text);
    stroke-width: 2;
    stroke-linecap: round;
}

.portal-navtoggle:hover {
    border-color: var(--palabahan-orange);
}

/* --------------------------------------------------------------------------
   18b. Wider content column

   The portal chooser is two cards and wants a narrow column. The home page is
   a four-up grid and a price table, and would look starved in the same width.
   -------------------------------------------------------------------------- */

.portal-main--wide {
    max-width: 1180px;
}

/* --------------------------------------------------------------------------
   18c. Home hero
   -------------------------------------------------------------------------- */

.home-hero {
    display: grid;
    grid-template-columns: 1.05fr .95fr;
    align-items: center;
    gap: clamp(20px, 4vw, 48px);
    margin-bottom: clamp(34px, 6vw, 60px);
}

.home-hero-copy h1 {
    margin: 10px 0 14px;
    font-size: clamp(32px, 6vw, 54px);
    line-height: 1.08;
    font-weight: 800;
    letter-spacing: -.02em;
    color: var(--palabahan-dark);
}

.home-hero-sub {
    margin: 0;
    max-width: 46ch;
    font-size: clamp(15px, 2vw, 18px);
    line-height: 1.6;
    color: var(--text-soft);
}

.home-hero-note {
    margin: 18px 0 0;
    font-size: 13.5px;
    color: var(--text-muted);
}

.home-hero-art {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(12px, 3vw, 26px);
    background: linear-gradient(155deg, var(--ice-mist) 0%, var(--surface) 62%, var(--brand-mist) 100%);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
}

.home-hero-art svg {
    width: 100%;
    max-width: 380px;
    height: auto;
}

/* --------------------------------------------------------------------------
   18d. Sections, feature cards and stat tiles
   -------------------------------------------------------------------------- */

.home-section {
    margin-bottom: clamp(32px, 5vw, 54px);
}

/* Four across where there is room, so the feature row and the stat row both
   land on one line instead of breaking three plus one. */
.card-grid--four {
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}

.feature-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 46px;
    height: 46px;
    margin-bottom: 12px;
    background: #C9DCF9;   /* a step deeper, so the small box reads on its card */
    border-radius: var(--radius);
    color: var(--brand-strong);
}

.feature-icon .nav-icon {
    width: 24px;
    height: 24px;
}

/* THE VERTICAL RHYTHM OF THE HOME PAGE.

   Two numbers, and only two. A new SECTION starts 34px below the one before
   it -- the gap .section-head already opens above itself -- and everything
   INSIDE a section sits 16px apart, the gap .section-head opens below itself.
   Read down dashboards/home.php and every boundary is now one or the other.

   .card-grid's own 22px is neither, and it was landing on the two boundaries
   below because the stat row has no heading of its own to carry the spacing.
   Both rules are matched by :has() on the Home page's own pairing rather than
   by a new class, so the 22px every other page uses is left alone -- no other
   page puts two four-up grids back to back, or a .note straight after one. */

/* The feature row ENDS a section; the stat tiles below start a new one. */
.card-grid--four:has(+ .card-grid--four) {
    margin-bottom: 34px;
}

/* The note belongs to the stat tiles it explains, so it sits a section-inner
   16px under them instead of drifting the full 22px away. */
.card-grid--four:has(+ .note) {
    margin-bottom: 16px;
}

/* .card-desc keeps its 18px for the cards that carry something after it -- the
   Services page puts a price below it. On the Home feature cards it is the
   last thing in the card, so that margin lands on top of the card's own 24px
   of bottom padding and the tallest card in the row reads 42px at the bottom
   against 26px at the top. Same idea as .panel > :last-child. */
.feature-card > .card-desc:last-child {
    margin-bottom: 0;
}

.stat-card {
    text-align: center;
}

.stat-value {
    margin: 0;
    font-size: clamp(26px, 4vw, 34px);
    font-weight: 800;
    line-height: 1.1;
    color: var(--brand-strong);
}

.stat-label {
    margin: 6px 0 0;
    font-size: 13.5px;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   18e. Numbered step list

   A CSS counter rather than a list-style, so the number can be a filled disc
   that matches the .tracker nodes on the order pages.
   -------------------------------------------------------------------------- */

.home-steps {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 18px;
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: home-step;
}

.home-step {
    position: relative;
    counter-increment: home-step;
    padding: 20px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
}

.home-step::before {
    content: counter(home-step);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-bottom: 12px;
    /* Falls back to --brand because .home-step is used on the signed-in Home
       page too, where the --palabahan-* tokens are not defined. */
    background: var(--palabahan-orange, var(--brand));
    border-radius: 50%;
    color: #fff;
    font-size: 15px;
    font-weight: 700;
}

.home-step h3 {
    margin: 0 0 6px;
    font-size: 16px;
}

.home-step p {
    margin: 0;
    font-size: 14px;
    line-height: 1.55;
    color: var(--text-soft);
}

/* --------------------------------------------------------------------------
   18f. Price line, track form, closing call to action
   -------------------------------------------------------------------------- */

.service-price {
    margin: 4px 0 12px;
    font-size: 21px;
    /* Same reason as .home-step::before: the Services page is a signed-in page
       now, where --palabahan-dark does not exist. */
    color: var(--palabahan-dark, var(--text));
}

.service-price strong {
    font-weight: 800;
}

.service-price-unit {
    display: block;
    margin-top: 2px;
    font-size: 12.5px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Field and button side by side once there is room for both. */
.track-form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 12px;
}

.track-form .field {
    flex: 1 1 260px;
    margin-bottom: 0;
}

.home-cta {
    padding: clamp(26px, 4vw, 40px);
    background: var(--palabahan-dark);
    border-radius: var(--radius-lg);
    text-align: center;
    color: #f8fafc;
}

.home-cta h2 {
    margin: 0 0 8px;
    font-size: clamp(21px, 3vw, 28px);
    color: #fff;
}

.home-cta p {
    margin: 0 auto;
    max-width: 52ch;
    color: #cbd5e1;
}

.home-cta .button-row {
    justify-content: center;
}

/* The ghost button sits on the dark panel here, so it needs its own edge. */
.home-cta .btn--ghost {
    background: transparent;
    border-color: rgba(255, 255, 255, .38);
    color: #fff;
}

.home-cta .btn--ghost:hover {
    background: rgba(255, 255, 255, .10);
    border-color: #fff;
}

/* --------------------------------------------------------------------------
   18g. Small screens

   The nav folds away first, then the hero stacks with the artwork above the
   copy, because the picture is what says "laundry" fastest on a phone.
   -------------------------------------------------------------------------- */

@media (max-width: 960px) {
    .portal-navtoggle {
        display: inline-flex;
    }

    .portal-nav {
        display: none;
        flex-basis: 100%;
        flex-direction: column;
        align-items: stretch;
        gap: 2px;
        margin: 10px 0 0;
        padding-top: 10px;
        border-top: 1px solid var(--border);
    }

    /* The checkbox is a sibling of .portal-header, so this opens the menu with
       no script involved. */
    .portal-nav-box:checked ~ .portal-header .portal-nav {
        display: flex;
    }

    .portal-nav-link {
        padding: 11px 12px;
        border-radius: var(--radius);
    }

    .portal-nav-actions {
        margin: 8px 0 0;
        padding: 10px 0 0;
        border-left: none;
        border-top: 1px solid var(--border);
    }

    .portal-nav-actions .btn {
        flex: 1;
        justify-content: center;
    }

    /* The phone number leaves the header row on a phone; the footer and the
       contact page still carry it. */
    .portal-help {
        display: none;
    }
}

@media (max-width: 780px) {
    .home-hero {
        grid-template-columns: 1fr;
        text-align: center;
    }

    .home-hero-art {
        order: -1;
    }

    .home-hero-sub {
        margin-inline: auto;
    }

    .home-hero .button-row {
        justify-content: center;
    }
}

@media (max-width: 560px) {
    /* Section 17 centres the header on a phone; the toggle has to stay pinned
       to the right of the wordmark rather than being centred with it. */
    .portal-header {
        justify-content: space-between;
        text-align: left;
    }

    .home-hero .button-row .btn {
        flex: 1 1 100%;
    }

    .track-form .btn {
        width: 100%;
    }
}

/* --------------------------------------------------------------------------
   18h. Dark mode

   Only the few places that hard-code a light surface. Everything else follows
   the tokens redefined in section 17i.
   -------------------------------------------------------------------------- */

@media not all {   /* dark theme switched off -- every page is always light; see the note at the top of this file */
    .home-hero-copy h1,
    .service-price {
        color: #eff6ff;
    }

    .home-hero-art {
        background: linear-gradient(155deg, var(--ice-mist) 0%, #16233a 62%, var(--brand-mist) 100%);
    }

    .portal-navtoggle {
        background: rgba(255, 255, 255, .06);
    }

    .home-cta {
        background: #0f1727;
        border: 1px solid var(--border);
    }
}

/* ==========================================================================
   19. Dashboard charts and stat tiles

   Scope
     The tiles reuse .card / .stat-card / .stat-value / .stat-label from
     section 18 unchanged -- those were written without a palette of their own
     precisely so both sides of the app could use them, and on <body
     class="page-app"> they resolve to the teal tokens rather than the orange.
     Only the two things section 18 had no need for are added here: the icon
     chip on a tile, and the chart itself.

   Colour
     Nothing in includes/charts.php carries a colour. Every drawn element takes
     its paint from the rules below, so the charts follow the dashboard palette
     and its dark mode instead of holding a second copy of it.
   ========================================================================== */

.stat-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin-bottom: 10px;
    background: #C9DCF9;              /* solid and a step deeper, so it reads on the off-white card */
    border: 1px solid #8FB3EC;
    border-radius: var(--radius);
    color: var(--brand);
}

.stat-icon .nav-icon {
    width: 21px;
    height: 21px;
}

/* A tile is a link target as much as a number, so the whole card lifts on
   hover the way .portal-card does rather than only the link inside it. */
.stat-card {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.stat-sub {
    margin: 8px 0 0;
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   19a. Charts

   The SVG is drawn in a fixed 720x250 coordinate space and stretched to the
   panel width. Stroke widths are given in that space too, so they scale with
   it -- which is why the line looks the same weight on a phone as on a
   desktop rather than thinning out.
   -------------------------------------------------------------------------- */

.chart-wrap {
    width: 100%;
    margin: 4px 0 6px;
    overflow-x: auto;
}

.chart {
    display: block;
    width: 100%;
    height: clamp(200px, 30vw, 260px);
}

.chart-grid {
    stroke: var(--border);
    stroke-width: 1;
}

.chart-axis {
    fill: var(--text-muted);
    font-size: 12px;
    font-family: inherit;
}

.chart-line {
    fill: none;
    stroke: var(--brand);
    stroke-width: 2.6;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Deliberately faint. The area is there to give the line a body and a
   baseline, not to be read as a quantity of its own. */
.chart-area {
    fill: var(--brand-tint);
    stroke: none;
}

.chart-dot {
    fill: var(--surface);
    stroke: var(--brand);
    stroke-width: 2.2;
}

.chart-dot:hover {
    fill: var(--brand);
}

.chart-bar {
    fill: var(--brand);
    transition: fill var(--fast) var(--ease);
}

.chart-bar:hover {
    fill: var(--brand-strong);
}

/* --------------------------------------------------------------------------
   19b. Small screens

   The chart keeps its aspect ratio and the wrapper scrolls, rather than the
   chart squashing until its axis labels overlap.
   -------------------------------------------------------------------------- */

@media (max-width: 620px) {
    .chart {
        min-width: 460px;
    }

    .stat-value {
        font-size: 26px;
    }
}

/* --------------------------------------------------------------------------
   19c. Dark mode

   Only the dot, which is the one element painted with a surface colour rather
   than with a brand token.
   -------------------------------------------------------------------------- */

@media not all {   /* dark theme switched off -- every page is always light; see the note at the top of this file */
    .chart-dot {
        fill: var(--surface-raised);
    }
}

/* --------------------------------------------------------------------------
   19d. Print

   A dashboard is a thing people take into a meeting on paper. The charts must
   keep their fills, which browsers strip by default.
   -------------------------------------------------------------------------- */

@media print {
    .chart-wrap {
        overflow: visible;
        break-inside: avoid;
    }

    .chart {
        print-color-adjust: exact;
        -webkit-print-color-adjust: exact;
    }
}

/* --------------------------------------------------------------------------
   19d2. The customer's Home hero banner

   A full-bleed picture with the headline, a sentence about the service and the
   Book Now button sitting on top of it.

   WHY THIS IS NOT .home-hero

   Section 18c's .home-hero is a two-column split -- copy beside artwork -- and
   belongs to the PUBLIC pages, where it reads the .page-portal tokens. This is
   a signed-in component inside the app shell, and it is a different thing
   structurally: one layer, a picture behind it, text over the top. Giving it
   its own name keeps both readable rather than forcing one set of rules to
   mean two layouts.

   THE PICTURE COMES FROM THE DATABASE

   dashboards/home.php sets --home-banner-image inline to whatever the platform
   administrator uploaded on dashboards/site_banner.php. That is the ONLY thing
   the page passes in; everything about how the picture is framed lives here.
   The var()'s fallback is the gradient, so a site with no banner uploaded gets
   a finished-looking blue hero rather than an empty box, and the page needs no
   second code path for it.

   THE OVERLAY IS A PLAIN GRADIENT, NOT A FILTER

   backdrop-filter would blur the picture behind the text and is exactly the
   kind of effect that makes this app stutter on the machine it is demonstrated
   on, so the readability comes from a dark gradient painted over the picture
   in ::after. background-attachment is left at its default for the same
   reason: a fixed background repaints the image on every scroll frame.

   The gradient is heavier at the bottom, where the text is, and lighter at the
   top, so the picture is still visible as a picture rather than being flattened
   to a dark rectangle.
   -------------------------------------------------------------------------- */

.home-banner {
    position: relative;
    display: flex;
    align-items: flex-end;
    min-height: clamp(300px, 40vw, 440px);

    /* A PLAIN RECTANGLE, EDGE TO EDGE. This element is not inside
       .container -- dashboards/home.php hands it to header.php as $beforeMain,
       which places it between the top bar and the reading column. So it is a
       plain block filling its parent, and it reaches from the sidebar to the
       right of the window with no margin arithmetic at all.

       No bottom margin either: the container underneath already opens with
       40px of top padding, and adding to it would leave a gap twice the size
       of every other gap on the page.

       The radius and the shadow are gone with the inset: both existed to make
       the banner read as a card, and a rectangle that reaches the edges is not
       one. overflow stays, so the picture is still clipped to the box. */
    margin: 0;
    padding: clamp(26px, 5vw, 52px);
    overflow: hidden;
    border-radius: 0;

    /* The colour under everything: what shows while a large photograph is
       still downloading, and what the gradient sits on if it never arrives. */
    background-color: var(--brand-deep);
    background-image: var(--home-banner-image,
        linear-gradient(135deg, var(--brand-deep) 0%, var(--brand-strong) 52%, var(--brand) 100%));
    background-position: center;
    background-size: cover;
    background-repeat: no-repeat;

    color: #ffffff;
}

/* THE COPY STARTS WHERE THE PAGE STARTS.

   The banner is a full-bleed rectangle, and <main class="container"> under it
   is a 1200px column centred in the same space. A fixed side padding put the
   headline and the cards below it on two different left edges -- 52px against
   roughly 330px on a 1920px window. Rather than guess at the column's edge,
   this reproduces it:

       (100% - 1200px) / 2    how far in the centred column begins
       + 24px                 .container's own side padding
       max(24px, ...)         under 1200px the column fills the space and
                              only that 24px is left

   100% is the width of .app-main, the box BOTH of them are laid out in, so
   the two edges track each other at every window size instead of being two
   numbers somebody has to keep in step by hand.

   :not() keeps the rounded preview on dashboards/site_banner.php out of it.
   That one sits inside a .panel rather than across the page, so it has no
   reading column to line up with and keeps the padding it has today. */
.home-banner:not(.home-banner--preview) {
    padding-inline: max(24px, calc((100% - 1200px) / 2 + 24px));
}


/* The dark transparent overlay. Absolutely positioned over the picture and
   under the copy, which is why .home-banner-copy is given a stacking position
   of its own below rather than relying on source order. */
.home-banner::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
        180deg,
        rgba(15, 23, 42, .30) 0%,
        rgba(15, 23, 42, .58) 48%,
        rgba(15, 23, 42, .82) 100%);
}

.home-banner-copy {
    position: relative;   /* lifts the text above ::after */
    z-index: 1;
    max-width: 40ch;
}

/* The greeting. Small, spaced and set above the headline, so it reads as an
   introduction to it rather than as a second heading competing with it. */
.home-banner-eyebrow {
    margin: 0 0 10px;
    font-size: 13.5px;
    font-weight: 650;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, .90);
    text-shadow: 0 1px 10px rgba(15, 23, 42, .45);
}

/* Uppercase in the stylesheet rather than in the markup, so the heading is
   still ordinary words to a screen reader while reading as a banner on screen. */
.home-banner-title {
    margin: 0 0 14px;
    font-size: clamp(30px, 5.2vw, 52px);
    line-height: 1.06;
    font-weight: 800;
    letter-spacing: -.015em;
    text-transform: uppercase;
    color: #ffffff;

    /* A soft shadow rather than a heavier overlay: it buys the white text its
       contrast back on the brightest part of a photograph without darkening
       the whole picture to get there. */
    text-shadow: 0 2px 18px rgba(15, 23, 42, .45);
}

.home-banner-sub {
    margin: 0 0 18px;
    font-size: clamp(15px, 1.6vw, 17.5px);
    line-height: 1.62;

    /* Not pure white. The headline is the white; the paragraph sits a step
       back from it so the two are not competing. */
    color: rgba(255, 255, 255, .92);
    text-shadow: 0 1px 12px rgba(15, 23, 42, .40);
}

/* Where this customer's laundry actually is. A translucent pill rather than
   another paragraph, so the one live figure on the banner is legible as a
   status and not mistaken for more marketing copy. inline-block so it hugs
   its own text instead of ruling a line across the picture.

   A flat rgba fill, NOT backdrop-filter: blurring what is behind it would
   cost a repaint over a full-size photograph, which is exactly the kind of
   effect this app has to stay away from. */
.home-banner-status {
    display: inline-block;
    margin: 0 0 24px;
    padding: 9px 16px;
    font-size: 14.5px;
    line-height: 1.45;
    color: #ffffff;
    background: rgba(255, 255, 255, .15);
    border: 1px solid rgba(255, 255, 255, .30);
    border-radius: var(--radius-pill);
}

.home-banner-status strong {
    font-weight: 750;
    color: inherit;   /* white like the sentence; the global strong rule is dark ink */
}

/* --------------------------------------------------------------------------
   The header at the top of ONE SHOP's page

   dashboards/shop_view.php opens with this: the shop's picture, its mark and
   its name, above its details and its price list.

   IT IS NOT THE HOME HERO, and that is the point of it existing at all. An
   earlier version reused .home-banner, and a customer who had just signed in
   at a shop arrived on a screen that looked exactly like the Home page. So
   this is half the height, it sits INSIDE the reading column with the same
   rounded corners as a card, and it reads as a heading for the page under it
   rather than as a landing screen of its own.

   The picture is handed in as --shop-hero-image, the same way every other
   picture in this app reaches the stylesheet, and the dark gradient over it is
   part of the same background declaration so the white text can never be left
   sitting on a bare photograph.
   -------------------------------------------------------------------------- */
.shop-hero {
    display: flex;
    align-items: flex-end;
    min-height: clamp(140px, 20vw, 190px);
    margin-bottom: 22px;
    padding: clamp(18px, 3vw, 26px);
    overflow: hidden;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    box-shadow: var(--shadow-sm);

    background-color: var(--brand-deep);
    background-image: linear-gradient(to bottom,
                                      rgba(9, 17, 34, .58) 0%,
                                      rgba(9, 17, 34, .74) 60%,
                                      rgba(9, 17, 34, .86) 100%),
                      var(--shop-hero-image);
    background-size: cover, cover;
    background-position: center, center;
    background-repeat: no-repeat, no-repeat;
}

.shop-hero-copy {
    width: 100%;
}

.shop-hero .shop-card-head {
    margin-bottom: 0;
}

.shop-hero-title {
    margin: 0;
    font-size: clamp(22px, 3vw, 30px);
    font-weight: 800;
    line-height: 1.15;
    color: #fff;
    text-shadow: 0 2px 12px rgba(0, 0, 0, .5);
}

.shop-hero-facts {
    margin: 6px 0 0;
    font-size: 14.5px;
    line-height: 1.5;
    color: rgba(255, 255, 255, .92);
    text-shadow: 0 1px 4px rgba(0, 0, 0, .55);
}

/* The shop's mark, pinned to its photograph. The white ring is what separates
   it from a picture of any colour, exactly as on a .card--photo. */
.shop-hero .shop-logo {
    border-color: rgba(255, 255, 255, .5);
    box-shadow: 0 2px 12px rgba(0, 0, 0, .4);
}

/* A photo header prints as nothing useful — browsers drop background images —
   so on paper it becomes plain text on white, like the cards do. */
@media print {
    .shop-hero {
        background: #fff;
        border-color: #999;
        box-shadow: none;
        min-height: 0;
    }

    .shop-hero-title,
    .shop-hero-facts {
        color: #000;
        text-shadow: none;
    }
}

.home-banner .button-row {
    margin: 0;
}

/* The preview on dashboards/site_banner.php: the same component, shorter,
   so an administrator sees the real overlay over their real picture without
   the panel it sits in becoming a page of its own.

   It keeps the rounded, inset treatment the real banner just gave up. The
   bleed above is measured against .container's padding, and this one is not in
   a container -- it is inside a .panel, where the same negative margins would
   drag it out through the panel's edges.

   Selected as ".panel .home-banner--preview" rather than on its own so it
   outranks the plain .home-banner overrides in the media queries below, which
   would otherwise win simply by appearing later in the file. */
.panel .home-banner--preview {
    min-height: clamp(180px, 26vw, 260px);
    margin: 0 0 22px;
    border-radius: var(--radius);
    box-shadow: none;
}

.panel .home-banner--preview .home-banner-title {
    font-size: clamp(22px, 3.4vw, 34px);
}

@media (max-width: 720px) {
    .home-banner {
        min-height: 320px;
        padding: 26px 22px 30px;
    }

    .home-banner-copy {
        max-width: none;
    }

    /* Full width on a phone: the pill's own width would leave a ragged edge
       above two full-width buttons. */
    .home-banner-status {
        display: block;
        border-radius: var(--radius);
    }

    /* Stacked and full width on a phone, so both calls to action are
       comfortable targets rather than two small pills sharing one line at the
       bottom of a large picture. The row already wraps; this is what makes each
       button take the whole of its line rather than only as much as its label. */
    .home-banner .button-row {
        flex-direction: column;
        align-items: stretch;
    }

    .home-banner .button-row .btn {
        width: 100%;
    }

    /* The ghost is the smaller of the two everywhere else in the app, which is
       right beside a table row and wrong when it is one of two stacked bars.
       Only its box grows; the colours are untouched. */
    .home-banner .button-row .btn--ghost {
        padding: 11px 20px;
        font-size: 15px;
    }
}

/* .container narrows its own side padding to 16px here -- see the
   max-width: 480px block in section 12 -- so the banner follows it down and
   the two left edges stay on one line on a phone as well. */
@media (max-width: 480px) {
    .home-banner:not(.home-banner--preview) {
        padding-inline: 16px;
    }
}

/* Nothing is redefined for dark mode on purpose. The banner is white text on a
   darkened picture in both themes -- that is what the overlay is for -- and
   its fallback gradient is built from --brand tokens, which the dark blocks
   have already swapped by the time this rule is read. */
/* --------------------------------------------------------------------------
   19e. The customer's Home welcome block

   Lives here rather than in section 18 because it belongs to a SIGNED-IN page.
   Section 18's .home-hero is the public split hero; this is its quieter
   counterpart inside the app shell, built on .panel so it sits in the same
   rhythm as every other panel on the page.
   -------------------------------------------------------------------------- */

.home-welcome {
    display: grid;
    grid-template-columns: 1.2fr .8fr;
    align-items: center;
    gap: clamp(16px, 3vw, 36px);
}

.home-welcome-copy h2 {
    margin: 0 0 8px;
}

.home-welcome-copy p {
    margin: 0;
    color: var(--text-soft);
}

/* The artwork takes its strokes from currentColor, so setting the colour here
   is what makes it follow the signed-in palette instead of carrying the public
   orange across with it. */
.home-welcome-art {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--brand);
}

.home-welcome-art svg {
    width: 100%;
    max-width: 240px;
    height: auto;
}

@media (max-width: 720px) {
    .home-welcome {
        grid-template-columns: 1fr;
    }

    /* The picture leads on a phone, the way the public hero does. */
    .home-welcome-art {
        order: -1;
    }

    .home-welcome-art svg {
        max-width: 190px;
    }
}

/* ==========================================================================
   20. Phase 14 — ratings, payments, the address book and the log

   Scope
     Unlike sections 17 to 19, nothing here is scoped to a body class. Every
     component below appears on BOTH sides of the system — a star rating shows
     on the public shop cards and on a shop owner's own reviews panel, a
     payment badge shows on the customer's history and on the counter screen —
     so they are all built from the brand tokens rather than from a palette.
     That is what makes them come out orange on <body class="page-portal"> and
     teal on <body class="page-app"> with no second copy of anything.

   Reuse first
     .card, .panel, .table, .badge, .chip, .field, .btn, .meta-list and
     .section-head are all reused untouched. A class appears below only where
     there was genuinely nothing already doing the job.
   ========================================================================== */

/* --------------------------------------------------------------------------
   20a. Star ratings

   Drawn as five inline SVGs rather than as a text glyph or a background image,
   for the reason every other mark in this app is drawn inline: PALABAHAN has
   to run from a XAMPP folder with no network. A star that is a broken-image
   box is worse than no star at all.

   Half stars are deliberately not drawn — see rating_stars_html(). The row
   rounds, and the exact figure sits beside it in .rating-score.
   -------------------------------------------------------------------------- */

.rating {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-size: 14px;
    line-height: 1;
}

.stars {
    display: inline-flex;
    gap: 1px;
}

.star {
    width: 16px;
    height: 16px;
    /* The empty state is a filled star in a faint ink rather than an outlined
       one: at 16px an outline reads as noise, and the contrast between the two
       fills is what makes the score legible at a glance. */
    fill: var(--border-strong);
    flex: none;
}

.star--on {
    fill: var(--palabahan-yellow, #60a5fa);
}

.rating-score {
    font-weight: 700;
    color: var(--text);
}

.rating-count,
.rating-none-text {
    color: var(--text-muted);
    font-size: 13px;
}

/* An unrated shop keeps the same row height as a rated one, so a grid of cards
   does not jump about depending on which of them somebody has reviewed. */
.rating--none .star {
    fill: var(--border);
}

.card-rating {
    margin: -2px 0 10px;
}

/* The verified-permit dot, sitting against a shop's name on its card.

   The shop's own page carries the worded .badge; a card in a grid has no room
   for one, so it carries this instead. Only a VERIFIED permit is ever drawn --
   see shop_card_permit_dot_html() in includes/shop_images.php for why the
   other two states draw nothing.

   A BRIGHTER GREEN THAN --success, deliberately. A card's background is a
   photograph with a dark wash over it, and the ink-weight green that reads
   well as text on white disappears against that. This is the same green a step
   brighter, and the white ring is what separates it from a photograph of any
   colour -- the trick .shop-hero .shop-logo already uses for the shop's mark.

   vertical-align keeps it on the name's middle rather than its baseline, so it
   reads as a mark beside the name instead of a full stop after it. */
.permit-dot {
    display: inline-block;
    width: 9px;
    height: 9px;
    margin-left: 9px;
    vertical-align: middle;
    background: #22c55e;
    border-radius: 50%;
    box-shadow: 0 0 0 2px rgba(255, 255, 255, .92),
                0 1px 3px rgba(15, 23, 42, .45);
}

/* On the one card that is NOT drawn over a photograph -- a fresh installation
   that has lost the shipped backgrounds -- the ring would vanish into white,
   so there the dot keeps its own darker edge instead. */
.card:not(.card--photo) .permit-dot {
    box-shadow: 0 0 0 2px var(--surface),
                0 0 0 3px var(--success-line);
}

/* --------------------------------------------------------------------------
   20b. A section heading with an action beside it

   .section-head on its own is a full-width heading. This puts a link on the
   right of one without either of them needing their own margin fix.
   -------------------------------------------------------------------------- */

.section-head-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 8px 16px;

    /* The 16px that a bare .section-head puts below itself is carried by the
       ROW instead of by the heading, so a heading with a link beside it opens
       the same gap above its cards as a heading without one. It cannot stay on
       the heading: once the row wraps on a narrow screen, a margin there
       pushes the action link away from the title and leaves nothing below the
       row at all. */
    margin-bottom: 16px;
}

/* Inside a panel the heading is a .panel h2, which already carries its own
   18px and its rule, so those rows stay exactly as they were. */
.panel .section-head-row {
    margin-bottom: 0;
}

.section-head-row .section-head {
    margin-bottom: 0;
}

/* Home: whatever follows the How it works steps carried no top margin, so the
   two sat touching. The same 34px every other section on Home opens with.
   Since 2026-10-29 that is the On Its Way panel (the user swapped it with
   "Laundry shops you can book"); the shops row keeps the rule too, in case the
   order is ever swapped back. */
.home-steps + .section-head-row,
.home-steps + .panel {
    margin-top: 34px;
}

/* --------------------------------------------------------------------------
   20c. The star picker on the review form

   Radio buttons, not a JavaScript widget. The inputs are the real control and
   are only visually hidden, so the row is keyboard-operable and announces
   itself to a screen reader without a line of script — and it still submits
   with JavaScript switched off, which is the rule every interactive thing in
   this app follows.

   The stars are laid out in REVERSE order (5 first) so that a plain CSS
   sibling selector can light up every star to the left of the hovered one.
   There is no previous-sibling selector; there is a next-sibling selector, and
   reversing the row is what turns one into the other.
   -------------------------------------------------------------------------- */

.ratepick {
    display: inline-flex;
    flex-direction: row-reverse;
    justify-content: flex-end;
    gap: 2px;
}

.ratepick input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.ratepick label {
    display: inline-flex;
    padding: 3px;
    margin: 0;
    cursor: pointer;
    line-height: 0;
    border-radius: 6px;
    transition: transform var(--fast) var(--ease);
}

.ratepick label svg {
    width: 30px;
    height: 30px;
    fill: var(--border-strong);
    transition: fill var(--fast) var(--ease);
}

.ratepick label:hover {
    transform: scale(1.08);
}

/* The checked star and every star after it in the reversed row — which is
   every star to its LEFT on screen. */
.ratepick input:checked ~ label svg,
.ratepick label:hover svg,
.ratepick label:hover ~ label svg {
    fill: var(--palabahan-yellow, #60a5fa);
}

/* The focus ring has to be on the label, because the input it belongs to is
   the thing that is visually hidden. Without this the picker is invisible to
   anyone tabbing through the form. */
.ratepick input:focus-visible + label {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   20d. Donut chart

   For "one whole split into a few named parts" — sales per service, orders by
   outcome, payments by method. A line chart answers "how did this change over
   time" and a bar chart "which of these is biggest"; neither of them answers
   "what share was this", which is the only question a donut is good at.

   The ring is drawn with stroke-dasharray on circles, so there is no path
   arithmetic and no library. See chart_donut_svg() in includes/charts.php.
   -------------------------------------------------------------------------- */

.donut-wrap {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: clamp(14px, 3vw, 32px);
}

/* The Sales page's two rings (user request, 2026-10-27): the two panels share
   the row between them instead of leaving an empty third column, and each ring
   sits in the middle of its panel. auto-fit rather than auto-fill is what
   lets the two stretch. */
.sales-donuts {
    grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
}

/* Ring on top, centred; the legend beneath it keeps the full width so its
   long service names are not cut short. */
.sales-donuts .donut-wrap {
    flex-direction: column;
    align-items: center;
}

.sales-donuts .donut-legend {
    flex: none;
    width: 100%;
}

.donut {
    flex: none;
    width: clamp(150px, 34vw, 190px);
    height: auto;
    /* -90deg, so the first slice starts at twelve o'clock rather than at three.
       A chart that starts on the right reads as though a slice is missing. */
    transform: rotate(-90deg);
}

.donut-track {
    fill: none;
    stroke: var(--border);
}

.donut-slice {
    fill: none;
    stroke-linecap: butt;
    transition: opacity var(--fast) var(--ease);
}

.donut-slice:hover {
    opacity: .82;
}

/* The figure in the middle of the ring. Counter-rotated, because the <svg> it
   sits in was turned to move the starting angle. */
.donut-centre {
    transform: rotate(90deg);
    transform-origin: center;
}

.donut-centre-value {
    font-size: 15px;
    font-weight: 800;
    fill: var(--text);
}

.donut-centre-label {
    font-size: 7.5px;
    font-weight: 600;
    fill: var(--text-muted);
    letter-spacing: .04em;
    text-transform: uppercase;
}

.donut-legend {
    flex: 1 1 190px;
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 8px;
}

.donut-legend li {
    display: grid;
    grid-template-columns: 12px 1fr auto;
    align-items: center;
    gap: 10px;
    font-size: 14px;
}

.donut-key {
    width: 12px;
    height: 12px;
    border-radius: 3px;
}

.donut-legend-name {
    color: var(--text-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.donut-legend-value {
    font-weight: 700;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

.donut-legend-share {
    color: var(--text-muted);
    font-weight: 500;
}

/* --------------------------------------------------------------------------
   20e. Notification feed

   A list of things that happened, newest first. Not a .table: these are events
   with a sentence each rather than records with columns, and forcing them into
   a grid would waste most of the width on two short cells.
   -------------------------------------------------------------------------- */

.feed {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 2px;
}

.feed-item {
    display: grid;
    grid-template-columns: 38px 1fr;
    gap: 12px;
    padding: 14px 4px;
    border-bottom: 1px solid var(--border);
}

.feed-item:last-child {
    border-bottom: 0;
}

.feed-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background: #C9DCF9;   /* a step deeper, so the small box reads on its card */
    color: var(--brand-strong);
    flex: none;
}

.feed-icon .nav-icon {
    width: 19px;
    height: 19px;
}

/* An event that did not land — a failed SMS — takes the danger wash, so a page
   of green ticks does not hide the one line that needs attention. */
.feed-icon--failed {
    background: var(--danger-bg);
    color: var(--danger);
}

.feed-body p {
    margin: 0;
}

.feed-title {
    font-weight: 600;
    color: var(--text);
}

.feed-meta {
    margin-top: 3px;
    font-size: 13px;
    color: var(--text-muted);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 10px;
}

/* The shop's name, highlighted so each order's shop stands out from the date
   and price beside it (user request, 2026-10-30): Home's "On Its Way", the
   order list (includes/customer_orders_table.php) and the notification feed
   (includes/customer_feed.php). */
.feed-meta .feed-shop {
    padding: 2px 10px;
    font-weight: 700;
    color: var(--brand-deep);
    background: #C9DCF9;
    border-radius: var(--radius-pill);
}

/* The customer's Notifications page and the shop's Machine Alerts: each update sits in a box of its own
   instead of on a shared ruled list, so one message reads as one message. The
   box is a step lighter than the panel it sits on, the same way the typing
   boxes are a step darker, so it separates without a heavy shadow. */
.feed--boxed {
    gap: 10px;
}

.feed--boxed .feed-item,
.feed--boxed .feed-item:last-child {
    padding: 14px 16px;
    background: #F3F6FA;
    border: 1px solid #C9D4E3;
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-sm);
}

/* A box with one button at its right-hand edge -- My Orders' Track. A third
   column on the same grid, centred so the button sits level with the box. */
.feed-item--action {
    grid-template-columns: 38px 1fr auto;
    align-items: center;
}

/* --------------------------------------------------------------------------
   20f. Booking summary and money lines

   The list on the last step of the booking, and the same shape on the order
   and payment screens. A <dl> rather than a table: these are label/value
   pairs, not rows of a record.
   -------------------------------------------------------------------------- */

.sumline {
    margin: 0;
    display: grid;
    gap: 0;
}

.sumline > div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
    padding: 9px 0;
    border-bottom: 1px dashed var(--border);
}

.sumline dt {
    color: var(--text-soft);
}

.sumline dd {
    margin: 0;
    font-weight: 600;
    color: var(--text);
    font-variant-numeric: tabular-nums;
    text-align: right;
}

/* The total: a solid rule, no dashes, and the figure at heading weight, so the
   eye lands on the number the customer is actually agreeing to. */
.sumline > .sumline-total {
    margin-top: 4px;
    padding-top: 13px;
    border-bottom: 0;
    border-top: 2px solid var(--border-strong);
}

.sumline-total dt {
    font-weight: 700;
    color: var(--text);
}

.sumline-total dd {
    font-size: 21px;
    font-weight: 800;
    color: var(--brand-strong);
}

/* A line the customer still owes, so an unpaid balance is not the same colour
   as an amount already settled. */
.sumline-due dd {
    color: var(--danger);
}

/* --------------------------------------------------------------------------
   20g. Address book cards

   A .card with a default marker and a row of small actions. The marker is a
   .badge reused as-is; only the positioning is new.
   -------------------------------------------------------------------------- */

.addr-card {
    display: flex;
    flex-direction: column;
}

.addr-head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 6px;
}

.addr-head h3 {
    margin: 0;
}

.addr-lines {
    margin: 0 0 14px;
    color: var(--text-soft);
    line-height: 1.55;
}

/* Pushes the action row to the bottom, so a grid of address cards has its
   buttons on one line however long the streets are. */
.addr-card .row-actions {
    margin-top: auto;
}

/* --------------------------------------------------------------------------
   20h. Report toolbar and printing

   The export row on the reports screen, and the rules that make the printed
   sheet a report rather than a screenshot of a web page.
   -------------------------------------------------------------------------- */

.report-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-left: auto;
}

.report-head {
    display: none;
}

@media print {
    /* Everything that is navigation rather than report. */
    .sidebar,
    .topbar,
    .portal-header,
    .portal-foot,
    .report-actions,
    .chip-row,
    .toolbar,
    .backlink,
    .button-row,
    .row-actions {
        display: none !important;
    }

    /* The printed sheet needs its own title block, because the topbar that
       normally names the page has just been hidden. */
    .report-head {
        display: block;
        margin-bottom: 16px;
    }

    .app-shell,
    .app-main,
    .container {
        display: block;
        max-width: none;
        margin: 0;
        padding: 0;
    }

    .panel,
    .card {
        border: 1px solid #ccc;
        box-shadow: none;
        break-inside: avoid;
    }

    .table thead {
        display: table-header-group;
    }

    .donut,
    .chart {
        print-color-adjust: exact;
        -webkit-print-color-adjust: exact;
    }
}

/* --------------------------------------------------------------------------
   20i. Small screens
   -------------------------------------------------------------------------- */

@media (max-width: 620px) {
    .donut-wrap {
        justify-content: center;
    }

    .feed-item {
        grid-template-columns: 30px 1fr;
        gap: 10px;
    }

    .feed-item--action {
        grid-template-columns: 30px 1fr auto;
    }

    .feed-icon {
        width: 30px;
        height: 30px;
    }

    .feed-icon .nav-icon {
        width: 16px;
        height: 16px;
    }

    .ratepick label svg {
        width: 26px;
        height: 26px;
    }
}

/* --------------------------------------------------------------------------
   20j. Dark mode

   Only the places that paint with a surface colour rather than with a token
   that already flips. Everything else above follows the brand tokens and needs
   no override.
   -------------------------------------------------------------------------- */

@media not all {   /* dark theme switched off -- every page is always light; see the note at the top of this file */
    .star,
    .ratepick label svg {
        fill: rgba(255, 255, 255, .18);
    }

    .rating--none .star {
        fill: rgba(255, 255, 255, .12);
    }

    .donut-track {
        stroke: rgba(255, 255, 255, .07);
    }
}

/* ==========================================================================
   21. The signed-in app, in the PALABAHAN brand palette

   Scope
     <body class="page-app"> — every signed-in screen in the system. The
     customer's pages, the shop owner's, the shop administrator's and the
     platform administrator's all wear the PALABAHAN orange, so the product is
     one colour from the login page through to the reports.

     This landed in two steps: first the customer side alone, then the staff
     side at the user's request. It is now the whole app, which is why the
     modifier class it started as is gone — a modifier that is always on is
     just the base rule wearing a longer name.

   Why it exists
     The login page is orange, and everybody walks straight from it into the
     app. Landing on a teal screen one click after an orange one reads as two
     different products bolted together.

     The dark sidebar with orange highlights is also what the original brief
     asked for on the management side: "Professional, information-dense
     dashboard with dark sidebar, orange highlights, charts, tables".

   How it works, and what it deliberately does NOT do
     ONLY TOKENS ARE REDEFINED. Not one component, layout, spacing rule or
     class is touched — .btn, .card, .panel, .table, .badge, .chip, .field, the
     sidebar, the charts and the tracker are all reused exactly as they are,
     and simply follow the new values. That is what makes this a palette change
     rather than a redesign, and it is why the flow, the navigation and every
     page's structure are identical before and after.

     The base tokens in section 1 are NOT dead: only the brand, ground and
     sidebar tokens are overridden here. The ink, the surfaces, the status
     colours and the shadows still come from :root, exactly as section 17 does
     it for the public pages.

     The values below are COPIED from section 17, which is the login page's own
     palette, so the two cannot drift apart. If the brand orange ever changes,
     it changes in both places or there is a visible seam at the login door.
   ========================================================================== */

.page-app {
    /* PALABAHAN brand: orange #2563eb, yellow #60a5fa, dark #0f172a.
       Identical to section 17 — see the note above. */
    --brand:          #1d4ed8;   /* the blue at text/link weight, for contrast */
    --brand-strong:   #1e40af;
    --brand-deep:     #0f172a;
    --brand-bright:   #60a5fa;
    --brand-wash:     #dbeafe;
    --brand-mist:     #f0f7ff;
    --brand-tint:     rgba(37, 99, 235, .12);
    --brand-line:     rgba(37, 99, 235, .28);
    --brand-ring:     rgba(37, 99, 235, .24);

    --palabahan-orange: #2563eb;
    --palabahan-yellow: #60a5fa;
    --palabahan-dark:   #0f172a;
    --palabahan-gray:   #8B8B8B;

    /* The ice-blue ground the orange sits on, exactly as the login page uses
       it. A laundry is water and cold clean linen, so the page reads cool and
       the blue stays the accent on top of it. */
    --ice:            #D3D9E1;   /* the page ground: soft blue-grey, so white cards stand out */
    --ice-deep:       #D3D9E1;
    --ice-mist:       #E7ECF3;

    --bg:             var(--ice);
    --bg-deep:        var(--ice-deep);

    /* Kept a step darker than --ice, so a white card still has a visible edge
       against the ground. */
    --border:         #e2e8f0;
    --border-strong:  #cbd5e1;
    --border-input:   #cbd5e1;

    /* ----------------------------------------------------------------------
       The sidebar.

       Navy, the same #0f172a as the public header and the footer (the
       user asked for it not to be white, 2026-10-18), with the blue as the
       active row -- the reference design's own dark navigation panel with
       blue highlights. Rules inside the sidebar that read --text get white
       from the .sidebar override just after this block.

       The text tokens stay light and low-saturation on purpose: a blue
       sidebar with blue text would lose the contrast that makes the menu
       readable, so the colour is spent on the ACTIVE row alone, where it is
       carrying meaning.
       ---------------------------------------------------------------------- */
    --sidebar-bg:       #1A2539;   /* a touch lighter than the header navy (user asked "medyo light", then "dark kaunti") */
    --sidebar-bg-soft:  #283750;
    --sidebar-text:     #cbd5e1;
    --sidebar-text-dim: #94a3b8;
    --sidebar-line:     rgba(255, 255, 255, .10);
    --sidebar-active:   #2563eb;

    /* The wordmark droplet, in the brand blue. */
    --mark: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.2c4.4 5.3 6.8 8.6 6.8 11.6a6.8 6.8 0 0 1-13.6 0c0-3 2.4-6.3 6.8-11.6z' fill='%232563eb'/%3E%3Ccircle cx='9.4' cy='14.6' r='1.9' fill='%23ffffff' opacity='.9'/%3E%3C/svg%3E");
}

/* The sidebar is navy, so the ink its rules borrow from --text (the wordmark,
   the hovered row, the open group, the signed-in name) is white in here. */
.page-app .sidebar { --text: #ffffff; }

/* The top bar too: navy like the sidebar, the public header and the footer
   (the user asked for it, 2026-10-18). Opaque, so the frosted blur it used to
   carry is dropped -- it had nothing left to show through, and blur is the
   effect this project avoids for speed. Its controls (search box, Branch
   pill, bell, account button) read the tokens below, so only colours move. */
.page-app .topbar {
    --text:           #ffffff;
    --text-soft:      #cbd5e1;
    --text-muted:     #94a3b8;
    --text-faint:     #94a3b8;
    --surface:        rgba(255, 255, 255, .06);
    --surface-sunken: rgba(255, 255, 255, .03);
    --border:         rgba(255, 255, 255, .10);
    --border-strong:  rgba(255, 255, 255, .16);
    --border-input:   rgba(255, 255, 255, .16);
    --brand:          #60a5fa;
    --brand-strong:   #93c5fd;
    --brand-wash:     rgba(96, 165, 250, .14);

    background: #0f172a;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}

/* The Branch and account dropdowns open below the bar onto the light page,
   so they take the light tokens back. */
.page-app .topbar .branchpick-menu,
.page-app .topbar .usermenu-menu {
    --text:           #0f172a;
    --text-soft:      #334155;
    --text-muted:     #64748b;
    --text-faint:     #94a3b8;
    --surface:        #E7ECF3;
    --surface-sunken: #E7ECF3;
    --border:         #e2e8f0;
    --border-strong:  #cbd5e1;
    --border-input:   #cbd5e1;
    --brand:          #1d4ed8;
    --brand-strong:   #1e40af;
    --brand-wash:     #dbeafe;
}

/* The admin, shop owner and shop admin pages: their boxes (cards, panels,
   inputs) are a step darker white than the customer side's #E7ECF3 (user
   request, 2026-10-18). Still lighter than the #D3D9E1 page around them. */
.page-app--staff .app-main {
    --surface:        #DCE2EA;
    --surface-raised: #DCE2EA;
    --surface-sunken: #DCE2EA;
}

/* The account menu (the round button at the top right) opens in the light
   blue of the tables and machine cards (user request, 2026-10-18). Its words
   are set dark here, because inside the navy bar they would otherwise
   inherit the bar's pale ink -- which is why the name was hard to read. */
.page-app .topbar .usermenu-menu {
    color: var(--text);
    background: #E3EDFC;
    border-color: #BCD3F5;
}

.page-app .topbar .usermenu-menu .btn--ghost {
    background: #C9DCF9;
    border-color: #8FB3EC;
}

/* Primary buttons take the full-strength orange, the same rule section 17
   applies to the login page. .btn itself is untouched — only the fill it
   paints with. */
.page-app .btn {
    background: linear-gradient(180deg, #3b82f6 0%, var(--palabahan-orange) 100%);
    border-color: #1d4ed8;
}

.page-app .btn:hover {
    background: linear-gradient(180deg, var(--palabahan-orange) 0%, #1d4ed8 100%);
}

/* --------------------------------------------------------------------------
   The two button MODIFIERS have to be restated, and this is not decoration.

   ".page-app .btn" above is a two-class selector, so it outranks the
   single-class ".btn--ghost" and ".btn--danger" rules in section 8. Without
   the blocks below, an outline button and a DELETE button inside the app shell
   would both be painted with the orange gradient — a destructive action would
   come out looking exactly like "Book Now".

   So these restate the base appearance rather than inventing a new one. They
   are the same declarations section 8 already makes; all they do is win the
   specificity fight that the rule above would otherwise take from them.
   -------------------------------------------------------------------------- */

/* .btn--ghost stays an outline button; it only needs the orange edge. */
.page-app .btn--ghost {
    background: var(--surface);
    border-color: var(--brand-line);
    color: var(--brand-strong);
}

.page-app .btn--ghost:hover {
    background: var(--brand-mist);
    border-color: var(--palabahan-orange);
}

/* The danger button keeps its red OUTLINE, exactly as section 8 draws it. A
   destructive action must not be repainted in the brand colour just because
   the page around it changed: "Cancel This Booking" and "Suspend Account" have
   to stay visibly different from "Book Now". */
.page-app .btn--danger {
    color: var(--danger);
    background: var(--surface);
    border-color: var(--danger-line);
}

.page-app .btn--danger:hover {
    color: var(--on-brand);
    background: var(--danger);
    border-color: var(--danger);
}

/* --------------------------------------------------------------------------
   21a. Dark mode

   Only the brand tokens are lightened, exactly as section 17i does for the
   login page. The surfaces, ink and shadows keep coming from section 15, so
   the signed-in pages follow the same dark theme as the rest of the app rather
   than staying stubbornly cream.
   -------------------------------------------------------------------------- */

@media not all {   /* dark theme switched off -- every page is always light; see the note at the top of this file */
    .page-app {
        --brand:          #60a5fa;
        --brand-strong:   #93c5fd;
        --brand-deep:     #bfdbfe;
        --brand-bright:   #60a5fa;
        --brand-wash:     #16243d;
        --brand-mist:     #131f33;
        --brand-tint:     rgba(37, 99, 235, .18);
        --brand-line:     rgba(37, 99, 235, .34);
        --brand-ring:     rgba(37, 99, 235, .28);

        /* Ice blue after dark is a deep slate, not a warm brown — the same
           values the login page uses, so the door still has no seam in it. */
        --ice:            #0f1a2b;
        --ice-deep:       #080f1c;
        --ice-mist:       #16233a;

        --bg:             var(--ice);
        --bg-deep:        var(--ice-deep);
        --border:         #24314a;
        --border-strong:  #33425f;
        --border-input:   #33425f;

        /* Deeper than the light theme, so the sidebar stays distinct from the
           page behind it instead of merging with it. */
        --sidebar-bg:       #070d18;
        --sidebar-bg-soft:  #16203a;
        --sidebar-text:     #dfe6f2;
        --sidebar-text-dim: #93a2ba;
        --sidebar-line:     rgba(255, 255, 255, .10);
        --sidebar-active:   #2563eb;
    }
}

/* ==========================================================================
   22. Customer feedback

   Two screens share these: dashboards/my_feedback.php, where a customer writes
   and reads their own, and dashboards/feedback.php, where staff answer.

   Reuse first
     .panel, .feed, .feed-item, .feed-icon, .feed-meta, .table, .chip, .badge,
     .field and .btn are all reused untouched. What was missing was somewhere
     to put a paragraph of somebody's own words, and somewhere to put the reply
     underneath it — a message is not a table cell, and a reply is not another
     message.
   ========================================================================== */

/* --------------------------------------------------------------------------
   22a. The message itself

   pre-wrap, because the customer typed it. Line breaks they put in are theirs
   and carry meaning — a list of three things that went wrong should not be
   reflowed into one paragraph. The PHP escapes the text and converts newlines
   for the feed, and this keeps the spacing where it was in the textarea.
   -------------------------------------------------------------------------- */

.fb-message {
    margin: 10px 0 0;
    white-space: pre-wrap;
    /* Long words and pasted URLs wrap instead of pushing the panel wider. */
    overflow-wrap: anywhere;
    color: var(--text-soft);
    line-height: 1.6;
}

/* --------------------------------------------------------------------------
   22b. The shop's reply

   Indented behind a brand-coloured rule rather than boxed in a card of its
   own. A reply is part of the same conversation, so it should read as attached
   to the message above it — a separate card would suggest a separate thing.
   -------------------------------------------------------------------------- */

.fb-reply {
    margin: 14px 0 0;
    padding: 12px 0 4px 16px;
    border-left: 3px solid var(--brand);
    background: none;
}

.fb-reply p {
    margin: 0;
    color: var(--text-soft);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    line-height: 1.6;
}

.fb-reply-head {
    margin: 0 0 6px !important;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--brand-strong) !important;
    white-space: normal !important;
}

/* The timestamp beside the heading sits on its own line under it, the way
   .cell-sub does everywhere else in the app. */
.fb-reply-head .cell-sub {
    font-weight: 500;
}

/* --------------------------------------------------------------------------
   22c. A feed entry that carries a reply

   .feed-item is a two-column grid (icon, body) built for one-line events. A
   feedback entry is a paragraph plus a reply, so it needs room to breathe;
   nothing else about the grid changes.
   -------------------------------------------------------------------------- */

.fb-entry {
    padding-top: 18px;
    padding-bottom: 18px;
    align-items: start;
}

/* --------------------------------------------------------------------------
   22c-2. Comments on a public post

   Each comment is the reply's own indented rule, in the softer line colour so
   the shop's answer still stands out as the official one. The thread sits
   under a thin divider; the box to add one is the ordinary textarea.
   -------------------------------------------------------------------------- */

.fb-thread {
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}

.fb-thread-head {
    margin: 0 0 4px !important;
    font-size: 13px;
    font-weight: 700;
    color: var(--text-muted);
}

.fb-comment {
    border-left-color: var(--border-strong);
}

.fb-comment .fb-reply-head {
    color: var(--text) !important;
}

.fb-comment-remove {
    margin-top: 6px;
}

.fb-comment-form {
    margin-top: 12px;
}

.fb-comment-form .field textarea {
    min-height: 60px;
}

/* --------------------------------------------------------------------------
   22d. The message box on the form

   textarea is the one form control the app had not used until now, so it
   inherits nothing from .field. These are the same values .field input carries,
   plus the two a multi-line box needs: a resize handle that cannot break the
   layout sideways, and a readable line height.
   -------------------------------------------------------------------------- */

.field textarea {
    width: 100%;
    padding: 11px 13px;
    font: inherit;
    line-height: 1.55;
    color: var(--text);
    background: #D2DAE5;              /* same darker white as the other typing boxes */
    border: 1px solid #A3B3C8;
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    /* Vertical only: a textarea a reader can drag wider than its panel is a
       textarea that can push the page sideways. */
    resize: vertical;
    min-height: 96px;
}

.field textarea:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-ring);
}

.field textarea::placeholder {
    color: var(--text-faint);
}

/* ==========================================================================
   22. FreshCycle login  (login.php only)
   ==========================================================================

   Everything here is scoped under .page-fc or prefixed .fc-, so this section
   cannot reach any other page. login.php is the ONLY file that sets the
   page-fc body class; the portal, the branded shop logins and register.php
   are untouched and keep the styles above.

   The palette is declared on .page-fc rather than :root for the same reason:
   these are this page's colours, and letting them into :root would re-tint
   every component in the app that reads the shared tokens.

   NO NETWORK. This app has to run from a XAMPP folder with no internet, so
   there is no @import for a webfont and no icon CDN -- Inter and Poppins are
   asked for in case they are installed locally and the stack falls through to
   the system UI font when they are not. The icons are inline SVG in login.php.
   -------------------------------------------------------------------------- */

.page-fc {
    --fc-primary:       #07549A;
    --fc-primary-hover: #06447D;
    --fc-accent:        #0788E8;
    --fc-accent-light:  #DDF1FF;
    --fc-background:    #D3D9E1;   /* soft blue-grey, so the white cards stand out (was #F5F9FF, #E3ECF7, #D8E2EF, then blue-grey #C8D5E5) */
    --fc-surface:       #E7ECF3;
    --fc-text:          #123B63;
    --fc-text-2:        #647B98;
    --fc-muted:         #94A3B8;
    --fc-border:        #DCE8F5;
    --fc-success:       #16A34A;
    --fc-danger:        #DC2626;

    --fc-ink:   #06345C;   /* the wave, darker than --fc-primary */
    --fc-ring:  rgba(7, 136, 232, .18);

    /* ALWAYS LIGHT. These pages paint a light ground of their own, so the
       app-wide tokens are pinned back to their light values here -- otherwise
       a computer set to dark mode hands every reused component (the shop
       cards, the section heading, the footer, the back link) pale dark-theme
       text on this pale page, and the words all but disappear. */
    color-scheme: light;
    --brand:          #2563eb;
    --brand-strong:   #1d4ed8;
    --brand-mist:     #eff6ff;
    --brand-wash:     #dbeafe;
    --brand-tint:     rgba(37, 99, 235, .10);
    --brand-line:     rgba(37, 99, 235, .22);
    --on-brand:       #ffffff;   /* dark mode makes this navy: dark words on the blue buttons */
    --text:           #0f172a;
    --text-soft:      #334155;
    --text-muted:     #64748b;
    --text-faint:     #94a3b8;
    --surface:        #E7ECF3;
    --surface-raised: #E7ECF3;
    --surface-sunken: #E7ECF3;
    --border:         #e2e8f0;
    --border-strong:  #cbd5e1;
    --border-input:   #cbd5e1;

    /* The footer paints itself from these, and they are otherwise only
       defined on .page-portal -- without them the Welcome page and the login
       card got a see-through footer with near-white words on a white page. */
    --palabahan-orange: #2563eb;
    --palabahan-yellow: #60a5fa;
    --palabahan-dark:   #0f172a;
    --palabahan-gray:   #8B8B8B;

    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--fc-background);
    color: var(--fc-text);
    font-family: "Inter", "Poppins", system-ui, -apple-system, "Segoe UI",
                 Roboto, "Helvetica Neue", Arial, sans-serif;
}

/* The shared header and footer stay in normal flow; the card takes the space
   between them and centres in whatever is left. */
.page-fc .fc-wrap {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(18px, 3.4vw, 46px) clamp(16px, 3vw, 36px);
}

.fc-card {
    width: 100%;
    max-width: 1140px;
    min-height: 680px;
    display: grid;
    grid-template-columns: 1fr 1fr;      /* the 50/50 split */
    background: var(--fc-surface);
    border-radius: 20px;
    overflow: hidden;                     /* clips the photo to the corners */
    box-shadow:
        0 1px 2px rgba(18, 59, 99, .04),
        0 12px 28px rgba(18, 59, 99, .07),
        0 36px 70px rgba(18, 59, 99, .08);

    /* Clearer text inside the card. The page-wide greys were too pale on
       white: --fc-text-2 sat at about 4.3:1 and --fc-muted at about 2.6:1, so
       the subtitle, the placeholders, the "or" and the field icons all looked
       faded. These darker values pass 4.5:1 and up. Scoped to the card so the
       header and the shop list keep the colours they already have. */
    --fc-text-2: #45607F;
    --fc-muted:  #687A92;
    --fc-border: #B9C9DD;   /* darker edges, so the inputs still show on the dark-white card */
}

/* --------------------------------------------------------------------------
   22a. Left: the photograph
   -------------------------------------------------------------------------- */

.fc-brandside {
    position: relative;
    isolation: isolate;
    display: flex;
    flex-direction: column;
    justify-content: center;
    overflow: hidden;
    color: #fff;
    /* A small static shadow so white words stay sharp wherever the photo
       shows through brighter -- painted once, no per-frame cost. */
    text-shadow: 0 1px 3px rgba(6, 52, 92, .45);
}

.fc-photo {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;        /* fills the panel at any card height */
    z-index: -2;
}

/* The blue overlay. Two stops rather than a flat wash, so the towels and the
   machine still read through the top of the panel while the lower half goes
   dark enough to carry the wave into. */
.fc-tint {
    position: absolute;
    inset: 0;
    z-index: -1;
    background:
        linear-gradient(162deg,
            rgba(7, 84, 154, .90) 0%,
            rgba(7, 136, 232, .78) 42%,
            rgba(6, 52, 92, .94) 100%);
}

.fc-brandinner {
    padding: clamp(34px, 4vw, 56px);
    padding-bottom: clamp(140px, 17vw, 190px);   /* clears the wave */
}

.fc-mark {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-bottom: clamp(26px, 4vw, 44px);
}

.fc-mark-badge {
    display: grid;
    place-items: center;
    width: 54px;
    height: 54px;
    flex: none;
    border-radius: 16px;
    background: rgba(255, 255, 255, .16);
    border: 1px solid rgba(255, 255, 255, .28);
}

.fc-i--mark { width: 29px; height: 29px; }

.fc-mark-text { display: block; line-height: 1.15; }

.fc-mark-name {
    display: block;
    font-size: 1.6rem;
    font-weight: 700;
    letter-spacing: -.015em;
}

.fc-mark-sub {
    display: block;
    margin-top: 3px;
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .20em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, .80);
}

.fc-headline {
    margin: 0 0 14px;
    font-size: clamp(1.9rem, 3.1vw, 2.65rem);
    font-weight: 700;
    line-height: 1.16;
    letter-spacing: -.02em;
}

.fc-sub {
    margin: 0 0 clamp(26px, 4vw, 40px);
    max-width: 30rem;
    font-size: 1rem;
    line-height: 1.6;
    color: rgba(255, 255, 255, .96);
}

.fc-benefits {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 18px 16px;
    max-width: 30rem;
}

.fc-benefit {
    display: flex;
    align-items: center;
    gap: 12px;
}

.fc-benefit-dot {
    display: grid;
    place-items: center;
    width: 46px;
    height: 46px;
    flex: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, .17);
    border: 1px solid rgba(255, 255, 255, .26);
}

.fc-benefit-dot .fc-i { width: 21px; height: 21px; }

.fc-benefit-label {
    font-size: .92rem;
    font-weight: 600;
    line-height: 1.3;
    color: #fff;
}

/* ---- The wave and the slogan ------------------------------------------- */

.fc-wave {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 0;
    pointer-events: none;
}

.fc-wave svg {
    display: block;
    width: 100%;
    height: clamp(96px, 13vw, 132px);
    fill: var(--fc-ink);
}

.fc-slogan {
    position: absolute;
    left: 0;
    right: 0;
    bottom: clamp(16px, 2.4vw, 26px);
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    font-family: "Segoe Script", "Brush Script MT", "Snell Roundhand", cursive;
    font-size: clamp(1.05rem, 1.7vw, 1.32rem);
    font-weight: 600;
    color: #fff;
}

.fc-i--heart {
    width: 18px;
    height: 18px;
    fill: #FF7A8A;
    stroke: #FF7A8A;
}

/* --------------------------------------------------------------------------
   22b. Right: the form
   -------------------------------------------------------------------------- */

.fc-formside {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--fc-surface);
    padding: clamp(30px, 3.6vw, 56px) clamp(22px, 3.4vw, 54px);
}

.fc-form-inner {
    width: 100%;
    max-width: 418px;
}

.fc-title {
    margin: 0 0 6px;
    font-size: clamp(1.7rem, 2.5vw, 2.1rem);
    font-weight: 700;
    letter-spacing: -.02em;
    color: var(--fc-text);
}

.fc-title-sub {
    margin: 0 0 26px;
    font-size: 1rem;
    color: var(--fc-text-2);
}

.fc-shopline {
    margin: -14px 0 22px;
    font-size: .95rem;
    color: var(--fc-text-2);
}

.fc-shopline strong { color: var(--fc-primary); }

/* ---- Alerts ------------------------------------------------------------- */

.fc-alert {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 0 0 18px;
    padding: 13px 15px;
    border-radius: 14px;
    font-size: .9rem;
    line-height: 1.5;
    border: 1px solid transparent;
}

.fc-alert .fc-i { flex: none; margin-top: 1px; }

.fc-alert--bad {
    background: #FEF2F2;
    border-color: #FBD5D5;
    color: #99231F;
}

.fc-alert--ok {
    background: #F0FDF4;
    border-color: #BBF7D0;
    color: #14663A;
}

/* ---- Role selector ------------------------------------------------------ */

.fc-seg {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
    margin-bottom: 22px;
    padding: 6px;
    background: var(--fc-accent-light);
    border-radius: 16px;
}

.fc-seg-opt {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    height: 52px;
    border-radius: 11px;
    font-size: .95rem;
    font-weight: 600;
    color: var(--fc-text-2);
    cursor: pointer;
    user-select: none;
    transition: background .18s ease, color .18s ease, box-shadow .18s ease;
}

.fc-seg-opt:hover { color: var(--fc-primary); }

.fc-seg-in:checked + .fc-seg-opt {
    background: var(--fc-surface);
    color: var(--fc-primary);
    box-shadow: 0 1px 2px rgba(18, 59, 99, .06), 0 4px 12px rgba(18, 59, 99, .09);
}

/* The radios are visually hidden, so the focus ring has to be drawn on the
   label instead or a keyboard user cannot see where they are. */
.fc-seg-in:focus-visible + .fc-seg-opt {
    outline: 3px solid var(--fc-ring);
    outline-offset: 1px;
}

/* ---- Fields ------------------------------------------------------------- */

.fc-field { margin-bottom: 17px; }

.fc-label {
    display: block;
    margin-bottom: 7px;
    font-size: .875rem;
    font-weight: 600;
    color: var(--fc-text);
}

.fc-input {
    position: relative;
    display: flex;
    align-items: center;
    height: 66px;
    padding: 0 14px 0 48px;
    background: #D2DAE5;              /* darker white than the card, like the app's typing boxes */
    border: 1px solid var(--fc-border);
    border-radius: 14px;
    transition: border-color .18s ease, box-shadow .18s ease;
}

.fc-input:hover { border-color: #C3DAF0; }

.fc-input:focus-within {
    border-color: var(--fc-accent);
    box-shadow: 0 0 0 4px var(--fc-ring);
}

.fc-input.is-bad {
    border-color: var(--fc-danger);
    box-shadow: 0 0 0 4px rgba(220, 38, 38, .12);
}

.fc-input input {
    width: 100%;
    height: 100%;
    border: 0;
    background: none;
    font: inherit;
    font-size: .97rem;
    color: var(--fc-text);
}

.fc-input input:focus { outline: none; }

.fc-input input::placeholder { color: var(--fc-muted); }

.fc-i--lead {
    position: absolute;
    left: 16px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--fc-muted);
    pointer-events: none;
}

.fc-input:focus-within .fc-i--lead { color: var(--fc-accent); }

/* Added by the script, so it only exists where it actually works. */
.fc-peek {
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    flex: none;
    margin-right: -6px;
    padding: 0;
    border: 0;
    border-radius: 10px;
    background: none;
    color: var(--fc-muted);
    cursor: pointer;
    transition: color .15s ease, background .15s ease;
}

.fc-peek:hover { color: var(--fc-primary); background: var(--fc-accent-light); }

.fc-peek:focus-visible {
    outline: 3px solid var(--fc-ring);
    outline-offset: 1px;
}

.fc-err {
    margin: 7px 0 0;
    font-size: .86rem;
    font-weight: 500;
    color: var(--fc-danger);
}

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

.fc-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    height: 62px;
    border: 1px solid transparent;
    border-radius: 14px;
    font: inherit;
    font-size: 1rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    transition: background .18s ease, border-color .18s ease,
                color .18s ease, box-shadow .18s ease, transform .12s ease;
}

.fc-btn:focus-visible {
    outline: 3px solid var(--fc-ring);
    outline-offset: 2px;
}

.fc-btn--go {
    margin-top: 4px;
    background: var(--fc-accent);
    color: #fff;
    box-shadow: 0 1px 2px rgba(7, 84, 154, .14), 0 8px 20px rgba(7, 136, 232, .26);
}

.fc-btn--go:hover {
    background: var(--fc-primary-hover);
    box-shadow: 0 1px 2px rgba(7, 84, 154, .18), 0 10px 24px rgba(7, 84, 154, .30);
}

.fc-btn--go:active { transform: translateY(1px); }

.fc-i--go { transition: transform .18s ease; }

.fc-btn--go:hover .fc-i--go { transform: translateX(3px); }

.fc-btn--ghost {
    background: var(--fc-surface);
    border-color: var(--fc-border);
    color: var(--fc-primary);
}

.fc-btn--ghost:hover {
    background: var(--fc-accent-light);
    border-color: var(--fc-accent);
}

/* ---- Submitting --------------------------------------------------------- */

.fc-spin {
    display: none;
    width: 19px;
    height: 19px;
    border: 2.5px solid rgba(255, 255, 255, .42);
    border-top-color: #fff;
    border-radius: 50%;
    animation: fc-spin .7s linear infinite;
}

@keyframes fc-spin { to { transform: rotate(360deg); } }

.fc-form.is-sending .fc-btn--go {
    cursor: progress;
    background: var(--fc-primary-hover);
}

.fc-form.is-sending .fc-i--go { display: none; }
.fc-form.is-sending .fc-spin  { display: block; }

/* ---- Forgot / divider / footnote ---------------------------------------- */

.fc-forgot {
    margin: 16px 0 0;
    text-align: center;
    font-size: .9rem;
}

.fc-forgot a {
    color: var(--fc-primary);
    font-weight: 600;
    text-decoration: none;
    border-bottom: 1px solid transparent;
}

.fc-forgot a:hover {
    color: var(--fc-primary-hover);
    border-bottom-color: currentColor;
}

/* The way back to the shop list on a shop's own login card. */
.fc-back {
    margin: 0 0 18px;
    text-align: left;
}

/* ---- Choose Your Laundry Shop, in the login card's colours ---------------
   The shop list -- and the other public pages (track, about, pricing,
   services, legal) -- carry .page-portal (the layout) AND .page-fc (the flat
   ground, the font and the palette above), so the second screen reads as
   the same UI as the Welcome page and the login card on either side of it.
   Only colours change here; the cards and the grid are the portal's own. */
.page-fc--list::before { display: none; }   /* no veil: there is no photo to hold back */

.page-fc--list .portal-hero h1        { color: var(--fc-text); }
.page-fc--list .portal-hero .subtitle { color: var(--fc-text-2); }
.page-fc--list .portal-hero h1::after { background: var(--fc-accent); }

.page-fc--list .btn {
    background: var(--fc-accent);
    border-color: var(--fc-accent);
    color: #fff;
}

.page-fc--list .btn:hover {
    background: var(--fc-primary-hover);
    border-color: var(--fc-primary-hover);
}

.page-fc--list .backlink a { color: var(--fc-primary); }

/* ---- Sign Up and the Platform Admin login, in the login card's colours ---
   register.php and admin/login.php -- and since the always-light change also
   customer/login.php, forgot_password.php and verify_mobile.php -- keep
   their own .auth-card form and
   .page-auth--framed layout, and add .page-fc (the flat light ground, the
   font, the always-light tokens and the navy header) plus this modifier, so
   pressing Sign Up -- or opening the admin door -- lands on the same colours
   as the login card instead of the old photograph, which a computer in dark
   mode painted navy. Only colours change; the forms are untouched. */
.page-fc--auth::before { display: none; }   /* no veil: there is no photo to hold back */

.page-fc--auth .auth-card {
    background: var(--fc-surface);
    border-color: var(--fc-border);
    color: var(--fc-text);
}

/* The admin Sign In button is the same blue as the header's Sign Up button
   (the user asked for it), with the same darker blue on hover. */
.page-fc--auth .auth-card--admin .btn {
    background: var(--fc-accent);
    border-color: var(--fc-accent);
    color: #fff;
}

.page-fc--auth .auth-card--admin .btn:hover {
    background: var(--fc-primary-hover);
    border-color: var(--fc-primary-hover);
}

/* The "For platform administrators..." note wears the header's colours: the
   same navy ground, pale slate words, white for the role names and the
   light blue accent on the icon and the left rule. */
.page-fc--auth .auth-card--admin .auth-note {
    background: var(--palabahan-dark);
    border-color: var(--palabahan-dark);
    border-left-color: var(--palabahan-yellow);
    color: #cbd5e1;
}

.page-fc--auth .auth-card--admin .auth-note strong { color: #fff; }

.page-fc--auth .auth-card--admin .auth-note svg {
    stroke: var(--palabahan-yellow);
    opacity: 1;
}

/* ---- The public header, in the footer's colours -------------------------
   On every page-fc page -- Welcome, Choose Your Laundry Shop and the login
   card, plus Sign Up and the Platform Admin login (see .page-fc--auth) --
   so the header is the same on all of them. The user asked for the
   header to wear the footer's colours, so it takes the same dark ground
   (--palabahan-dark), the same pale slate words (#cbd5e1) and the same light
   blue accent (--palabahan-yellow, which is blue despite its name) as
   .portal-foot, framing the light page between two matching bars. Two
   classes deep, so these win over both the plain rules and the dark-theme
   ones. */

.page-fc .portal-header {
    background: var(--palabahan-dark);
    border-bottom: 1px solid rgba(255, 255, 255, .08);
    box-shadow: 0 1px 2px rgba(15, 23, 42, .10), 0 6px 18px rgba(15, 23, 42, .12);
}

.page-fc .portal-mark-name { color: #fff; }
.page-fc .portal-mark-sub { color: #cbd5e1; }

.page-fc .portal-nav-link { color: #cbd5e1; }

.page-fc .portal-nav-link:hover,
.page-fc .portal-nav-link.is-current {
    background: rgba(96, 165, 250, .14);
    color: var(--palabahan-yellow);
}

.page-fc .portal-nav,
.page-fc .portal-nav-actions { border-color: rgba(255, 255, 255, .12); }

.page-fc .portal-nav-actions .btn {
    background: var(--fc-accent);
    border-color: var(--fc-accent);
    color: #fff;
}

.page-fc .portal-nav-actions .btn:hover {
    background: var(--fc-primary-hover);
    border-color: var(--fc-primary-hover);
}

.page-fc .portal-help {
    background: rgba(255, 255, 255, .06);
    border-color: rgba(255, 255, 255, .12);
}

.page-fc .portal-help svg { stroke: var(--palabahan-yellow); }
.page-fc .portal-help-key { color: #cbd5e1; }
.page-fc .portal-help-num { color: #fff; }

.page-fc .portal-navtoggle {
    background: rgba(255, 255, 255, .06);
    border-color: rgba(255, 255, 255, .16);
}

.page-fc .portal-navtoggle svg { stroke: #fff; }

.fc-or {
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 22px 0;
    color: var(--fc-muted);
    font-size: .9rem;
}

.fc-or::before,
.fc-or::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--fc-border);
}

/* Customer side only. The script sets [hidden]; this does the same with no
   script in any browser that understands :has(), and in one that does not the
   button simply stays visible -- which is the harmless answer, since
   register.php turns away anyone who is not signing up as a customer. */
.fc-form-inner:has(#fc-as-admin:checked) #fc-register {
    display: none;
}

.fc-footnote {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 9px;
    margin-top: 30px;
    color: var(--fc-muted);
}

.fc-footnote p {
    margin: 0;
    font-family: "Segoe Script", "Brush Script MT", "Snell Roundhand", cursive;
    font-size: 1rem;
    color: var(--fc-text-2);
}

.fc-i--foot { width: 26px; height: 26px; color: var(--fc-accent); }

/* ---- The icons ---------------------------------------------------------- */

.fc-i {
    width: 20px;
    height: 20px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* --------------------------------------------------------------------------
   22c. Tablet and phone
   -------------------------------------------------------------------------- */

/* Tablet: the photo panel gives up some width to the form, which needs a
   fixed amount of room to stay comfortable while the picture can take any
   share of what is left. */
@media (max-width: 1080px) {
    .fc-card {
        grid-template-columns: 44fr 56fr;
        min-height: 620px;
    }

    .fc-benefits { gap: 14px 12px; }

    .fc-benefit-dot { width: 42px; height: 42px; }

    .fc-benefit-label { font-size: .86rem; }
}

/* Phone: one column, branding above the form. The photo panel keeps a short
   band rather than disappearing -- it is what tells the visitor whose login
   this is -- but the benefits grid and the wave go, because at this width
   they push the form itself below the fold for no gain. */
@media (max-width: 860px) {
    .page-fc .fc-wrap { padding: 14px; }

    .fc-card {
        grid-template-columns: 1fr;
        min-height: 0;
        border-radius: 18px;
    }

    .fc-brandside { min-height: 232px; justify-content: flex-end; }

    .fc-brandinner {
        padding: 26px 22px 28px;
    }

    .fc-mark { margin-bottom: 18px; }

    .fc-mark-badge { width: 46px; height: 46px; border-radius: 14px; }

    .fc-i--mark { width: 25px; height: 25px; }

    .fc-mark-name { font-size: 1.35rem; }

    .fc-headline { font-size: 1.6rem; }

    .fc-headline br { display: none; }

    .fc-sub {
        margin-bottom: 0;
        font-size: .92rem;
    }

    .fc-benefits,
    .fc-wave { display: none; }

    .fc-formside { padding: 28px 20px 32px; }

    .fc-form-inner { max-width: none; }

    .fc-footnote { justify-content: center; margin-top: 24px; }
}

/* Small phones: the controls stay full-width and tall enough to tap, which is
   the one thing that must not shrink here. */
@media (max-width: 420px) {
    .fc-input  { height: 60px; padding-left: 44px; }
    .fc-i--lead { left: 14px; }
    .fc-btn    { height: 58px; }
    .fc-seg-opt { height: 48px; font-size: .9rem; }
    .fc-title  { font-size: 1.55rem; }
}

/* Honour a reduced-motion preference: the spinner is the only thing that
   still moves, because it is the feedback itself. */
@media (prefers-reduced-motion: reduce) {
    .page-fc .fc-btn,
    .page-fc .fc-seg-opt,
    .page-fc .fc-input,
    .page-fc .fc-i--go,
    .page-fc .fc-peek {
        transition: none;
    }

    .fc-btn--go:hover .fc-i--go { transform: none; }
}

/* The front page's machine boards (index.php): each board is one link to its
   shop's login, so it must still read as a panel, not as underlined link text.
   The outline on hover is the only sign it can be pressed -- a static ring,
   nothing that composites per frame. */
.board-link {
    display: block;
    color: inherit;
    text-decoration: none;
    border-radius: var(--radius);
}

.board-link:hover .panel,
.board-link:focus-visible .panel {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   The shop footer (includes/shop_footer.php)

   One band naming the shop a page belongs to: its logo and name, where it is,
   how to call it, when it is open, and its most-used pages. Built from the
   shop card's own pieces -- .shop-card-head, .shop-logo, .shop-facts -- so it
   reads as the same shop the visitor picked from the list. Lined up with
   .container so its left edge matches the page above it.
   -------------------------------------------------------------------------- */
.shop-foot {
    background: var(--surface);
    border-top: 1px solid var(--border);
}

.shop-foot-inner {
    max-width: 1200px;
    margin: 0 auto;
    padding: 28px 24px 24px;
}

.shop-foot .shop-card-title .badge {
    margin-top: 6px;
}

.shop-foot-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 10px 28px;
    margin-bottom: 16px;
}

.shop-foot-facts .badge {
    margin-left: 6px;
    vertical-align: middle;
}

.shop-foot-facts a,
.shop-foot-links a,
.shop-foot-platform a {
    color: var(--brand-strong);
    font-weight: 600;
    text-decoration: none;
}

.shop-foot-facts a:hover,
.shop-foot-links a:hover,
.shop-foot-platform a:hover {
    text-decoration: underline;
}

.shop-foot-facts li > span > a {
    margin-left: 6px;
    white-space: nowrap;
}

.shop-foot-links,
.shop-foot-platform {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 12px;
    margin: 0;
    font-size: 14px;
}

/* The shop's own links run one per line, on every screen (user request,
   2026-10-29), so the dots that separated them in a row are not drawn. */
.shop-foot-links {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
}

.shop-foot-links .sep {
    display: none;
}

.shop-foot-platform {
    margin-top: 16px;
    padding-top: 14px;
    border-top: 1px solid var(--border);
    font-size: 13px;
    color: var(--text-muted);
}

.shop-foot .sep {
    color: var(--text-muted);
}

/* The picture across the top of a service's box (includes/service_cards.php
   and My Services): the shop-cover box, pulled out to the card's edges so the
   picture runs edge to edge under the card's own rounded corners, with the
   text underneath it. A service with no picture keeps the same strip, empty,
   with the picture icon -- every box in a row stays the same shape. */
.service-grid .shop-cover--service {
    height: 170px;
    margin: -26px -24px 18px;
    border: 0;
    border-bottom: 1px solid #BCD3F5;
    border-radius: 0;
}

.service-grid > .card .btn--block {
    margin-top: auto;
}

/* Home's "Laundry shops you can book" on a phone: one row of shop cards that
   is swiped sideways instead of a tall stack (user request, 2026-10-29). Each
   card takes most of the width so the next one peeks in at the edge -- the
   only hint needed that there is more to swipe to -- and scroll-snap settles
   the row on a whole card. Plain native scrolling: nothing composites per
   frame. A computer keeps the ordinary grid.

   A phone is recognised by its touch screen as well as by its width: a phone
   browser set to "Desktop site" reports a computer-sized width, but still has
   no mouse to hover with and a finger for a pointer, so it gets the swipe too
   (the user's phone kept showing the stacked grid, 2026-10-29).

   The row runs out to the screen's own edges, across the page's side gutter,
   and each card settles in the CENTRE (user report, 2026-11-06: on the middle
   shop one side was blank while the other showed a chopped-off sliver of the
   next card, because the row stopped at the gutter). Now the middle card has
   an equal peek of its neighbours on both sides; the first and last cards
   still sit on the gutter, because the row cannot scroll past its ends.
   The bleed equals .container's side padding: 24px, or 16px at 480px. */
@media (max-width: 720px), (hover: none) and (pointer: coarse) {
    .card-grid--swipe {
        --swipe-gutter: 24px;
        grid-template-columns: none;
        grid-auto-flow: column;
        /* No wider than a card on a computer (about 340px in the three-column
           grid there), so a phone on "Desktop site" does not blow each card up
           to most of a 980px page (user request, 2026-11-06: same size and
           width as on the computer). A narrow phone still gets 84%. */
        grid-auto-columns: min(84%, 340px);
        margin-inline: calc(var(--swipe-gutter) * -1);
        padding: 0 var(--swipe-gutter) 8px;
        scroll-padding-inline: var(--swipe-gutter);
        overflow-x: auto;
        overscroll-behavior-x: contain;
        scroll-snap-type: x mandatory;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
    }

    .card-grid--swipe::-webkit-scrollbar {
        display: none;
    }

    .card-grid--swipe > * {
        scroll-snap-align: center;
    }
}

@media (max-width: 480px) {
    .card-grid--swipe {
        --swipe-gutter: 16px;
    }

    /* The card's details keep the computer's label-beside-value layout
       instead of the phone's usual label-above-value stack, so a shop card
       reads the same as on the computer (user request, 2026-11-06). */
    .card-grid--swipe .meta-list {
        grid-template-columns: max-content 1fr;
        gap: 12px 18px;
    }

    .card-grid--swipe .meta-list dd + dt {
        margin-top: 0;
    }
}

/* The machine boards of the front page (index.php) and the customer dashboard
   ($fleetSwipe) on a phone: the same sideways
   swipe, but FOUR machines to a swipe, two over two (user request,
   2026-10-29; the user picked 2x2 over four narrow cards in one row). The
   grid has two rows and flows down each column, so every column holds two
   cards and is half the row wide; only the first card of every second column
   is a snap point -- with scroll-snap-stop so one flick moves exactly four
   machines. The board is a newspaper-column block on a computer, so it is
   turned back into a grid here, and the cards are tightened to fit. */
/* A machine holding a load gets a page to itself, full width, and the idle
   machines beside it move to the next swipe (user request, 2026-10-30): in a
   half-width column its clock and tracker ran far longer than its neighbour
   and left an empty gap. The pages are built in includes/machine_fleet.php;
   each is one swipe, so one flick still moves exactly one page. */
@media (max-width: 720px), (hover: none) and (pointer: coarse) {
    .machine-grid.card-grid--quad {
        display: grid;
        column-width: auto;
        gap: 10px;
        grid-auto-columns: 100%;
        align-items: start;
        /* machine-fleet.js sizes the row to the page on screen; a taller page
           off to the side must not turn that into a scroll box of its own. */
        overflow-y: hidden;
    }

    .card-grid--quad > .fleet-page {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px;
        align-items: start;
        scroll-snap-stop: always;
    }

    .card-grid--quad > .fleet-page--solo {
        grid-template-columns: minmax(0, 1fr);
    }

    .card-grid--quad .fleet-page > .machine {
        min-width: 0;
        margin-bottom: 0;
        padding: 14px 12px;
        gap: 10px;
        font-size: 0.85rem;
    }

    /* Full width, so the clock can sit side by side again. */
    .card-grid--quad .fleet-page--solo > .machine {
        padding: 18px 16px;
        font-size: 0.9rem;
    }

    .card-grid--quad .machine-head {
        flex-wrap: wrap;
        gap: 6px;
    }

    .card-grid--quad .machine-clock {
        flex-direction: column;
        gap: 4px;
        padding: 8px 10px;
    }

    .card-grid--quad .fleet-page--solo .machine-clock {
        flex-direction: row;
        gap: 10px;
        padding: 10px 14px;
    }
}

/* A shop's own note above its services, e.g. "How to sort your laundry" and
   the capacity of a load (user request, 2026-11-05). See service_note_html(). */
.service-note {
    margin: 0 0 18px;
    padding: 14px 18px;
    background: var(--brand-mist);
    border: 1px solid var(--brand-line);
    border-left: 4px solid var(--brand);
    border-radius: var(--radius-sm);
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--text);
}

.service-note p {
    margin: 0 0 4px;
    font-weight: 700;
}

.service-note ul {
    margin: 2px 0 8px;
    padding-left: 20px;
}

.service-note li {
    margin: 2px 0;
}

.service-note > :last-child {
    margin-bottom: 0;
}

/* ==========================================================================
   SIMPLE ANIMATIONS (user request, 2026-11-05: "a simple animation, not all")
   --------------------------------------------------------------------------
   Only three, and all of them once, when a page opens:
     1. the panels of a page rise in, one after another (first five only);
     2. the cards in a grid (services, shops, machines) rise in the same way;
     3. a flash message slides down into place.
   Opacity and transform only -- nothing that repaints while scrolling, no
   blur, no looping (see the no-heavy-effects rule). The hover lift on .card
   and the press on .btn were already there and are unchanged. Anyone who
   has asked their system for less motion gets none of this.
   ========================================================================== */
@keyframes plb-rise {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}

@keyframes plb-drop {
    from { opacity: 0; transform: translateY(-8px); }
    to   { opacity: 1; transform: none; }
}

main.container > .panel {
    animation: plb-rise .4s var(--ease, ease-out) backwards;
}
main.container > .panel:nth-of-type(2) { animation-delay: .06s; }
main.container > .panel:nth-of-type(3) { animation-delay: .12s; }
main.container > .panel:nth-of-type(4) { animation-delay: .18s; }
main.container > .panel:nth-of-type(n+5) { animation: none; }

.card-grid > .card,
.card-grid > .fleet-page {
    animation: plb-rise .35s var(--ease, ease-out) backwards;
}
.card-grid > :nth-child(2) { animation-delay: .04s; }
.card-grid > :nth-child(3) { animation-delay: .08s; }
.card-grid > :nth-child(4) { animation-delay: .12s; }
.card-grid > :nth-child(5) { animation-delay: .16s; }
.card-grid > :nth-child(6) { animation-delay: .20s; }
.card-grid > :nth-child(n+7) { animation-delay: .24s; }

.alert {
    animation: plb-drop .3s var(--ease, ease-out) backwards;
}

/* A page brought up to date by assets/js/live-refresh.js (user request,
   2026-11-11) does not replay the page-open rise -- new data just appears. */
.live-swap main.container > .panel,
.live-swap .card-grid > .card,
.live-swap .card-grid > .fleet-page,
.live-swap main.container .alert {
    animation: none;
}

@media (prefers-reduced-motion: reduce) {
    main.container > .panel,
    .card-grid > .card,
    .card-grid > .fleet-page,
    .alert {
        animation: none;
    }
}

/* Scroll animation (user request, 2026-11-05): what starts below the screen
   rises in once as it scrolls into view. assets/js/scroll-reveal.js adds
   .reveal, then .is-in, and takes both off again when done. Opacity and
   transform only. Without the script nothing is hidden. */
.reveal {
    opacity: 0;
    transform: translateY(16px);
    animation: none !important;
}
.reveal.is-in {
    opacity: 1;
    transform: none;
    transition: opacity .5s ease, transform .5s cubic-bezier(.32, .72, .32, 1);
}
@media print {
    .reveal, .reveal.is-in { opacity: 1 !important; transform: none !important; }
}


/* --- Hide-on-scroll header (2026-11-05) -----------------------------------
   assets/js/header-autohide.js adds .hdr-autohide to the page header and
   toggles .hdr-hidden: scrolling down slides the header up out of view,
   scrolling up brings it back. Transform only, so it costs no layout.
   Without the script none of this applies and the header is unchanged. */
.hdr-autohide {
    position: sticky;
    top: 0;
    z-index: 30;
    /* Coming back: a little longer, with a soft landing (user request,
       2026-11-06: "mas smooth"). */
    transition: transform .38s cubic-bezier(.22, 1, .36, 1);
    will-change: transform;
}

.hdr-autohide.hdr-hidden {
    /* the extra 12px tucks the drop shadow away with it */
    transform: translate3d(0, calc(-100% - 12px), 0);
    /* Going away: eases out of sight rather than snapping. */
    transition: transform .32s cubic-bezier(.4, 0, .6, 1);
}

@media (prefers-reduced-motion: reduce) {
    .hdr-autohide,
    .hdr-autohide.hdr-hidden { transition: none; }
}

/* Picture of a payment's reference (2026-11-06): the "or add a picture" line
   under the Reference box, and the small thumbnail in the payment history. */
.proof-label {
    margin-top: 8px;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-muted);
}
.proof-thumb {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-top: 4px;
    font-size: 13px;
}
.proof-thumb img {
    width: 56px;
    height: 56px;
    object-fit: cover;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
}
/* The picture chosen in Record a Payment, with its Remove button. */
.proof-chosen {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 8px;
}
.proof-chosen[hidden] {
    display: none;
}
.proof-chosen img {
    width: 56px;
    height: 56px;
    object-fit: cover;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
}
/* A payment's reference picture opened on the page, over a plain dark
   backdrop (no blur -- see the no-heavy-effects rule). */
.proof-viewer {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    background: rgba(15, 23, 42, .78);
}
.proof-viewer[hidden] {
    display: none;
}
.proof-viewer-box {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
    max-width: min(960px, 100%);
    max-height: 100%;
}
.proof-viewer-box img {
    display: block;
    max-width: 100%;
    max-height: calc(100vh - 100px);
    object-fit: contain;
    border-radius: 8px;
    background: #fff;
}
.proof-viewer-close {
    min-width: 96px;
}
html.proof-viewer-open {
    overflow: hidden;
}
/* The Quantity box on the booking and walk-in forms: a number of a few digits,
   so it no longer stretches the full width once Date and Slot were removed
   (user request, 2026-11-06). */
.field input.qty-input {
    width: 100%;
    max-width: 160px;
}

/* ==========================================================================
   Online payment (user request, 2026-11-11): the shop's GCash / Maya / bank
   boxes (Shop Profile and the customer's order page), the QR picture, and the
   Confirm / Not Accepted row on a customer's screenshot. Reuses .card, .feed
   and .proof-thumb; only sizes and spacing are new.
   ========================================================================== */
.card-grid--pay {
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    align-items: start;
    margin: 0 0 18px;
}
.pay-account .section-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-top: 0;
}
.pay-account-name {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 10px;
    font-size: 16px;
}
.pay-method-logo {
    flex: 0 0 auto;
    width: 26px;
    height: 22px;
}
/* Maya's mint wordmark is made for a black ground, as on their app icon. */
.pay-method-logo--maya {
    box-sizing: border-box;
    width: 60px;
    padding: 5px 7px 3px;
    border-radius: 6px;
    background: #000;
}
.pay-qr {
    display: block;
    width: 160px;
    max-width: 100%;
    height: auto;
    margin: 0 0 10px;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    background: #fff;
}
.pay-verify .feed-body {
    min-width: 0;
}
.pay-verify-actions {
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-top: 8px;
}
.pay-reject {
    display: flex;
    flex: 1 1 340px;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}
.pay-reject input[type="text"] {
    flex: 1 1 200px;
    min-width: 0;
    padding: 7px 12px;
    font: inherit;
    font-size: 13px;
    color: var(--text);
    background: #fff;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
}
.pay-verify-notice .status-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin: 0;
}
/* A feed box led by a payment screenshot instead of the round icon: the first
   column is as wide as the 56px thumbnail, so the picture never covers the text. */
.feed-item--thumb {
    grid-template-columns: 56px 1fr;
    align-items: start;
}
.feed-item--thumb .proof-thumb {
    margin-top: 0;
}
.feed-item--thumb .proof-thumb span {
    display: none;
}
/* "Remove the QR code" sits inside a .field, whose input rule would stretch the
   box; keep it a small tick beside its words, like the other .chip--check. */
.pay-account .chip--check {
    display: inline-flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
}
.pay-account .chip--check input[type="checkbox"] {
    width: auto;
    height: auto;
    margin: 0;
    padding: 0;
}
