/* Customer App showcase — additive layer for the end-customer mobile screen.
   Kept OUT of app.css (which is locked). Uses the app's design tokens so it
   stays on-brand and follows any future theming (the app is light-only; no
   OS prefers-color-scheme override here, to match the rest of the page). */

/* ===========================================================================
   Reference design language.

   The customer screens follow laundry/admin/branch_access_codes.php. That page
   does NOT load app.css - _layout.php:239 is skipped because the page passes
   'include_app_css' => false - it loads laundry/public/assets/auth/preview.css,
   the same stylesheet as the sign-in screen. So the look being matched here is
   preview.css's, and its literal values are reproduced below.

   preview.css and admin-shell.css cannot simply be linked from this page:
   preview.css carries unscoped global rules (* , html, body, table) and a :root
   block redeclaring --ink/--line/--shadow, which would silently repaint every
   app.css rule; admin-shell.css scopes everything to .admin-shell-image-a.

   app.css is entirely token-driven (app.css:1447-1509), so re-pointing its base
   tokens on .customer-page repaints every card, border, button and shadow on
   the customer surface at once, without editing the locked file and without
   touching the admin pages. Values from preview.css:17-32.
   =========================================================================== */

.customer-page {
    --color-brand-primary: #55eca4;      /* preview.css:24 --mint */
    --color-brand-primary-hover: #3fd991;
    --color-brand-primary-soft: #e4fbef;
    --color-mint: #55eca4;
    --color-mint-soft: #e4fbef;
    --color-ink: #111820;                /* preview.css:19 --ink */
    --color-text-secondary: #68717d;     /* preview.css:20 --muted */
    --color-text-muted: #697079;
    --color-border: #dfe4e8;             /* preview.css:21 --line */
    --color-background: #f7f9fa;
    --color-success: #137b51;            /* preview.css:25 --mint-dark */
    --color-success-soft: #e4fbef;
    --radius-medium: 14px;               /* preview.css:596 .preview-panel */
    --shadow-card: 0 12px 34px rgba(22, 35, 48, 0.08);   /* preview.css:30 */
    --shadow-hover: 0 12px 34px rgba(22, 35, 48, 0.08);  /* one shadow only */
    --success: #137b51;

    /* Flat canvas. app.css:4765 lays a teal-to-blue gradient over --bg; the
       reference page is a single #f7f9fa field (preview.css:382-387). */
    background: #f7f9fa;
}

/* preview.css:213 - the small uppercase label above a heading. app.css:4989
   paints it with --brand, which would now be mint on white and too pale.
   Scoped to the customer surface so the admin definition is untouched. */
.customer-page .eyebrow {
    color: #137b51;
    font-size: 0.688rem;
    font-weight: 700;
    letter-spacing: 0.16em;
}

/* The customer's own points, in the reference's .preview-panel shape
   (preview.css:594-599) with the .summary-card number treatment
   (preview.css:1040-1049). Revealed by JS once the balance arrives. */
.customer-points {
    display: grid;
    gap: 4px;
    padding: 18px 20px;
    border: 1px solid #dfe4e8;
    border-radius: 14px;
    background: #ffffff;
    box-shadow: 0 12px 34px rgba(22, 35, 48, 0.08);
}

.customer-points[hidden] {
    display: none;
}

.customer-points-value {
    margin: 0;
    color: #111820;
    line-height: 1.1;
}

.customer-points-value strong {
    font-size: 2.125rem;
    font-weight: 900;
    letter-spacing: -0.03em;
    color: #137b51;
}

.customer-points-value span {
    margin-left: 6px;
    font-size: 0.875rem;
    font-weight: 700;
    color: #68717d;
}

.customer-points-note {
    margin: 2px 0 0;
    font-size: 0.75rem;
    color: #68717d;
}

.customer-points-note:empty {
    display: none;
}

/* ---------------------------------------------------------------------------
   The journey bar.

   The steps in screen order, on every screen, replacing both the old
   three-item .journey-steps list inside the dark panel and the per-card
   <span>1</span> numbers that disagreed with it.

   Laid out as equal columns rather than a flex row so every step keeps the same
   width whatever the label length - Thai "ใช้แต้มส่วนลด" and English
   "Redeem points" are very different, and a row that reflows between languages
   looks broken rather than translated.

   The column count is NOT written down. It used to say repeat(4, 1fr), from
   when there was a "เลือกเครื่อง" step; that step went away on 2026-07-23
   because the QR now names the machine, and the fourth column stayed behind -
   empty, on the right, pushing the three real steps off centre on every screen
   a customer sees. Letting the columns come from the number of <li> means the
   bar cannot drift again the next time a step is added or removed.
   --------------------------------------------------------------------------- */

.customer-steps {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 0;
    margin: 0 0 16px;
    padding: 0;
    list-style: none;
}

.customer-step {
    position: relative;
    display: grid;
    justify-items: center;
    gap: 5px;
    padding: 0 2px;
    text-align: center;
}

/* The connector runs BEHIND the numbers, from the middle of one to the middle
   of the next, so it never pokes out past the first or last step. */
.customer-step::before {
    content: "";
    position: absolute;
    top: 13px;
    right: 50%;
    left: -50%;
    height: 2px;
    background: #dfe4e8;
}

.customer-step:first-child::before {
    display: none;
}

.customer-step.is-done::before,
.customer-step.is-current::before {
    background: #55eca4;
}

.customer-step-no {
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: 2px solid #dfe4e8;
    background: #ffffff;
    color: #8b94a0;
    font-size: 0.75rem;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.customer-step-label {
    font-size: 0.625rem;
    font-weight: 600;
    line-height: 1.25;
    color: #8b94a0;
}

.customer-step.is-done .customer-step-no {
    border-color: #55eca4;
    background: #55eca4;
    color: #0a121b;
}

.customer-step.is-current .customer-step-no {
    border-color: #137b51;
    background: #137b51;
    color: #ffffff;
}

.customer-step.is-done .customer-step-label {
    color: #68717d;
}

.customer-step.is-current .customer-step-label {
    color: #111820;
    font-weight: 800;
}

/* ---------------------------------------------------------------------------
   Spending points as a discount.

   Same .preview-panel shape as .customer-points above (white card, hairline,
   14px radius, one shadow) so the two points surfaces read as one feature.
   Revealed by JS only once the server has quoted a spendable amount.
   --------------------------------------------------------------------------- */

.customer-redeem {
    display: grid;
    gap: 0;
    margin: 18px 0;
    padding: 20px;
    border: 1px solid #dfe4e8;
    border-radius: 14px;
    background: #ffffff;
    box-shadow: 0 12px 34px rgba(22, 35, 48, 0.08);
}

.customer-redeem[hidden] {
    display: none;
}

/* app.css:4821 gives this `display: grid`, which beats the `hidden` attribute -
   the UA rule for [hidden] is only `display: none` at zero specificity. Without
   this the picker stayed on screen behind the points card after the order was
   already created. */
.customer-page .customer-journey-shell[hidden] {
    display: none;
}

.customer-redeem-title {
    margin: 0 0 2px;
    font-size: 1.25rem;
    font-weight: 800;
    letter-spacing: -0.03em;
    color: #111820;
}

.customer-redeem-order {
    margin: 0;
    font-size: 0.75rem;
    color: #68717d;
}

/* The balance, in the .summary-card number treatment the points card already
   uses (preview.css:1040-1049), on the soft-mint panel that marks it as the
   customer's own money rather than the shop's price. */
.customer-redeem-balance {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin: 14px 0 0;
    padding: 12px 14px;
    border: 1px solid #b7f0d3;
    border-radius: 10px;
    background: #e4fbef;
}

.customer-redeem-balance-value strong {
    font-size: 1.75rem;
    font-weight: 900;
    letter-spacing: -0.03em;
    color: #137b51;
    font-variant-numeric: tabular-nums;
}

.customer-redeem-balance-value span {
    margin-left: 5px;
    font-size: 0.813rem;
    font-weight: 700;
    color: #68717d;
}

.customer-redeem-rate {
    font-size: 0.688rem;
    font-weight: 700;
    color: #137b51;
}

.customer-redeem-slider {
    margin: 16px 0 0;
}

/* app.css:1596 sets `input, select, textarea { min-height: 42px; padding: 9px 11px }`.
   min-height CLAMPS height whatever the specificity, so setting height alone
   left the track 42px tall with 11px of dead padding either side - the thumb
   could not reach the ends of its own rail. Both have to be reset here, not
   just overridden. */
.customer-redeem-slider input[type="range"] {
    display: block;
    width: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    /* The whole control is the target on a phone, so it is given a real height
       rather than the 4px the platform default paints. */
    height: 26px;
    min-height: 26px;
    accent-color: #55eca4;
    background: transparent;
}

.customer-redeem-ends {
    display: flex;
    justify-content: space-between;
    font-size: 0.688rem;
    color: #68717d;
    font-variant-numeric: tabular-nums;
}

.customer-redeem-use {
    margin: 10px 0 0;
    text-align: center;
    font-size: 0.875rem;
    font-weight: 700;
    color: #137b51;
    font-variant-numeric: tabular-nums;
}

.customer-redeem-total {
    display: grid;
    gap: 6px;
    margin: 14px 0 0;
    padding-top: 12px;
    border-top: 1px solid #dfe4e8;
    font-variant-numeric: tabular-nums;
}

.customer-redeem-total div {
    display: flex;
    justify-content: space-between;
    gap: 12px;
}

.customer-redeem-total dt,
.customer-redeem-total dd {
    margin: 0;
    font-size: 0.813rem;
    color: #68717d;
}

.customer-redeem-total .is-off {
    color: #137b51;
    font-weight: 700;
}

.customer-redeem-total .is-due {
    margin-top: 4px;
    padding-top: 10px;
    border-top: 1px solid #dfe4e8;
}

.customer-redeem-total .is-due dt,
.customer-redeem-total .is-due dd {
    font-size: 0.938rem;
    font-weight: 800;
    color: #111820;
}

.customer-redeem-actions {
    display: grid;
    gap: 8px;
    margin-top: 16px;
}

/* .pay-button is already restyled for this surface further down the file, so
   the primary action here inherits it and needs no colour of its own. Only the
   disabled state is new: nothing else on the customer page has one.

   Deliberately over-specified at (0,4,1). `.customer-page .pay-button` further
   down this file is only (0,2,0), so on paper a shorter selector would win -
   but this is the same family of trap as app.css:5716, where a rule that looks
   weaker keeps repainting the button, and the house fix in admin-shell.css is
   to out-specify it rather than argue with it. The element type and :disabled
   are here to buy specificity as much as to select. */
.customer-page .customer-redeem-actions button.pay-button:disabled {
    border-color: #dfe4e8;
    background: #f0f3f4;
    color: #8b94a0;
    box-shadow: none;
    cursor: not-allowed;
}

.customer-redeem-done {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 16px;
    padding: 13px 14px;
    border: 1px solid #b7f0d3;
    border-radius: 10px;
    background: #e4fbef;
}

.customer-redeem-done[hidden] {
    display: none;
}

.customer-redeem-tick {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: #137b51;
    color: #ffffff;
    font-size: 0.813rem;
    font-weight: 800;
    line-height: 1;
}

.customer-redeem-done-text {
    display: grid;
    gap: 2px;
    min-width: 0;
}

.customer-redeem-done-text strong {
    font-size: 0.813rem;
    font-weight: 700;
    color: #111820;
}

.customer-redeem-done-text span {
    font-size: 0.75rem;
    color: #68717d;
}

/* Points can be spent on an order once, so once they have been the controls do
   not come back - the card becomes a statement of what happened. */
.customer-redeem.is-done .customer-redeem-balance,
.customer-redeem.is-done .customer-redeem-slider,
.customer-redeem.is-done .customer-redeem-use,
.customer-redeem.is-done .customer-redeem-actions {
    display: none;
}

/* ---------------------------------------------------------------------------
   Program rows.

   The row used to be one bold name over "4 pulses". It now carries a tone icon,
   the temperature in words, and who the program is for. The tone colours are
   SEMANTIC and deliberately outside the mint brand palette: mint means "this is
   the OOO product", and a temperature is information, not branding. Cool to hot
   reads blue -> amber -> red, which is the one colour metaphor a customer needs
   no legend for. The word is always present next to the icon, so the meaning
   never rests on colour alone.
   --------------------------------------------------------------------------- */

.customer-page .program-content {
    align-items: flex-start;
    gap: 13px;
}

.customer-page .program-icon {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    border-radius: 11px;
    border: 1px solid currentColor;
    /* The border and the glyph share currentColor; only the wash is set per
       tone below, so a new tone needs two values instead of three. */
    background: #eef2f5;
    color: #5a6b7a;
}

.customer-page .program-icon-svg {
    width: 22px;
    height: 22px;
    display: block;
}

.customer-page .program-icon.is-cold     { color: #1c6fb8; background: #e9f3fc; }
.customer-page .program-icon.is-warm     { color: #a86a10; background: #fdf4e4; }
.customer-page .program-icon.is-hot      { color: #bb4630; background: #fdece8; }
.customer-page .program-icon.is-quick    { color: #137b51; background: #e4fbef; }
.customer-page .program-icon.is-dry-low  { color: #5a6b7a; background: #eef2f5; }
.customer-page .program-icon.is-dry-mid  { color: #a86a10; background: #fdf4e4; }
.customer-page .program-icon.is-dry-high { color: #bb4630; background: #fdece8; }
.customer-page .program-icon.is-topup    { color: #5a6b7a; background: #eef2f5; }

.customer-page .program-text {
    flex: 1 1 auto;
    min-width: 0;
}

/* Name on the left, price hard right, on one line. baseline alignment rather
   than centre so the two different type sizes sit on the same footing instead
   of the price floating. */
.customer-page .program-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
}

/* app.css:5085 sets this to 18px, which wrapped the longer Thai labels onto a
   second line inside a 78px row. */
.customer-page .program-content .program-text strong {
    font-size: 1rem;
    font-weight: 800;
    letter-spacing: -0.01em;
    line-height: 1.3;
    color: #111820;
    /* The names are two or three syllables by design, so holding them on one
       line costs nothing and keeps every row the same height. */
    white-space: nowrap;
}

/* app.css:5089 paints every <small> in the row bold. The description is the one
   line meant to be read rather than glanced at, so it is set back to normal
   weight and given room to breathe. */
.customer-page .program-content .program-desc {
    margin-top: 3px;
    font-size: 0.75rem;
    font-weight: 400;
    line-height: 1.45;
    color: #68717d;
}

.customer-page .program-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-top: 7px;
}

.customer-page .program-chip {
    display: inline-block;
    padding: 2px 8px;
    border: 1px solid #dfe4e8;
    border-radius: 999px;
    background: #f6f8f9;
    font-size: 0.656rem;
    font-weight: 700;
    line-height: 1.6;
    color: #68717d;
    /* A chip that breaks across two lines stops looking like a chip. They are
       short enough to hold, and the row wraps between chips instead. */
    white-space: nowrap;
}

/* A price promotion is the one chip worth the brand colour - it is the only one
   that changes what the customer pays. */
.customer-page .program-chip.is-promo {
    border-color: #b7f0d3;
    background: #e4fbef;
    color: #137b51;
}

/* The price is what the customer is deciding on, so it is the largest thing in
   the row. app.css:5095 gave it min-width 86px and right alignment for the old
   third column; both are dropped now that it sits inline, and nowrap is what
   stops "40 บาท" splitting across two lines the way it did at 355px. */
.customer-page .program-content .program-price {
    flex: 0 0 auto;
    min-width: 0;
    font-size: 1.438rem;
    font-weight: 900;
    letter-spacing: -0.02em;
    line-height: 1.2;
    color: #111820;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* The struck-through original, when a promotion is running. Sits before the
   live price on the same line rather than above it. */
.customer-page .program-content .program-price .old-price {
    display: inline;
    margin-right: 5px;
    font-size: 0.813rem;
    font-weight: 700;
    color: #8b94a0;
    text-decoration: line-through;
}

@media (max-width: 640px) {
    .customer-page .program-icon {
        width: 38px;
        height: 38px;
        border-radius: 10px;
    }

    .customer-page .program-icon-svg {
        width: 20px;
        height: 20px;
    }

    .customer-page .program-content .program-text strong {
        font-size: 0.938rem;
    }

    .customer-page .program-content .program-price {
        font-size: 1.313rem;
    }
}

.customer-showcase {
    margin-top: 22px;
}

.customer-showcase-title {
    margin: 0 0 12px;
    font-size: 0.938rem;
    font-weight: 700;
    letter-spacing: .01em;
    color: var(--color-ink);
}

.customer-showcase-grid {
    display: grid;
    gap: 10px;
}

.customer-showcase-card {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    border-radius: var(--radius-medium, 16px);
    background: var(--color-success-soft);
    border: 1px solid var(--color-border);
}

.customer-showcase-icon {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    border-radius: 12px;
    background: var(--color-success);
    color: #fff;
    font-size: 0.813rem;
    font-weight: 800;
    line-height: 1;
    text-align: center;
}

.customer-showcase-body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.customer-showcase-body strong {
    font-size: 0.875rem;
    font-weight: 700;
    color: var(--color-ink);
}

.customer-showcase-body small {
    font-size: 0.75rem;
    color: var(--color-text-secondary);
}

/* ---------------------------------------------------------------------------
   Two languages in the markup, one on screen.

   The promotion lines carry the branch's own numbers, so the dictionary can
   never match them, and PHP has no idea which language the phone asked for -
   only the browser does. So both are rendered and one is hidden here, keyed off
   the lang attribute that i18n.js keeps up to date. That means switching
   language needs no JavaScript at all for these lines.
   --------------------------------------------------------------------------- */
html[lang="en"] .customer-page .lang-th,
html[lang="th"] .customer-page .lang-en {
    display: none;
}

/* The reward is the reason to read the card, so it carries the brand colour and
   the weight; the condition sits under it in plain muted text because it is the
   small print, and dressing small print up as a promise is how a customer ends
   up feeling misled. */
.customer-page .customer-showcase-reward {
    display: block;
    margin-top: 1px;
    font-size: 0.781rem;
    font-weight: 700;
    color: #137b51;
}

.customer-page .customer-showcase-condition {
    display: block;
    margin-top: 2px;
    font-size: 0.719rem;
    font-weight: 400;
    line-height: 1.45;
    color: #68717d;
}

/* Three lines instead of two, so the row can no longer be vertically centred
   against a 42px icon without the text drifting below it. */
.customer-page .customer-showcase-card {
    align-items: flex-start;
}

.customer-page .customer-showcase-icon {
    margin-top: 1px;
}

/* ---------------------------------------------------------------------------
   Header wordmark + machine cards.

   Every rule below OVERRIDES one that lives in app.css, which is hash-pinned
   and must never be edited. This file is linked after app.css, so equal
   specificity wins on source order.

   The overridden mobile rules sit inside @media (max-width: 640px) in app.css
   (app.css:5474-5492). A media query adds no specificity, so the plain rules
   here would otherwise beat them at EVERY width and flatten the responsive
   sizes - hence the matching @media block at the end of this file.
   --------------------------------------------------------------------------- */

/* --- Dark topbar -----------------------------------------------------------
   Colours sampled from the sign-in screen (auth/preview.css): --dark #0a121b,
   --dark-soft #111c27, --mint #55eca4, brand text #f7fafc. app.css:4770 paints
   this bar near-white, so everything sitting in it has to be re-inverted here.
   --------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   The dark bar.

   No safe-area padding here, and that is the second answer to the same question.
   A 1cm top pad was added on 2026-07-22 to clear the Dynamic Island, then
   removed the same day: this page is reached by scanning a QR code, so it opens
   inside the LINE in-app browser, and that browser already draws its own header
   bar below the Island. Padding for the Island underneath a bar that has already
   cleared it just stacked empty space on top of ours and pushed the wordmark and
   the back button to the bottom edge of the black.

   index.php also no longer sends viewport-fit=cover, so every env() inset
   resolves to 0. Do not re-add either without testing inside LINE first.
   --------------------------------------------------------------------------- */

.customer-topbar {
    background: #0a121b;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

/* The bar carries one control now, so it can be tighter than the admin bar it
   inherits from. app.css:1614 gives .topbar-inner a 56px floor and a 32px side
   inset; on a 375px phone that inset is most of a machine tile.

   The symmetric padding is what centres the brand and the button in the black
   rather than letting them sit against its bottom edge - app.css centres them
   inside this row, so the row itself has to be evenly inset from the bar. */
.customer-page .topbar-inner {
    width: min(1040px, calc(100% - 24px));
    min-height: 52px;
    padding-top: 8px;
    padding-bottom: 8px;
    gap: 12px;
}

/* Wordmark over tagline. app.css:1624 lays .brand out inline at 18px, which put
   the two side by side and let the Thai tagline wrap to three lines. */
.customer-page .customer-brand {
    display: grid;
    gap: 2px;
    align-content: center;
    min-width: 0;
}

.customer-brand-tagline {
    font-size: 0.688rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    line-height: 1.2;
    color: #9fb0bf;
    /* Thai renders this as ร้านซักอบอัจฉริยะ, which is long. Holding it on one
       line is what keeps the bar one height on every device. */
    white-space: nowrap;
}

/* The nav is a single button now; without this it keeps the wrap behaviour it
   needed when it held three things. */
.customer-page .customer-topbar .nav {
    flex: 0 0 auto;
    flex-wrap: nowrap;
    gap: 8px;
}

/* Matches the sign-in wordmark (auth/preview.css:196 .auth-brand), which is
   text and not an image. app.css:4796 .brand-mark is left untouched and unused.

   Weight 700, not 900: the sign-in screen uses 700, and at 900 the browser
   picks the Heavy/ExtraBold cut whose glyphs are wide enough to collide once
   the -0.05em tracking is applied at this much smaller size. The tracking is
   eased off for the same reason - it is a proportion of a 20px em here, not
   of the 42-66px em the sign-in screen sets it against. */
.customer-brand-wordmark {
    /* 19px, down from 21px: the wordmark now has a tagline sitting under it, so
       the pair reads as one block and the block should not be taller than the
       back button beside it. */
    font-size: 1.188rem;
    font-weight: 700;
    letter-spacing: -0.015em;
    line-height: 1.05;
    color: #f7fafc;
    white-space: nowrap;
}

/* auth/preview.css:204 .auth-brand strong, same mint. Now on the dark bar it
   was designed for, so it reads properly instead of washing out. */
.customer-brand-wordmark strong {
    font-weight: 700;
    color: #55eca4;
}

.customer-brand {
    color: #f7fafc;
}

.customer-topbar .nav a {
    color: #f7fafc;
}

/* Same treatment as the sign-in language pill (auth/preview.css .language-control:
   #0d1721 on a #33404c hairline). */
.customer-back,
.customer-topbar .language-switch {
    border-color: #33404c;
    background: #111c27;
    color: #f7fafc;
}

/* Nothing sized this button, so it took the UA's absolute 13.3333px and was one
   of only two things on the customer screen that ignored the reader's text-size
   setting. 0.833rem is that same size today and scales from here. */
.customer-back {
    font-size: 0.833rem;
}

/* app.css:1561 sets this to --color-text-secondary (#626760), which disappears
   against the dark pill. */
.customer-topbar .language-switch button {
    color: #9fb0bf;
}

/* Restore the pressed state. The rule above ties on specificity with app.css:1567
   .language-switch button[aria-pressed="true"] and, loading later, was winning -
   which painted the selected language in muted grey on its own mint pill. This
   selector carries one more class than that rule, so it takes the tie back. */
.customer-topbar .language-switch button[aria-pressed="true"] {
    color: var(--color-ink);
}

/* position: relative creates the containing block the number is pinned to;
   app.css:4920 has no positioning of its own. */
/* position: relative creates the containing block the number is pinned to;
   app.css:4920 has no positioning of its own.

   The fixed first row is what keeps the grid tidy: the images below are given
   different heights on purpose, so without it a shorter machine would pull its
   label upwards and the captions would not line up across a row. Pinning the
   row and bottom-aligning the image also stands every machine on one floor. */
.customer-machine-tile {
    position: relative;
    grid-template-rows: 180px auto;
    align-content: center;
    gap: 10px;
    min-height: 276px;
    padding: 30px 12px 18px;
}

/* Telling washers from dryers, 2026-07-23.
   -----------------------------------------------------------------------------
   First attempt filled the whole card with Kim's blue and amber. It separated
   the two kinds perfectly and looked bad: fourteen saturated blocks fight the
   product photographs that are the actual content, leave the eye nowhere to
   rest, and turn the mint number badge into an unrelated third colour.

   So the colour moves off the card and behind the machine: a soft disc, the
   shape shops use to stand a product on, plus a number badge in the same
   family. The card stays white, the photograph sits on its own colour, and the
   grid reads calm again.

   Colour is still only the second signal — the artwork differs and both label
   lines name the machine in words. `.is-unavailable` dims by opacity rather
   than repainting, so an out-of-service machine keeps its type. */
/* A rounded panel rather than a disc — Kim, 2026-07-23. It frames the tall
   machines better than a circle did, which had to be wide enough to clear the
   artwork's height and so left a lot of colour either side of a narrow machine.

   Sized against the card, not in fixed pixels: at a flat 158px the shape left
   6px of margin on a 170px card and would have burst out of the rounded corners
   on a 320px phone. */
/* A wide, low bar the machine STANDS ON rather than a frame that contains it —
   Kim's reference, 2026-07-23. The artwork deliberately breaks out above and
   below, which is what stops the panel reading as a box round the photo.

   The artwork occupies 32-212px of the 276px card. The bar is a little over
   half that height and centred on it, so it never reaches the badge at the top
   or the label at the bottom. */
.customer-machine-tile::before {
    content: "";
    position: absolute;
    top: 76px;
    left: 50%;
    width: min(188px, 88%);
    height: 92px;
    transform: translateX(-50%);
    border-radius: 16px;
    background: #f1f4f7;
    z-index: 0;
}

/* The artwork, badge and labels all sit above the disc. */
.customer-machine-icon,
.customer-machine-number,
.customer-machine-label {
    position: relative;
    z-index: 1;
}

.customer-machine-tile.is-washer::before {
    background: #dceafd;
}

.customer-machine-tile.is-dryer::before {
    background: #ffeecd;
}

/* White, not a tint of the type colour. The panel now reaches under the badge,
   and a pale blue badge on a pale blue panel all but vanished — the number is
   the one thing a customer standing in front of the machines is hunting for.
   White holds against both the card and the panel; the border and the numeral
   carry the colour. */
/* A soft filled disc, no outline — Kim's reference. It can be a plain tint now
   because the bar sits low on the card and the badge sits on white, so the two
   never touch; the earlier outlined version existed only because a full-height
   panel reached under the badge and swallowed it. */
.customer-machine-tile.is-washer .customer-machine-number,
.customer-machine-tile.is-dryer .customer-machine-number {
    width: 38px;
    min-width: 0;
    height: 38px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    font-size: 1rem;
    font-weight: 800;
}

.customer-machine-tile.is-washer .customer-machine-number {
    background: #dceafd;
    color: #1b4f8f;
}

.customer-machine-tile.is-dryer .customer-machine-number {
    background: #ffeecd;
    color: #8a5a06;
}

/* Sized by height with the width left to follow, so the artwork fills the card
   instead of being letterboxed inside a square.

   Each PNG draws its machine at a different height inside the same 180x240
   canvas - measured content heights are hc 201, hxc 201, dc/de 203, wdc 208,
   sdc 216. One shared size therefore renders the stacked dryers ~7.5% taller
   than the washers. The per-file heights below are 240 x (150 / contentHeight),
   which lands every machine at the same ~150px drawn height. Re-measure if the
   images are ever re-cut. */
.customer-machine-icon {
    width: auto;
    height: 180px;
    max-width: 100%;
    align-self: end;
}

.customer-machine-icon.is-dc,
.customer-machine-icon.is-de {
    height: 177px;
}

.customer-machine-icon.is-wdc {
    height: 173px;
}

.customer-machine-icon.is-sdc {
    height: 167px;
}

/* Pulled out of the grid flow into the top-right corner, and restyled as the
   reference's soft chip (preview.css:721-756 .status-pill) rather than a solid
   brand-filled pill. */
.customer-machine-number {
    position: absolute;
    top: 10px;
    right: 10px;
    min-width: 34px;
    height: 28px;
    padding: 0 10px;
    border: 1px solid #b7f0d3;
    background: #e4fbef;
    color: #137b51;
    font-size: 0.938rem;
    font-weight: 700;
}

/* --- The dark panels become cards ------------------------------------------
   The reference page has no dark hero: every block is a white .preview-panel
   on the flat canvas (preview.css:594-599, :653-658).

   Two separate app.css rules paint the customer surface dark - .branch-picker-head
   (app.css:4895, background var(--ink)) on the branch grid, and .journey-panel
   (app.css:4828, background var(--ink), color #fff) on BOTH the machine/program
   screen (index.php:298) and the order-status screen (index.php:168). Their
   headings clamp up to 64px (app.css:4903), far past anything on the reference. */
.customer-page .branch-picker-head,
.customer-page .journey-panel {
    padding: 20px;
    border: 1px solid #dfe4e8;
    border-radius: 14px;
    background: #ffffff;
    color: #111820;
    box-shadow: 0 12px 34px rgba(22, 35, 48, 0.08);
}

.customer-page .branch-picker-head h1,
.customer-page .journey-panel h1 {
    font-size: clamp(1.625rem, 5vw, 2.125rem);
    letter-spacing: -0.04em;
    line-height: 1.15;
    color: #111820;
}

/* The machine screen is NOT a landing screen: the customer has already read the
   branch name on the previous one and is here to pick a program. At the shared
   26px the same branch name broke onto two lines and the panel stood 421px tall
   against 190px on the branch screen, which is what made the two look like
   different products.

   The line that actually mattered is `max-width: none`. app.css:4903 caps this
   heading at `11ch` - about eleven characters - which was sized for the old dark
   hero where the name was meant to stack. "KHT Demo Branch" is fifteen, so it
   wrapped no matter what font size was set, and max-width clamps width whatever
   the specificity, exactly like the min-height trap on the range input above.
   text-wrap: balance was tried here first and made it worse: it was doing its
   job on a box that was only ever 147px wide. */
.customer-page .journey-panel h1 {
    max-width: none;
    font-size: 1.25rem;
    letter-spacing: -0.02em;
    line-height: 1.25;
}

/* app.css:4828 gives this panel `min-height: 360px`, `gap: 28px` and
   `justify-content: space-between` - all sized for the old full-height dark hero
   it used to be. min-height is the third constraint property today that beat a
   sizing rule regardless of specificity; see the range input and the 11ch
   heading cap above. */
.customer-page .journey-panel {
    min-height: 0;
    gap: 6px;
    justify-content: flex-start;
    padding: 16px 18px;
}

/* ---------------------------------------------------------------------------
   The chosen machine.

   Kim, 2026-07-22: this card used to carry the branch code and the branch name,
   both of which the customer had just read on the previous screen. It now
   answers the only question they actually have standing in front of the machine
   - did I tap the right one - so the artwork is large and the number sits on it
   the way it does on the tile they came from.
   --------------------------------------------------------------------------- */

.customer-page .journey-panel.chosen-machine {
    flex-direction: row;
    align-items: center;
    gap: 16px;
    padding: 18px;
}

.chosen-machine-art {
    position: relative;
    flex: 0 0 auto;
    display: grid;
    place-items: end center;
    width: 104px;
    height: 118px;
}

/* Each PNG draws its machine at a different height inside the same canvas, so
   the per-file heights below land them all on the same drawn size - the same
   normalisation the branch tiles use, scaled down. Re-measure if the images are
   ever re-cut. */
.chosen-machine-icon {
    width: auto;
    height: 118px;
    max-width: 100%;
    align-self: end;
}

.chosen-machine-icon.is-dc,
.chosen-machine-icon.is-de { height: 116px; }
.chosen-machine-icon.is-wdc { height: 113px; }
.chosen-machine-icon.is-sdc { height: 109px; }

/* The number badge, matching the branch tile so the customer recognises it as
   the same machine they just tapped. */
.chosen-machine-no {
    position: absolute;
    top: -2px;
    right: -4px;
    min-width: 32px;
    height: 26px;
    display: grid;
    place-items: center;
    padding: 0 9px;
    border: 1px solid #b7f0d3;
    border-radius: 999px;
    background: #e4fbef;
    color: #137b51;
    font-size: 0.938rem;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.chosen-machine-text {
    display: grid;
    gap: 4px;
    min-width: 0;
}

.chosen-machine-line {
    font-size: 1.063rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.25;
    color: #111820;
}

.chosen-machine-time {
    font-size: 0.781rem;
    font-weight: 400;
    line-height: 1.4;
    color: #68717d;
}

.chosen-machine-status {
    justify-self: start;
    margin-top: 3px;
}

@media (max-width: 380px) {
    .chosen-machine-art {
        width: 94px;
        height: 104px;
    }

    .chosen-machine-icon { height: 104px; }
    .chosen-machine-icon.is-dc,
    .chosen-machine-icon.is-de { height: 102px; }
    .chosen-machine-icon.is-wdc { height: 100px; }
    .chosen-machine-icon.is-sdc { height: 96px; }

    .chosen-machine-line {
        font-size: 0.938rem;
    }
}

/* Was stacked with generous spacing to fill the old dark hero. It is three
   short facts, so it reads better as one wrapped line. */
.customer-page .machine-summary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 10px;
    margin-top: 8px;
    font-size: 0.781rem;
}

/* app.css:4841, :4848 and :5007 set these to translucent white for the dark
   panel; on the card they would be invisible. */
.customer-page .branch-picker-head .muted,
.customer-page .journey-panel .muted,
.customer-page .journey-panel p,
.customer-page .machine-summary {
    color: #68717d;
}

/* The step list inside the panel was drawn entirely in translucent white
   (app.css:4866-4868 border and text, :4877 the dot, :4883 the active label) -
   invisible now that the panel is white. Repainted as the reference's hairline
   rows with a soft-mint active state. */
.customer-page .journey-steps li {
    border-color: #dfe4e8;
    background: #ffffff;
    color: #68717d;
}

.customer-page .journey-steps li::before {
    background: #dfe4e8;
}

.customer-page .journey-steps li.active {
    border-color: #b7f0d3;
    background: #e4fbef;
    color: #111820;
}

/* app.css:5123-5129 paints this as a gradient with white text. The reference
   has no gradients and puts dark text on mint (preview.css:633 .mint-button);
   white on #55eca4 would be about 2:1 and fail to read. */
.customer-page .pay-button {
    border-color: #55eca4;
    background: #55eca4;
    color: #0a121b;
    box-shadow: 0 12px 34px rgba(22, 35, 48, 0.08);
}

/* preview.css:1029-1038 .summary-card - white on a hairline, not a colour wash.
   Overrides the mint-tinted card this file set at the top. */
.customer-showcase-card {
    border-radius: 10px;
    background: #ffffff;
    border-color: #dfe4e8;
}

.customer-showcase-icon {
    background: #f6f8f9;
    color: #137b51;
}

.customer-machine-label {
    display: grid;
    gap: 2px;
    text-align: center;
    line-height: 1.25;
}

.customer-machine-label-en {
    font-size: 0.875rem;
    font-weight: 900;
    color: var(--color-ink);
}

.customer-machine-label-th {
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--color-text-secondary);
}

@media (max-width: 640px) {
    .customer-machine-tile {
        grid-template-rows: 143px auto;
        min-height: 228px;
        padding: 26px 10px 14px;
    }

    /* Same normalisation as above, scaled to a ~120px drawn machine height. */
    .customer-machine-icon {
        height: 143px;
    }

    .customer-machine-icon.is-dc,
    .customer-machine-icon.is-de {
        height: 142px;
    }

    .customer-machine-icon.is-wdc {
        height: 138px;
    }

    .customer-machine-icon.is-sdc {
        height: 133px;
    }

    .customer-machine-number {
        top: 8px;
        right: 8px;
        min-width: 30px;
        height: 26px;
        padding: 0 8px;
        font-size: 0.938rem;
    }

    /* The rule above sets a height but no width, which would stretch the typed
       badge from a circle into an oval on a phone. Both axes, together. */
    .customer-machine-tile.is-washer .customer-machine-number,
    .customer-machine-tile.is-dryer .customer-machine-number {
        width: 32px;
        height: 32px;
        font-size: 0.875rem;
    }

    /* The bar tracks the shorter artwork row on a phone: 26px of padding then a
       143px row, so 58 + 78 keeps it centred on the machine. */
    .customer-machine-tile::before {
        top: 58px;
        height: 78px;
    }

    .customer-machine-label-en {
        font-size: 0.813rem;
    }

    .customer-machine-label-th {
        font-size: 0.688rem;
    }
}

/* ---------------------------------------------------------------------------
   While the machine runs.

   Shown on the order screen only once the device has acknowledged the start.
   Carries the one thing the customer actually wants at that moment — when to
   come back — plus how to treat this load and, after a wash, where to go next.
   --------------------------------------------------------------------------- */

.order-running {
    display: grid;
    gap: 10px;
    margin: 16px 0 4px;
    padding: 18px 20px;
    border: 1px solid #b7f0d3;
    border-radius: 14px;
    background: #e4fbef;
}

.order-running[hidden] {
    display: none;
}

.order-collect {
    margin: 0;
    color: #0a121b;
    font-size: 1.25rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.25;
}

.order-care {
    margin: 0;
    color: #245c42;
    font-size: 0.813rem;
    line-height: 1.5;
}

.order-care[hidden] {
    display: none;
}

.order-next-step {
    justify-self: start;
    margin-top: 2px;
    padding: 10px 18px;
    border-radius: 999px;
    background: #137b51;
    color: #ffffff;
    font-size: 0.875rem;
    font-weight: 700;
    text-decoration: none;
}

.order-next-step[hidden] {
    display: none;
}

/* ---------------------------------------------------------------------------
   The receipt.

   Drawn as a canvas so the slip on screen and the slip that gets saved are the
   same pixels. The text twin beside it carries the same content for screen
   readers and for anyone who wants to select and copy it.
   --------------------------------------------------------------------------- */

.receipt-canvas {
    display: block;
    max-width: 100%;
    height: auto;
    margin: 0 auto;
    border: 1px solid #dfe4e8;
    border-radius: 12px;
    background: #ffffff;
    box-shadow: 0 12px 34px rgba(22, 35, 48, 0.08);
}

.receipt-text {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: pre;
}

.receipt-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-top: 16px;
}

@media (max-width: 380px) {
    .receipt-actions {
        grid-template-columns: 1fr;
    }
}

/* The thank-you wrapper around the slip. */
.promptpay-head[hidden] {
    display: none;
}

.receipt-thanks {
    display: grid;
    gap: 4px;
    margin-bottom: 16px;
    text-align: center;
}

.receipt-thanks-title {
    margin: 0;
    color: #111820;
    font-size: 1.25rem;
    font-weight: 800;
    letter-spacing: -0.02em;
}

.receipt-thanks-sub {
    margin: 0;
    color: #68717d;
    font-size: 0.813rem;
}

.receipt-advice {
    display: grid;
    gap: 6px;
    margin-top: 16px;
    padding: 16px 18px;
    border: 1px solid #b7f0d3;
    border-radius: 14px;
    background: #e4fbef;
}

.receipt-advice[hidden],
.receipt-advice-care[hidden],
.receipt-line[hidden],
.receipt-line-button[hidden] {
    display: none;
}

.receipt-advice-collect {
    margin: 0;
    color: #0a121b;
    font-size: 1.063rem;
    font-weight: 800;
}

.receipt-advice-collect:empty {
    display: none;
}

.receipt-advice-care {
    margin: 0;
    color: #245c42;
    font-size: 0.813rem;
    line-height: 1.5;
}

.receipt-line {
    display: grid;
    gap: 10px;
    justify-items: center;
    margin-top: 14px;
    padding: 16px 18px;
    border: 1px solid #dfe4e8;
    border-radius: 14px;
    background: #ffffff;
    text-align: center;
}

.receipt-line-text {
    margin: 0;
    color: #68717d;
    font-size: 0.813rem;
}

.receipt-line-button {
    padding: 10px 22px;
    border-radius: 999px;
    background: #06c755;
    color: #ffffff;
    font-size: 0.875rem;
    font-weight: 700;
    text-decoration: none;
}

/* ---------------------------------------------------------------------------
   The PromptPay screen.

   Written for a customer in their fifties standing at a machine: the amount
   first and large, one green button, then the steps numbered in the order the
   thumb performs them. No bank chooser — a PromptPay QR is read by every bank
   app, and offering a list would imply only those banks work.
   --------------------------------------------------------------------------- */

.pay-amount-label {
    margin: 0;
    color: #68717d;
    font-size: 0.813rem;
    text-align: center;
}

.pay-amount {
    margin: 2px 0 14px;
    color: #111820;
    text-align: center;
    line-height: 1.1;
}

.pay-amount strong {
    font-size: 2.5rem;
    font-weight: 900;
    letter-spacing: -0.03em;
}

.pay-amount span {
    font-size: 1rem;
    font-weight: 700;
    color: #68717d;
}

/* Navy, matching the band above it, so the button reads as part of the Thai QR
   flow rather than as this app asking for something separate. */
.pay-save-button {
    display: block;
    width: 100%;
    margin: 16px 0 0;
    padding: 16px 20px;
    border: 0;
    border-radius: 999px;
    background: #12395c;
    color: #ffffff;
    font-size: 1.063rem;
    font-weight: 800;
    cursor: pointer;
}

/* The main action on the payment sheet. Without the guard a touch screen left
   it in its darkened hover state after the tap, so the one button the customer
   is meant to press looked stuck down for the rest of the session. */
@media (hover: hover) and (pointer: fine) {
    .pay-save-button:hover {
        background: #0d2c48;
    }
}

.pay-save-note {
    margin: 10px 0 0;
    color: #245c42;
    font-size: 0.813rem;
    text-align: center;
}

.pay-save-note[hidden] {
    display: none;
}

.pay-steps {
    margin: 18px 0 0;
    padding-left: 22px;
    color: #111820;
    font-size: 0.938rem;
    line-height: 1.65;
}

.pay-steps li {
    margin-bottom: 6px;
}

.pay-amount-locked {
    margin: 6px 0 0;
    padding: 10px 14px;
    border-radius: 10px;
    background: #e4fbef;
    color: #245c42;
    font-size: 0.813rem;
    text-align: center;
}

/* Offered when the customer comes back to a page that has lost their order —
   most often because Android killed LINE while they were in their bank app.
   Amber rather than red: it is an unfinished errand, not an error. */
.resume-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    flex-wrap: wrap;
    margin: 0 0 18px;
    padding: 14px 18px;
    border: 1px solid #f2c98a;
    border-left: 4px solid #e79c2f;
    border-radius: 12px;
    background: #fff8ec;
}

.resume-banner-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.resume-banner-text strong {
    color: #7a4a06;
    font-size: 0.938rem;
}

.resume-banner-text span {
    color: #99702f;
    font-size: 0.813rem;
}

.resume-banner-action {
    flex-shrink: 0;
    padding: 9px 20px;
    border-radius: 999px;
    background: #e79c2f;
    color: #fff;
    font-size: 0.875rem;
    font-weight: 600;
    text-decoration: none;
}

/* ---------------------------------------------------------------------------
   The PromptPay sheet.

   None of these classes had any CSS. The markup called itself a modal but had
   no positioning, so it stacked underneath the payment panel, the close button
   sat loose at the left, and the detail list fell back to a browser default
   <dl> — which is why "เหลือเวลา" and its value landed left-aligned in a
   column of their own.

   Palette is the one the app already uses: #137b51 for the action the customer
   is meant to take, the #e4fbef/#245c42 mint that already marks a settled fact,
   and the #e79c2f/#fff8ec amber introduced for the resume banner, reused here
   because a countdown is the same kind of signal — pay attention, nothing is
   wrong yet.
   --------------------------------------------------------------------------- */

.payment-modal {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    box-sizing: border-box;
    /* Room above the sheet so it never reaches the very top of the screen. */
    padding: 24px 0 0;
}

/* ---------------------------------------------------------------------------
   Press feedback.

   Nothing on the customer page acknowledged a tap. On a phone on a shop's
   wifi, a button that does not visibly react is a button people press again -
   and one of these creates an order. The scale is small on purpose: enough to
   register as "heard you", not enough to look like a toy.

   transition on the element, not on :active, so the release eases back instead
   of snapping. Only transform is animated - it skips layout and paint.
   --------------------------------------------------------------------------- */

.customer-page .pay-save-button,
.customer-page .promptpay-actions > *,
.customer-page .promptpay-close,
.customer-page .program-chip,
.customer-page button[type="submit"] {
    transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
}

.customer-page .pay-save-button:active,
.customer-page .promptpay-actions > *:active,
.customer-page .promptpay-close:active,
.customer-page .program-chip:active,
.customer-page button[type="submit"]:active {
    transform: scale(0.97);
}

/* ---------------------------------------------------------------------------
   Motion on the payment sheet.

   This sheet used to appear with no transition at all: one frame the customer
   is choosing a programme, the next a full-screen panel is covering everything.
   That reads as the screen breaking rather than as a new step, and it happens
   at the exact moment the customer is deciding whether to hand over money.

   It is a bottom sheet (the modal is a flex container with align-items:
   flex-end), so it comes up from the bottom - the direction it lives in. The
   curve is Ionic's iOS drawer curve; the duration sits at the fast end of the
   200-500ms a drawer gets, because this is a step in a payment, not a flourish.

   Written in CSS rather than JS on purpose. `display` is what hides this sheet,
   and @starting-style plus `transition-behavior: allow-discrete` is what lets
   display-based show/hide animate without a single line of script - so there is
   no timer that can leave the sheet stuck on screen if it fails to fire. On a
   browser too old for either, every rule here is ignored and the sheet appears
   instantly, which is exactly what it does today.

   Exit is deliberately faster than enter (200ms vs 320ms): opening introduces a
   screen, closing is the system getting out of the way.

   No prefers-reduced-motion block is needed - app.css:6875 already clamps
   transition-duration to 0.01ms globally for anyone who asks for less motion.
   --------------------------------------------------------------------------- */

.payment-modal {
    opacity: 1;
    transition: opacity 200ms ease, display 200ms allow-discrete;
}

.payment-modal[hidden] {
    display: none;
    opacity: 0;
}

@starting-style {
    .payment-modal:not([hidden]) {
        opacity: 0;
    }
}

.payment-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(8, 15, 24, 0.55);
}

/* The card carries its own slide so the backdrop can fade on a shorter,
   plainer curve underneath it - the dark comes in first, the sheet follows. */
.payment-modal .promptpay-card {
    transform: translateY(0);
    transition: transform 320ms cubic-bezier(0.32, 0.72, 0, 1);
}

.payment-modal[hidden] .promptpay-card {
    transform: translateY(100%);
    /* Matches the 200ms the modal itself takes to go, so the sheet is not still
       sliding when the thing containing it has already been removed. */
    transition-duration: 200ms;
}

@starting-style {
    .payment-modal:not([hidden]) .promptpay-card {
        transform: translateY(100%);
    }
}

.promptpay-card {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    width: 100%;
    max-width: 460px;
    /* Against the fixed parent, not vh. The in-app browsers this runs in
       resize their toolbars mid-session, and a vh-sized sheet either overflows
       or leaves a gap when they do. */
    max-height: 100%;
    overflow-y: auto;
    padding: 0 20px 24px;
    border-radius: 22px 22px 0 0;
    background: #ffffff;
    box-shadow: 0 -14px 40px rgba(8, 15, 24, 0.28);
}

@media (min-width: 520px) {
    .payment-modal {
        align-items: center;
        padding: 24px;
    }

    .promptpay-card {
        border-radius: 22px;
    }
}

/* The navy band from the standard Thai QR slip. It is the one part of this
   screen a customer has seen a hundred times before - at a market stall, in a
   hospital, on a donation poster - so it says "this is the payment you already
   know how to make" faster than any wording could. */
.promptpay-head {
    position: sticky;
    top: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin: 0 -20px 16px;
    padding: 16px 20px;
    background: #12395c;
    text-align: left;
}

.promptpay-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

.promptpay-brand-mark {
    flex-shrink: 0;
    width: 34px;
    height: 34px;
}

.promptpay-brand-text strong {
    display: block;
    color: #ffffff;
    font-size: 0.938rem;
    font-weight: 800;
    letter-spacing: 0.08em;
}

.promptpay-brand-text > span {
    display: block;
    margin-top: 2px;
    color: #a9c4dc;
    font-size: 0.75rem;
}

/* 44px, not 36. This is the only way out of a sheet that covers the whole
   screen, it sits in the top-right corner - the hardest place on a phone for a
   thumb to reach - and at 36x40 it was the smallest target on the page. The
   circle still LOOKS 36px: the padding does the growing, so the hit area is
   44x44 while the ring stays the size the design wants. */
.promptpay-close {
    position: relative;
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    border: 1px solid rgba(255, 255, 255, 0.35);
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.12);
    color: #ffffff;
    font-size: 1.125rem;
    line-height: 1;
    cursor: pointer;
}

/* The ring stays 36px because that is what the design wants; the TARGET grows
   to 44x44, the minimum a thumb can hit reliably. A transparent pseudo-element
   is part of the button for hit-testing, so this costs nothing visually.
   It matters here more than anywhere else on the page: this is the only way out
   of a sheet that covers the whole screen, and it sits in the top-right corner,
   the furthest point from a thumb. */
.promptpay-close::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}
/* Stated as a size rather than `inset: -4px`, because inset is measured from
   the PADDING box: with the ring's 1px border that gave 3px of extra target,
   not 4, and the corner tap still missed. Caught by hit-testing the corner in a
   browser, not by reading the rule. */

/* A touch screen fires :hover on tap and leaves it stuck until the next tap
   somewhere else, so on the LINE in-app browser this button stayed lit as if it
   were still being pressed. Gate it behind a real pointer. */
@media (hover: hover) and (pointer: fine) {
    .promptpay-close:hover::before {
        background: rgba(255, 255, 255, 0.24);
    }
}

/* Sits above the QR the way the wordmark does on the printed slip. */
.promptpay-badge {
    display: inline-block;
    margin-bottom: 12px;
    padding: 5px 16px;
    border: 2px solid #12395c;
    border-radius: 4px;
    color: #12395c;
    font-size: 0.938rem;
    font-weight: 800;
    letter-spacing: 0.01em;
}

/* Everything centres except the instructions, which need a common left edge. */
.promptpay-body {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}

.promptpay-body[hidden] {
    display: none;
}

.promptpay-body > * {
    width: 100%;
}

.promptpay-qr-shell {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 14px;
    border: 1px solid #e2e7ec;
    border-radius: 16px;
    background: #ffffff;
}

.promptpay-qr-shell img {
    display: block;
    width: 100%;
    max-width: 260px;
    height: auto;
}

/* Emphasis 1 of 2. Bigger than body text because this is the part a customer
   in their fifties has to follow while holding a basket. */
.pay-steps {
    margin: 18px 0 0;
    padding: 16px 18px 16px 52px;
    border-radius: 14px;
    background: #f4f8fb;
    color: #111820;
    font-size: 1.063rem;
    line-height: 1.55;
    text-align: left;
    list-style: none;
    counter-reset: pay-step;
}

.pay-steps li {
    position: relative;
    margin-bottom: 12px;
    counter-increment: pay-step;
}

.pay-steps li:last-child {
    margin-bottom: 0;
}

.pay-steps li::before {
    content: counter(pay-step);
    position: absolute;
    left: -36px;
    top: 1px;
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: #137b51;
    color: #ffffff;
    font-size: 0.875rem;
    font-weight: 800;
    line-height: 1;
}

.pay-steps b {
    font-weight: 800;
    white-space: nowrap;
}

/* Emphasis 2 of 2. It was a row in the detail list, which put the only number
   on this screen that moves next to two that never do. */
.pay-countdown {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    margin: 14px 0 0;
    padding: 12px 16px;
    border: 1px solid #f2c98a;
    border-radius: 14px;
    background: #fff8ec;
}

.pay-countdown-label {
    color: #99702f;
    font-size: 0.813rem;
    font-weight: 700;
}

.pay-countdown-value {
    color: #7a4a06;
    font-size: 2.125rem;
    font-weight: 900;
    line-height: 1.05;
    letter-spacing: -0.02em;
    /* Digits keep their column as the seconds tick, so the number does not
       jitter once a second for ten minutes. */
    font-variant-numeric: tabular-nums;
}

/* Under two minutes. Red is held back until then so it still means something. */
.pay-countdown.is-urgent {
    border-color: #f0b4ac;
    background: #fdf0ee;
}

.pay-countdown.is-urgent .pay-countdown-label {
    color: #a3382b;
}

.pay-countdown.is-urgent .pay-countdown-value {
    color: #8f2c20;
}

.promptpay-detail {
    display: grid;
    gap: 8px;
    margin: 14px 0 0;
    padding: 14px 16px;
    border-radius: 14px;
    background: #f7fafc;
}

.promptpay-detail > div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    text-align: left;
}

.promptpay-detail dt {
    flex-shrink: 0;
    color: #68717d;
    font-size: 0.813rem;
}

.promptpay-detail dd {
    margin: 0;
    color: #111820;
    font-size: 0.813rem;
    font-weight: 700;
    text-align: right;
    word-break: break-all;
}

.promptpay-processing-bar {
    margin: 14px 0 0;
    padding: 10px 14px;
    border-radius: 999px;
    background: #eef1f4;
    color: #68717d;
    font-size: 0.813rem;
    font-weight: 700;
}

.promptpay-help {
    margin: 10px 0 0;
    color: #68717d;
    font-size: 0.813rem;
    line-height: 1.5;
}

.promptpay-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    margin: 16px 0 0;
}

/* 44px minimum. app.css gives every button min-height: 40px, which is four
   pixels under what a thumb hits reliably, and these two sit at the bottom of a
   full-screen payment sheet where a mis-tap costs the customer their place. */
.promptpay-actions > * {
    flex: 1 1 auto;
    min-width: 120px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-align: center;
}

/* Outlined in the band's navy rather than filled with it. Three solid navy
   buttons would each look as important as the one the customer is actually
   meant to press, and one of them only exists in the sandbox. */
.promptpay-actions .secondary,
.promptpay-actions a.button {
    border: 1px solid #b9cddf;
    background: #ffffff;
    color: #12395c;
    font-weight: 700;
}

/* Touch screens fire :hover on tap and leave it stuck until the next tap
   elsewhere, so on a phone these two stayed lit as if still being pressed. */
@media (hover: hover) and (pointer: fine) {
    .promptpay-actions .secondary:hover,
    .promptpay-actions a.button:hover {
        border-color: #12395c;
        background: #f2f7fb;
    }
}

/* Marketing opt-in card (LINE promotions consent). Hidden until the LIFF JS
   reveals it for an identified customer. Matches the customer-page card look. */
.customer-consent {
    margin-top: 14px;
    padding: 14px 16px;
    border: 1px solid #d7e3ee;
    border-radius: 14px;
    background: #f7fbff;
}
.consent-row {
    display: flex;
    align-items: center;
    gap: 10px;
    font-weight: 600;
    color: #123245;
    cursor: pointer;
}
.consent-row input[type="checkbox"] {
    width: 20px;
    height: 20px;
    flex: 0 0 auto;
    accent-color: #0a7d33;
}
.consent-note {
    margin: 8px 0 0;
    font-size: 0.82rem;
    color: #6a7a88;
}

/* ===========================================================================
   Referral cards (parts a + b). ONE .customer-referral shell serves the invite
   SHARE card (#customer-referral, home view) and the pre-payment ENTRY card
   (#referral-entry, machine view). They never render on the same screen. Same
   white card as .customer-points; both ship hidden, JS reveals only its own.
   Light-only, matching this file. IDs carry per-card differences so the two can
   never diverge.
   =========================================================================== */
.customer-referral {
    display: grid;
    gap: 6px;
    margin-top: 18px;
    padding: 18px 20px;
    border: 1px solid #dfe4e8;
    border-radius: 14px;
    background: #ffffff;
    box-shadow: 0 12px 34px rgba(22, 35, 48, 0.08);
}

.customer-referral[hidden] {
    display: none;
}

.customer-referral-title {
    margin: 0;
    font-size: 1.125rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.25;
    color: #111820;
}

.customer-referral-note {
    margin: 0;
    font-size: 0.781rem;
    line-height: 1.45;
    color: #68717d;
}

/* --- Part (a) share: caller's own code chip (written by JS) --------------- */
.customer-referral-code {
    margin: 4px 0 2px;
    padding: 10px 14px;
    border: 1px solid #b7f0d3;
    border-radius: 10px;
    background: #e4fbef;
    color: #137b51;
    font-size: 0.938rem;
    font-weight: 800;
    letter-spacing: 0.02em;
    font-variant-numeric: tabular-nums;
}

.customer-referral-code:empty {
    display: none;
}

#customer-referral .pay-button {
    margin-top: 8px;
}

/* --- Part (b) entry: claim form, input, result line ---------------------- */
.customer-referral-form {
    display: grid;      /* stacked; robust on a narrow phone and across TH/EN button text */
    gap: 10px;
    margin-top: 8px;
}

.customer-referral-input {
    width: 100%;
    min-width: 0;
    padding: 12px 14px;
    border: 1px solid #cfd6dc;
    border-radius: 10px;
    background: #f7f9fa;
    color: #111820;
    font-size: 1.125rem;    /* >=16px so iOS does not zoom on focus */
    font-weight: 800;
    letter-spacing: 0.18em;
    text-transform: uppercase;
}

.customer-referral-input:focus {
    outline: none;
    border-color: #137b51;
    background: #ffffff;
    box-shadow: 0 0 0 3px rgba(19, 123, 81, 0.15);
}

.customer-referral-input:disabled {
    color: #8a939d;
    background: #eef1f3;
}

.customer-referral-msg {
    margin: 0;
    padding: 11px 13px;
    border-radius: 10px;
    font-size: 0.813rem;
    font-weight: 600;
    line-height: 1.35;
}

.customer-referral-msg[hidden] {
    display: none;
}

.customer-referral-msg.is-success {
    border: 1px solid #b7f0d3;
    background: #e4fbef;
    color: #0f5c3c;
}

.customer-referral-msg.is-error {
    border: 1px solid #f2c2c2;
    background: #fdecec;
    color: #9c2a2a;
}

.customer-referral.is-done .customer-referral-form {
    opacity: 0.55;
}

/* ── prefers-contrast / prefers-reduced-transparency (2026-07-28) ─────────────
   The customer page's half of the pair added to admin-shell.css on the same
   day; admin-shell.css is not loaded here, so the rules have to be restated
   rather than shared. Same measured token replacements - the tokens live on
   :root in the hash-pinned app.css and are overridden on the body class, which
   is all the cascade needs.

   This page is where it matters most: it is the one surface a member of the
   public meets, on their own phone, in whatever light the laundromat has. */

@media (prefers-contrast: more) {
    .customer-page {
        --color-text-secondary: #3a3e38;   /* 5.79:1 -> 10.90:1 on white */
        --color-text-muted: #4a4f48;       /* 2.98:1 ->  8.39:1 */
        --color-border: #6b7169;           /* 1.25:1 ->  5.01:1 */
    }

    .customer-page :focus-visible {
        outline: 3px solid var(--color-ink);
        outline-offset: 2px;
    }

    .customer-page .customer-topbar {
        background: #ffffff;
        backdrop-filter: none;
        border-bottom: 1px solid var(--color-border);
    }
}

@media (prefers-reduced-transparency: reduce) {
    .customer-page .customer-topbar {
        background: #ffffff;
        backdrop-filter: none;
    }
}

/* Paid add-on under the programme list (2026-07-29, Primus idea #3). Same card
   language as .program-option but visually subordinate - it is an option ON the
   choice above it, not a sibling choice. Dryers only; the block for a programme
   with no add-ons never renders. */
.addon-area { margin-top: 10px; }
.addon-block { display: grid; gap: 8px; }
.addon-option {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
    border: 1px dashed #9db8a9;
    border-radius: 12px;
    background: #f4faf6;
    cursor: pointer;
}
.addon-option input[type="checkbox"] {
    width: 22px;
    height: 22px;
    accent-color: #1f7a53;
    flex: none;
}
.addon-option:has(input:checked) {
    border-style: solid;
    border-color: #1f7a53;
    background: #e8f6ee;
}
.addon-text { display: flex; flex: 1; align-items: baseline; justify-content: space-between; gap: 8px; }
.addon-text strong { font-size: 0.938rem; color: #14281d; }
.addon-price { font-size: 0.938rem; font-weight: 900; color: #1f7a53; white-space: nowrap; }
