/* ============================================================================
   หอม / HOM — design system.  Direction contract.

   THESIS        One light source. A warm dusk gradient drenches the top of every
                 page; everything under it stays white and quiet so a hundred
                 faces read as one calm catalogue.
   OWN-WORLD     Orange to coral to magenta to violet on near-white, one plum-black
                 ink, pill-shaped controls, and a portrait field that gives every
                 person without a photograph a stable colour of their own.
   STORY         You arrive inside the colour, land on the white search panel that
                 overlaps it, then scan. The page cools as you go down.
   FIRST VIEWPORT Phone: transparent bar on the gradient, headline, floating search
                 panel, two columns of people already showing. Desktop: white bar,
                 centred nav, wide hero, four columns.
   FORM          Pill, panel, card. Offset plum shadows, 12-24px radii, Thai
                 line-height never below 1.4, one rise on entry.
   ============================================================================ */

/* ---------------------------------------------------------------- 1. tokens */
/* Every value below is read from the owner's own design source — the four
   .dc.html files, whose theme() method carries both palettes literally — or
   measured from that source rendered at 1425px with its own font loaded.
   Nothing here is estimated from a screenshot; the values that were, were
   wrong, and this block replaces them. */

/* The face the comps are actually set in. The source declares this single
   Regular file across the whole 100–900 range, which tells the browser not to
   synthesise bold — so no weight renders heavier than any other. That is the
   owner's decision (15 ก.ย. 2569), not a limitation: declaring `400` here would
   restore synthetic bold. Hierarchy therefore comes from size and ink alone.
   PSL Kanda carries no ★ ✓ ● ○ ▾ → ☀ ☾ glyphs, which is why every symbol in
   this interface is drawn as inline SVG by icon() and must stay that way. */
@font-face {
  font-family: "PSL Kanda";
  src: url("/assets/fonts/PSLKandaModernExtraPro.ttf") format("truetype");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: light;

  /* ink + ground */
  --bg: #FFFAFC;                  /* appBg */
  --card: #FFFFFF;                /* cardBg */
  --ink: #2E1233;                 /* tTitle */
  --ink-soft: #6A4A72;            /* tBody */
  --muted: #7B6583;               /* tMuted. AA: the lightest value in the source's hue
                                     that holds 4.5:1 on --tint (was #9C86A5 = 2.93) */
  --border: rgba(122, 47, 140, .13);   /* borderCol — translucent plum, not an opaque pink */
  --tint: #FBEEF5;                /* subtleBg / chipOff */
  --subtle: var(--tint);          /* legacy alias */
  --nav-bg: rgba(255, 250, 252, .95);  /* the bar is translucent over a 14px blur */

  /* brand */
  --brand: #C13584;
  --brand-ink: #B0308A;           /* pillFg / navOn — the most-used text accent */
  --violet: #8E45E8;
  --orange: #FDA84B;
  --coral: #F5515F;
  --star: #F2A03C;                /* goldAccent; does NOT swap by theme */
  --icon-ink: var(--ink-soft);    /* the comp draws all header glyphs in tBody */

  /* nav + pill states */
  /* .60 flattened to #896E8B over --bg, which is 4.36:1 behind a 16px label —
     just under AA. .62 reads identically and measures 4.63:1. */
  --nav-off: rgba(58, 17, 64, .64);
  --nav-on: var(--brand-ink);
  --nav-on-bg: rgba(193, 53, 132, .11);
  --pill-fg: var(--brand-ink);
  --pill-bg: rgba(193, 53, 132, .10);
  --chip-off: var(--tint);
  --chip-off-fg: var(--ink-soft);

  /* status — real data, never invented. --busy and --star are deliberately
     different values now; they used to be byte-identical and moved together. */
  --ok: #34C77B;
  --busy: #FF9F1C;
  --off: #A8949F;
  --warn: #B15103;
  --danger: #C12652;
  --line: #047C36;                /* LINE green deepened to hold white text */
  --heart-on: #D62976;
  --heart-off: rgba(122, 47, 140, .4);

  /* ink that sits on a gradient — the source uses three near-whites on purpose */
  --on-grad: #FFF8F4;             /* headline, wordmark, primary label */
  --on-grad-2: #FFF3EE;           /* chips and small text on a gradient */
  --on-grad-3: #FFF6F2;           /* selected chip / CTA label */

  /* gradients */
  --grad-hero: linear-gradient(160deg, #FDA84B 0%, #F5515F 30%, #C13584 62%, #7B2FF7 100%);
  --grad-action: linear-gradient(100deg, #8E45E8 0%, #C13584 52%, #F58B4C 100%);
  --grad-mark: linear-gradient(135deg, #F5695A, #C13584 55%, #7B2FA8);
  /* The coral/orange end cannot carry white text at 4.5:1 without turning brown
     (orange light enough to stay orange is L > .30; white needs L < .18). So the
     warm stop moves to where no label sits: a rose stop (#C8385F, 4.77 with
     white) holds the text zone and the source's coral/orange stays as the edge.
     Measured: mean dE 2.3-3.4 along the gradient, chroma kept, not a scrim. */
  --grad-chip: linear-gradient(100deg, #8E45E8 0%, #C13584 50%, #C8385F 84%, #F5695A 100%);
  --grad-avatar: linear-gradient(135deg, #F5515F 0%, #C13584 45%, #C13584 100%);
  --grad-underline: linear-gradient(90deg, #F5515F, #C13584);
  --grad-scrim: linear-gradient(to top, rgba(58, 17, 64, .5), transparent);

  /* 23 ก.ย. 2569: the owner asked for every colour to pass AA, which supersedes
     the 15 ก.ย. decision to leave the hero without an overlay. What changed:

     - Buttons: labels span 13-87% of a content-sized button, so the orange end
       now starts at 86% (rose stop in between, same as --grad-chip).
     - Gradient bands carry a SCRIM IN DEEP RED, not plum. At the darkness white
       text needs, a plum overlay keeps 70-84% of the band's chroma (that is the
       'muddy' the owner rejected); #9E1B2E keeps 95-103% with a hue shift of
       ~4 degrees on the coral zones. Each scrim is the weakest that passes at
       360/375/393/768/900/1024/1440 in its theme; dark needs far less.
     - Still no text-shadow: it softens Thai strokes (see .hero h1). */
  --grad-action-safe: linear-gradient(100deg, #8E45E8 0%, #C13584 50%, #C8385F 86%, #F58B4C 100%);
  --scrim-hero: linear-gradient(105deg, rgba(158, 27, 46, .30) 0%, rgba(158, 27, 46, .22) 45%, rgba(158, 27, 46, 0) 75%);
  --scrim-hero-phone: linear-gradient(180deg, rgba(158, 27, 46, .58) 0px, rgba(158, 27, 46, .48) 100px, rgba(158, 27, 46, 0) 220px);
  --scrim-band: linear-gradient(105deg, rgba(158, 27, 46, .56) 0%, rgba(158, 27, 46, .44) 30%, rgba(158, 27, 46, .36) 60%, rgba(158, 27, 46, 0) 90%);
  --glass-on-grad: rgba(158, 27, 46, .32);      /* pills/chips on a band: deep glass, not white */
  --glass-on-grad-hover: rgba(158, 27, 46, .42);
  --check-ring: #9C86A5;                        /* unchecked box ring, 3:1 on white */
  --on-brand: #FFFFFF;                          /* text/glyph on a var(--brand) fill */

  /* The one place --grad-mark carries TEXT rather than a glyph: the guide's
     01/02/03 badge, at 15px/700, where the rule at site.css:2012 has always
     asked for this token — and nothing ever defined it. `background-image:
     var(--grad-mark-safe)` with no definition is invalid at computed-value
     time, which resolves to `none`, so the badge had no gradient at all and
     its white text sat on the white card underneath: invisible in the light
     theme, and a missing tile in the dark one.

     The scrim is what the "-safe" in the name means. --grad-mark starts at
     #F5695A, where white is 3.2:1 — under the 4.5 floor that 15px text needs.
     Measured with this scrim over it: 4.95:1 at the coral start and about 12:1
     by the violet end. The seven places that use the bare --grad-mark carry an
     icon, not text, and keep it: a graphic's floor is 3:1. */
  --grad-mark-safe:
    linear-gradient(135deg, rgba(58, 17, 64, .32), rgba(58, 17, 64, .10) 55%, transparent),
    var(--grad-mark);

  /* portrait fields, cycled by provider.id % 6 — three stops each, not two */
  --pf-0: linear-gradient(150deg, #FFD3A0 0%, #F97A6D 45%, #C13584 100%);
  --pf-1: linear-gradient(150deg, #FDA84B 0%, #F5515F 48%, #A32E8C 100%);
  --pf-2: linear-gradient(150deg, #8E45E8 0%, #B02E9E 50%, #5B1B6E 100%);
  --pf-3: linear-gradient(150deg, #C13584 0%, #8E45E8 50%, #4A1B6E 100%);
  --pf-4: linear-gradient(150deg, #FFC46B 0%, #F5695A 45%, #B02E9E 100%);
  --pf-5: linear-gradient(150deg, #6A2E9E 0%, #8E45E8 50%, #C13584 100%);

  /* radii — the source never uses a 999px pill on anything but dots and counts.
     Each token below is named for what the source actually applies it to. */
  --r-btn: 14px;        /* primary buttons */
  --r-field: 15px;      /* search field, form field, sort pill */
  --r-chip: 16px;       /* district chip */
  --r-card: 18px;       /* provider card */
  --r-nav: 11px;        /* nav button, icon button, badge on a portrait */
  --r-tile: 12px;       /* logo tile */
  --r-micro: 8px;       /* district micro-pill inside a card */
  --r-panel: 24px;        /* desktop search panel */
  --r-panel-phone: 22px;  /* the same panel on a phone, as the source draws it */
  --r-modal: 26px;      /* dialog, bottom sheet */
  --r-pill: 999px;      /* dots and count badges only */

  /* shadows — the source pairs a negative-spread drop with a 1px ring. We keep
     the existing 1px border, which draws the same hairline, and carry only the
     drop here: a ring plus a border would double the edge. */
  --sh-card: 0 8px 20px -12px rgba(58, 17, 64, .35);
  --sh-lift: 0 14px 30px -14px rgba(58, 17, 64, .5);
  --sh-panel: 0 22px 50px -24px rgba(58, 17, 64, .45);
  --sh-inset: 0 12px 28px -22px rgba(58, 17, 64, .6);
  --sh-cta: 0 12px 26px -14px rgba(193, 53, 132, .9);
  --sh-sheet: 0 -18px 46px -22px rgba(24, 10, 30, .7);
  --sh-overlay: 0 18px 40px -20px rgba(24, 10, 30, .75);

  /* metrics */
  --shell: 1300px;      /* the source's content column */
  --gutter: 16px;
  --topbar-h: 56px;
  --bottomnav-h: 84px;
  /* What the bar ACTUALLY occupies: 1px border + 8px top + 65px tab, plus the
     larger of 10px and the handset's own reserved strip. --bottomnav-h bakes in
     a flat 10, so anything that has to sit ON TOP of the bar, or clear it, must
     use this instead or it will be out by the inset. The floor was the comp's 24;
     the owner asked for less dead space under the labels (23 ก.ย. 2569). A phone
     with a home indicator still gets its full inset from env(). */
  --bottomnav-real: calc(74px + max(10px, env(safe-area-inset-bottom)));
  --grid-gap: 12px;
  --grid-cols: 2;
  --overlap: 24px;

  /* type */
  --font-display: "PSL Kanda", "Noto Serif Thai", Georgia, serif;
  --font-ui: "PSL Kanda", "Noto Sans Thai", system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
  --t-hero: clamp(26px, 4.4vw, 52px);
  --ease-rise: cubic-bezier(.16, 1, .3, 1);
}

/* ------------------------------------------------------------ 2. dark theme */
/* Also lifted whole from the source's theme() — the previous dark palette was
   invented, and several of its values had no counterpart in the design at all. */

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: #180E1D;
  --card: #251530;
  --ink: #F9EAF6;
  --ink-soft: #CBAED4;
  --muted: #A992B4;   /* >= 4.6 on every dark surface it sits on, incl. the selected-pill
                         fill (#9D86A7 was 3.95 there and 4.51 on --tint) */
  --border: rgba(240, 180, 220, .15);
  --tint: #33203F;
  --nav-bg: rgba(20, 11, 25, .95);

  --brand: #F19AC8;
  --brand-ink: #F7A8D4;
  --orange: #F5695A;
  --coral: #FF9EC7;

  --nav-off: rgba(245, 220, 240, .58);
  --nav-on-bg: rgba(241, 154, 200, .17);
  --pill-bg: rgba(241, 154, 200, .14);
  --chip-off: var(--tint);
  --chip-off-fg: #DCC4E4;

  --ok: #45D68C;
  --busy: #FFB648;
  --off: #9C879A;
  --heart-off: rgba(255, 243, 238, .4);
  --warn: #D97538;          /* #B45309 is 3.74 on --bg here; this is >= 4.62 on bg/card/tint */
  --check-ring: #75607D;    /* 3:1 on --card */
  --on-brand: #1F0E2B;      /* dark --brand is #F19AC8: white on it is 2.06 */
  --scrim-hero: linear-gradient(transparent, transparent);   /* the dark hero passes as drawn */
  --scrim-hero-phone: linear-gradient(180deg, rgba(158, 27, 46, .26) 0px, rgba(158, 27, 46, .20) 100px, rgba(158, 27, 46, 0) 200px);
  --scrim-band: linear-gradient(105deg, rgba(158, 27, 46, .24) 0%, rgba(158, 27, 46, .16) 45%, rgba(158, 27, 46, 0) 80%);

  /* --violet stays #8E45E8 in the source, but it is also this site's focus ring,
     and at #8E45E8 that ring falls to about 3:1 on the dark ground. Kept lighter
     here so keyboard focus stays visible; the source has no focus states at all. */
  --violet: #A97BFF;

  --grad-hero: linear-gradient(160deg, #F5695A 0%, #C13584 34%, #7B2FA8 68%, #2E1038 100%);

  --sh-card: 0 8px 20px -12px rgba(0, 0, 0, .55);
  --sh-lift: 0 14px 30px -14px rgba(0, 0, 0, .7);
  --sh-panel: 0 22px 50px -24px rgba(0, 0, 0, .65);
}

/* --------------------------------------------------------- 3. reset + base */

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

/* Why `background-origin: border-box` appears on every rule below that paints a
   gradient, and why it is not one line up here in the reset.

   CSS defaults background-origin to padding-box while background-clip stays
   border-box and background-repeat stays repeat. Give an element a gradient AND
   a border — every button here carries `border: 1px solid transparent`, which is
   what makes a primary and a secondary button the same size — and the gradient
   tile is laid out 2px smaller than the area it is painted into. The 1px ring is
   then filled by TILING it: the left edge gets the gradient's RIGHT end and the
   right edge gets its LEFT end.

   On the 1202px ค้นหา button that is a hairline of orange down the violet end,
   widest across the 14px corner arcs — which is what reads as colour spilling
   out past the shape. Scaling the real button 8x on the live site shows the
   orange strip plainly; the source comp writes `border:none` on its gradient
   buttons and never meets it.

   It cannot live on `*`, and that was tried first: a `background:` SHORTHAND
   resets every background longhand to its initial value, so
   `.btn { background: var(--card) }` puts the origin straight back to
   padding-box, and outranks a universal selector while doing it. The
   declaration has to sit in the rule that paints, after the paint.

   Paint-only. It can move nothing, and where there is no border the padding box
   and the border box are the same rectangle, so it is a no-op. */


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

body {
  /* The source sets this on body. It changes nothing on Windows, where the site
     is mostly read, but on macOS it is the difference between the comp's stroke
     weight and a heavier one — and the owner asked for the comp. */
  -webkit-font-smoothing: antialiased;
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 18px;
  line-height: 1.75;
  font-weight: 400;
  hyphens: none;
  overflow-wrap: break-word;
}

img,
svg,
video { max-width: 100%; }

img { height: auto; display: block; }

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

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.45;          /* Thai stacks vowels and tone marks: never below 1.4 */
  letter-spacing: -.005em;
  margin: 0 0 8px;
  text-wrap: balance;
  /* Thai has dictionary line-breaking in Chrome and Safari: never break mid-word */
  word-break: normal;
  overflow-wrap: normal;
}

h1 { font-size: 28.5px; letter-spacing: -.01em; }
h2 { font-size: 24px; }
h3 { font-size: 20px; }
h4 { font-size: 18.5px; font-family: var(--font-ui); font-weight: 600; }

p { margin: 0 0 12px; text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0 0 12px; padding-left: 20px; }

strong, b { font-weight: 700; }

small { font-size: 15.5px; }

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

/* Must sit above every component rule: any later `display:` would otherwise beat
   the UA default and strand collapsed panels open. (spec 5 + 11.2) */
[hidden] { display: none !important; }

/* A control only a script can work is not drawn when there is no script.
   <html> ships with class="no-js" and site.js swaps it for "js" before paint,
   so this hides nothing for anybody whose browser runs the file. What it does
   cover is the visitor who has scripting off, or whose network ate site.js.

   What carries it, and why each one has no no-script answer:

     .contact-open     profile.php's two ติดต่อ LINE buttons — <button
                       type="button"> whose whole behaviour is in site.js.
                       Each has a <noscript> beside it with a real LINE link,
                       or the reason there isn't one.
     .card-fav         the heart on every card and on a profile. Favourites
                       live in this browser's localStorage and only site.js
                       ever writes them, so without it there is nothing a tap
                       could do.
     .saved-link       the header's heart, and .account-stat's รายการโปรด
                       tile — working links, but to a list that can never hold
                       anything without the script that fills it.
     .theme-toggle     the header, the phone bar, the account row and both
                       admin shells. Nothing binds it without a script and
                       nothing can: dark is driven only by
                       :root[data-theme="dark"], and data-theme is written only
                       by applyTheme() in site.js. So a no-script visitor sees
                       the light theme and cannot change it — which was already
                       true before this rule existed, because the switch never
                       worked for them. Hiding it takes away a dead control,
                       not a capability.
                       (An earlier version of this comment claimed the
                       stylesheet follows prefers-color-scheme. It does not —
                       there is no such media query anywhere in this file. Real
                       support for it would make the hiding genuinely lossless,
                       and is worth doing; it is not done.)

   NOTE on what this does and does not cover. It covers scripting being OFF,
   where <noscript> renders the replacement. It does NOT cover scripting being
   ON while site.js fails to arrive or throws: there <html> keeps no-js, so the
   controls hide, and <noscript> stays empty because scripting is enabled. That
   visitor gets no contact button — but they got a dead one before, so nothing
   they could use has been lost. The real answer for that case is to make the
   control a real <a href> that script upgrades, which is worth doing for
   .contact-open once a profile has a LINE link to point at.

   Everything else on the site either works without script or degrades to
   something that does: the sort control falls back to a <select>, ใกล้ฉัน to a
   link to /location.php, the room's composer to a plain form post. */
.no-js .js-only { display: none !important; }

/* ------------------------------------------------------- 4. focus + a11y */

:focus-visible {
  outline: 3px solid var(--violet);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px rgba(255,255,255,.9);
}

/* One class lighter than it looks, and deliberately so.

   `:root[data-theme="dark"] :focus-visible` is (0,3,0), which outranks every
   component rule that had already decided what focus should look like on it —
   all of them (0,2,x). Three things were losing to it in the dark theme:

     .search-field input:focus-visible { box-shadow: none }   site.css:2810
         The ring belongs to the whole field, not the inner input (the comment
         there says so). Overruled, the input drew a 5px rgba(8,4,10,.92) ring
         INSIDE the field's own pink one — a black frame around the search bar,
         which is what the owner reported. Measured on a real click, dark theme:
         inputBoxShadow: rgba(8, 4, 10, 0.92) 0px 0px 0px 5px.

     .hero / .mobile-topbar / .btn-search / .search-submit :focus-visible
         "on a gradient the violet ring loses; flip ring and halo" — the flip
         sets a plum halo, and in dark mode this rule put the black one back,
         undoing half of it.

     .chip input:focus-visible { box-shadow: none }
         Harmless only by accident: that input is opacity:0, so the ring it was
         given could not be seen.

   Dropping :root leaves (0,2,0) — still above the base :focus-visible (0,1,0),
   which is the only thing it has to beat — so every one of those rules now
   decides its own focus again. The blast radius was enumerated over every
   :focus-visible rule in this file before the change: the rules that start
   winning are exactly the ones above, and each of them declares what it wants
   instead. Nothing is left without an indicator. */
[data-theme="dark"] :focus-visible { box-shadow: 0 0 0 5px rgba(8,4,10,.92); }

/* on a gradient the violet ring loses; flip ring and halo */
.hero :focus-visible,
.mobile-topbar :focus-visible,
.btn-search:focus-visible,
.search-submit:focus-visible {
  outline-color: #FFFFFF;
  box-shadow: 0 0 0 5px rgba(62,18,48,.6);
}

/* the chip's real input is 1px and invisible, so the ring is drawn on its label */
.chip input:focus-visible + span {
  outline: 3px solid var(--violet);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px rgba(255,255,255,.9);
}
:root[data-theme="dark"] .chip input:focus-visible + span { box-shadow: 0 0 0 5px rgba(8,4,10,.92); }

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

/* --------------------------------------------------------- 5. utilities */

.shell,
.container,
.page-shell,
.main-container {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.icon {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  vertical-align: -.18em;
}

.section-kicker,
.eyebrow,
.hero-eyebrow {
  display: inline-block;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--brand-ink);
  line-height: 1.5;
}

.text-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  color: var(--brand-ink);
  font-weight: 600;
  text-decoration: none;
  border-radius: var(--r-pill);
}
.text-link:hover { text-decoration: underline; }

.multiline { white-space: pre-line; }

.small-note {
  font-size: 15.5px;
  line-height: 1.7;
  color: var(--muted);
  margin: 0;
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 10px;
  border-radius: var(--r-micro);
  background: var(--tint);
  color: var(--brand-ink);
  font-size: 17px;
  font-weight: 700;
  line-height: 1.6;
  white-space: nowrap;
}
.badge-active { background: rgba(52,199,123,.16); color: #0B7143; }   /* 4.10 -> 4.66 on a tinted row */
:root[data-theme="dark"] .badge-active { color: #6FE0A5; }

/* --------------------------------------------- 6. buttons + form controls */

.btn,
.button,
.btn-search,
.btn-clear,
.search-submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 46px;
  padding: 10px 20px;
  border: 1px solid transparent;
  border-radius: var(--r-btn);
  background: var(--card);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 18px;
  font-weight: 600;
  line-height: 1.4;
  text-decoration: none;
  cursor: pointer;
  transition: transform .18s var(--ease-rise), box-shadow .18s ease, background-color .18s ease, border-color .18s ease;
}

.btn:active,
.button:active,
.btn-search:active,
.btn-clear:active { transform: translateY(1px); }

.btn:disabled,
.button:disabled,
.btn-search:disabled,
.btn-clear:disabled { opacity: .55; cursor: not-allowed; transform: none; }

/* primary: gradient with its own scrim so white copy holds 4.5:1 over the orange end */
.btn-primary,
.button-primary,
.btn-search,
.search-submit {
  /* No text-shadow and no stroke: both soften Thai glyph edges. The label relies on
     synthetic bold plus the light scrim baked into --grad-action-safe. */
  background-color: #B8318A;
  background-image: var(--grad-action-safe);
  background-origin: border-box;
  color: #FFFFFF;
  border-color: transparent;
  box-shadow: var(--sh-card);
}
.btn-primary:hover,
.button-primary:hover,
.btn-search:hover,
.search-submit:hover { box-shadow: var(--sh-lift); }

/* secondary / clear: white with a hairline */
.btn-clear,
.btn-ghost,
.button-secondary,
.button-filter {
  background: var(--card);
  color: var(--ink);
  border-color: var(--border);
}
.btn-clear:hover,
.btn-ghost:hover,
.button-secondary:hover,
.button-filter:hover { background: var(--tint); border-color: var(--tint); }

.button-small { min-height: 40px; padding: 8px 14px; font-size: 16.5px; }

.btn-line,
.button-line {
  background: var(--line);
  color: #FFFFFF;
  border-color: transparent;
}
/* --line is not redefined in dark, so the fill stays #047C36 and white holds
   5.32 on it; the old dark-green text was 2.91 */
:root[data-theme="dark"] .btn-line,
:root[data-theme="dark"] .button-line { color: #FFFFFF; }

/* round icon buttons */
.icon-btn,
.icon-button,
.avatar-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 40px;
  height: 40px;
  min-width: 40px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--r-nav);
  background: var(--tint);
  color: var(--brand-ink);
  cursor: pointer;
  transition: background-color .18s ease, color .18s ease, transform .18s var(--ease-rise);
}
.icon-btn .icon,
.icon-button .icon { width: 20px; height: 20px; }
.icon-btn:hover,
.icon-button:hover { background: var(--brand); color: var(--on-brand); }
.icon-btn:active,
.icon-button:active { transform: translateY(1px); }

.icon-btn .badge-count,
.badge-count {
  position: absolute;
  top: -3px;
  right: -3px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--r-micro);
  background: var(--brand);
  color: #FFFFFF;
  font-size: 13.5px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.avatar-btn {
  background-image: var(--grad-mark);
  background-origin: border-box;
  color: #FFFFFF;
  font-weight: 700;
  overflow: hidden;
}
.avatar-btn img { width: 100%; height: 100%; object-fit: cover; }

/* field: label span + control */
.field {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
  font-size: 16px;
}
.field > span {
  font-weight: 600;
  color: var(--ink-soft);
  line-height: 1.6;
}
/* 17, the note size, not 15.5 — 24 ก.ย. 2569.
   This is the base every form's help line falls back to, and it sat under the
   owner's own 16px floor. Six places had already bought their way out of it
   one page at a time (.login-panel, .chat-me-panel, .pricing-page, .admin-main,
   and the apply/report pair), which is what a wrong default looks like: it gets
   patched where somebody happened to look.
   billing.php had never been patched, so its payment instructions and its
   "this link is yours alone" warning were the smallest text on the one page
   where somebody is about to move money. Fixing the default covers it and
   anything added later, instead of waiting for a seventh override.
   What the surviving overrides actually do, now that the base is 17: the
   apply/report pair really does want more, at 17.5. .admin-main deliberately
   holds 16 — at the floor, not under it — for a dense back-office table.
   .login-panel, .login-form and .chat-me-panel repeat 17, which is the base,
   but they are NOT redundant: each also pins a line-height of 1.55-1.6 against
   this rule's 1.7, and .login-panel a margin: 0. Only the size is duplicated
   there, so they stay. */
.field small,
.form-help {
  font-size: 17px;
  line-height: 1.7;
  color: var(--muted);
}

/* `input` with no type attribute defaults to text and must be styled too —
   several admin fields are written that way, and without this they rendered
   as bare UA underlines next to properly shaped number fields. */
input:not([type]),
input[type="text"],
input[type="search"],
input[type="number"],
input[type="password"],
input[type="email"],
input[type="tel"],
input[type="url"],
input[type="date"],
input[type="time"],
select,
textarea {
  width: 100%;
  min-width: 0;
  min-height: 46px;
  padding: 11px 14px;
  border: 1px solid var(--border);
  border-radius: var(--r-field);
  background: var(--tint);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 18px;
  line-height: 1.6;
}
textarea { min-height: 110px; line-height: 1.75; resize: vertical; }
input::placeholder,
textarea::placeholder { color: var(--muted); opacity: 1; }
input:hover,
select:hover,
textarea:hover { border-color: rgba(214,64,143,.35); }

select {
  appearance: none;
  padding-right: 36px;
  cursor: pointer;
}
.field:has(select)::after,
.sort-pill::after {
  content: "";
  position: absolute;
  right: 16px;
  bottom: 19px;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--ink-soft);
  border-bottom: 2px solid var(--ink-soft);
  transform: rotate(45deg);
  pointer-events: none;
}
/* นับจากขอบ "บน" ของ .field ไม่ใช่ขอบล่าง

   .field เป็น flex คอลัมน์: ป้าย > ตัวควบคุม > คำอธิบาย  การวางคาเร็ตด้วย bottom
   จึงวัดจากใต้คำอธิบาย ไม่ใช่ใต้ช่องเลือก ช่องไหนมีคำอธิบายคาเร็ตจะตกไปอยู่กลาง
   บรรทัดคำอธิบาย วัดที่หน้าสมัครจริง: เยื้องต่ำไป 32px ทั้งช่องจังหวัดและอำเภอ
   สูตรนี้ขยับตามขนาดตัวอักษรไปเอง */
.field:has(select)::after {
  top: calc(1.6em + 6px + 21px);
  bottom: auto;
}
/* 24 ก.ย. 2569: ไม่มีคาเร็ตในแถบค้นหา  ช่องจังหวัดและอำเภอที่นั่นไม่มีป้ายที่มองเห็น
   (ป้ายเป็น sr-only) สูตรข้างบนบวกความสูงป้ายเข้าไปด้วย คาเร็ตเลยไปตกที่ขอบล่าง
   ของช่องพอดี (วัดได้ top 52.6px บนช่องสูง 53px) เป็นขีดโผล่ใต้กรอบ เจ้าของเห็นแล้ว
   สั่งเอาออก ข้อความในช่อง (ทุกจังหวัด / ทุกอำเภอ) บอกอยู่แล้วว่าเป็นตัวเลือก
   ช่องว่างขวา 36px ที่กันไว้ให้คาเร็ตก็คืนกลับเท่าขอบซ้าย */
.search-side .field:has(select)::after { content: none; }
.search-side select { padding-right: 14px; }
/* -6px was half of the 8px box this started as, plus a fudge. Section 33 later
   shrank the box to 7px and the offset stayed, which left the caret 2.5px below
   the middle of a 30px pill — visible, and the owner saw it. Half the box is
   what centres it: bottom:50% puts the box's BOTTOM edge at the middle, so the
   margin has to lift it by half its height and no more. */
.sort-pill::after { bottom: 50%; margin-bottom: -3.5px; right: 14px; }

input[type="file"] {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 10px;
  background: var(--tint);
  font-size: 16.5px;
  color: var(--ink-soft);
  cursor: pointer;
}
/* the native button is the one control a browser draws in its own world;
   dress it as a small secondary pill so the row reads like every other field */
input[type="file"]::file-selector-button {
  min-height: 34px;
  margin-right: 12px;
  padding: 6px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-btn);
  background: var(--card);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 16.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease;
}
input[type="file"]::file-selector-button:hover {
  background: var(--brand);
  border-color: transparent;
  color: var(--on-brand);
}
input[type="checkbox"],
input[type="radio"] {
  width: 20px;
  height: 20px;
  accent-color: var(--brand);
  cursor: pointer;
}

.check-label {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  font-size: 17px;
  font-weight: 500;
  color: var(--ink-soft);
  cursor: pointer;
}

.form-two {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
}

/* ------------------------------------------------- 7. shell: mobile topbar */

/* Phone header is TRANSPARENT and sits on the hero gradient: the hero is pulled up
   underneath it, and the bar stays positioned so it still paints on top.  The
   default carries the gradient itself, so a page with no hero (profile, guide) and
   any browser without :has() still shows white ink on colour, never on white. */
.mobile-topbar {
  position: relative;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  min-height: var(--topbar-h);
  padding: 8px max(var(--gutter), calc((100% - var(--shell)) / 2));
  background-color: #D93C92;
  background-image: var(--scrim-hero), var(--grad-hero);
  background-origin: border-box;
  color: #FFFFFF;
}

body:has(.hero) .mobile-topbar {
  background-color: transparent;
  background-image: none;
}

/* pull the hero up under the transparent bar */
body:has(.mobile-topbar) .hero { margin-top: calc(-1 * var(--topbar-h)); }

.mobile-topbar .brand,
.mobile-topbar .brand-text,
.mobile-topbar .brand-text strong,
.mobile-topbar .brand-text small { color: #FFFFFF; }
.mobile-topbar .brand-text small { color: rgba(255,255,255,.92); }

.mobile-topbar .icon-btn,
.mobile-topbar .icon-button {
  background: rgba(255,255,255,.2);
  border-color: rgba(255,255,255,.38);
  color: #FFFFFF;
}
.mobile-topbar .icon-btn:hover,
.mobile-topbar .icon-button:hover { background: rgba(255,255,255,.34); }
/* Four 40px buttons plus gaps overrun a 390px phone and clip the last one (TH).
   Shrink the row on the narrowest phones rather than letting it spill. */
@media (max-width: 430px) {
  .mobile-topbar .header-actions { gap: 6px; }
  .mobile-topbar .icon-btn,
  .mobile-topbar .icon-button { width: 36px; height: 36px; }
  .mobile-topbar .icon-btn .icon { width: 18px; height: 18px; }
}
@media (max-width: 359px) {
  .mobile-topbar .header-actions { gap: 4px; }
  .mobile-topbar .icon-btn,
  .mobile-topbar .icon-button { width: 33px; height: 33px; }
}
/* The bar sits on the same gradient in both themes, so this pip is hardcoded on
   both sides: var(--brand-ink) becomes #FF9BC9 under [data-theme=dark] and would
   sit at 1.95:1 on the literal white below it.  #B02F74 on white is 6.0:1. */
.mobile-topbar .badge-count {
  background: #FFFFFF;
  color: #B02F74;
}

/* ------------------------------------------------- 8. shell: white header */

:root { --header-h: 71px; }   /* what .search-side has to stay clear of */
.site-header {
  display: none;              /* phone uses .mobile-topbar */
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--card);
  border-bottom: 1px solid var(--border);
}

.header-inner {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 16px;
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding: 10px var(--gutter);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  color: var(--ink);
  text-decoration: none;
  border-radius: var(--r-tile);
}

.brand-mark,
.brand-symbol {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: var(--r-tile);
  background-image: var(--grad-mark);
  background-origin: border-box;
  color: #FFFFFF;
  box-shadow: var(--sh-card);
}
.brand-mark .icon,
.brand-symbol .icon { width: 21px; height: 21px; }

.brand-text,
.brand > span {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.35;
}
.brand-text strong,
.brand > span strong {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -.01em;
  line-height: 1.45;
}
.brand-text small,
.brand > span small {
  font-size: 14.5px;
  color: var(--muted);
  line-height: 1.5;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The white header shows the name only, at every width. This used to be hidden
   from 1024 up and left showing at 760-1023, where it cost nothing while the
   tagline was ค้นหาหมอนวด · นวดแผนไทย. v97's four-category tagline is twice
   that, and nowrap with a min-content-sized brand squeezed .main-nav instead:
   at 768, signed in, each link got 45-52px for a 72-91px label and หน้าแรก
   read "หน้". The phone's .mobile-topbar is a sibling header, not inside this
   one, and keeps its own tagline rules. */
.site-header .brand-text small { display: none; }

.main-nav,
.desktop-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-width: 0;
}
.main-nav a,
.desktop-nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 40px;
  padding: 8px 14px;
  border-radius: var(--r-nav);
  color: var(--ink-soft);
  font-size: 17.5px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color .18s ease, color .18s ease;
}
.main-nav a:hover,
.desktop-nav a:hover { background: var(--tint); color: var(--brand-ink); }
.main-nav a.active,
.desktop-nav a.active { background: var(--tint); color: var(--brand-ink); }

.nav-dot {
  position: absolute;
  top: 6px;
  right: 8px;
  width: 7px;
  height: 7px;
  border-radius: var(--r-pill);
  background: var(--brand);
}

.header-actions,
.header-tools {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}

.language-label {
  font-size: 15.5px;
  font-weight: 700;
  color: var(--muted);
  letter-spacing: .06em;
}

/* ------------------------------------------------- 9. shell: bottom nav */

.bottom-nav,
.mobile-nav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  align-items: stretch;
  gap: 2px;
  padding: 6px 6px max(6px, env(safe-area-inset-bottom));
  background: var(--card);
  border-top: 1px solid var(--border);
  box-shadow: 0 -6px 20px rgba(140,45,95,.07);
}

.bottom-nav a,
.mobile-nav a {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 50px;
  padding: 2px;
  border-radius: var(--r-tile);
  color: var(--muted);
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.5;
  text-decoration: none;
  text-align: center;
}
.bottom-nav a span,
.mobile-nav a span {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* the tinted rounded square lives behind the icon, not behind the whole item */
.bottom-nav a .icon,
.bottom-nav a .nav-icon,
.mobile-nav a .icon {
  width: 34px;
  height: 34px;
  padding: 6px;
  border-radius: var(--r-tile);
  background: transparent;
  transition: background-color .18s ease, color .18s ease;
}
.bottom-nav a.active,
.mobile-nav a.active { color: var(--brand-ink); }
.bottom-nav a.active .icon,
.bottom-nav a.active .nav-icon,
.mobile-nav a.active .icon { background: var(--tint); color: var(--brand-ink); }

/* reserve room so the fixed bar never covers the last row of cards */
body:has(.bottom-nav),
body:has(.mobile-nav) { padding-bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom)); }

/* ------------------------------------------------------- 10. hero + search */

.hero {
  position: relative;
  padding: calc(var(--topbar-h) + 24px) 0 calc(var(--overlap) + 36px);
  background-color: #D93C92;
  /* scrim baked in as a second background layer, NOT a ::before overlay: text drawn
     over a translucent sibling layer loses subpixel antialiasing and looks soft. */
  background-image: var(--scrim-hero), var(--grad-hero);
  background-origin: border-box;
  color: #FFFFFF;
}

.hero-inner {
  width: 100%;
  /* Deliberately NOT shell-constrained: in every comp the headline sits against the
     left edge of the gradient while the panel below it stays centred. */
  padding-inline: var(--gutter);
}

.hero h1 {
  margin: 8px 0 10px;
  font-size: var(--t-hero);
  line-height: 1.45;
  letter-spacing: -.01em;
  color: #FFFFFF;
  max-width: 18ch;
  /* No text-shadow: the .30 scrim already clears 3:1 at this size and a shadow only
     softens the Thai strokes. The comps render this headline perfectly crisp. */
}
.hero p {
  max-width: 44ch;
  color: rgba(255,255,255,.92);
  font-size: 18px;
  line-height: 1.75;
}
.hero .section-kicker,
.hero .eyebrow,
.hero-eyebrow { color: rgba(255,255,255,.92); }

/* --- search panel ------------------------------------------------------- */

.search-panel {
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: calc(-1 * var(--overlap));
  padding: 14px;
  border-radius: var(--r-panel);
  background: var(--card);
  box-shadow: var(--sh-panel);
  border: 1px solid var(--border);
}

.search-field,
.search-input {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 50px;
  padding: 6px 8px 6px 16px;
  border-radius: var(--r-field);
  background: var(--tint);
  border: 1px solid transparent;
  color: var(--ink);
  min-width: 0;
}
.search-field:focus-within,
.search-input:focus-within { border-color: rgba(214,64,143,.4); }
.search-field > .icon,
.search-input > .icon {
  width: 20px;
  height: 20px;
  color: var(--brand);
  flex: none;
}
.search-field input,
.search-input input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 36px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: 18px;
}
.search-field input:hover,
.search-input input:hover { border-color: transparent; }
.search-field input:focus,
.search-input input:focus { outline: none; }
.search-field input:focus-visible,
.search-input input:focus-visible { outline: 3px solid var(--violet); outline-offset: 1px; }

.search-near,
.search-location {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  margin-left: auto;
  padding: 6px 12px 6px 12px;
  border-left: 1px solid rgba(140,45,95,.18);
  color: var(--brand-ink);
  font-size: 15.5px;
  font-weight: 600;
  white-space: nowrap;
}
.search-near .icon,
.search-location .icon { width: 16px; height: 16px; }

/* --- chip row ----------------------------------------------------------- */

/* min-width:0 is structural: a <fieldset> carries a UA min-inline-size:min-content
   that silently defeats overflow-x and widens the whole page. */
.chip-row,
.district-chips {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-inline-size: 0;
  margin: 0;
  padding: 4px 2px;
  border: 0;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  -webkit-mask-image: linear-gradient(90deg, #000 0, #000 92%, transparent 100%);
  mask-image: linear-gradient(90deg, #000 0, #000 92%, transparent 100%);
}
.chip-row::-webkit-scrollbar,
.district-chips::-webkit-scrollbar { display: none; }
.chip-row legend,
.district-chips legend { padding: 0; }

.chip {
  position: relative;      /* keeps the 1px input anchored inside the chip */
  display: inline-flex;
  flex: none;
  min-width: 0;
  cursor: pointer;
}
.chip input {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  appearance: none;
  opacity: 0;
  pointer-events: none;
}
/* the ring belongs on the label, not on the 1px input */
.chip input:focus-visible { outline: none; box-shadow: none; }
.chip span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 8px 16px;
  border: 1px solid transparent;
  border-radius: var(--r-chip);
  background: var(--tint);
  color: var(--brand-ink);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
  transition: background-color .18s ease, color .18s ease, transform .18s var(--ease-rise);
}
.chip:hover span { background: rgba(214,64,143,.16); }
.chip input:checked + span {
  background-color: #B8318A;
  background-image: var(--grad-action-safe);
  background-origin: border-box;
  color: #FFFFFF;
  box-shadow: var(--sh-card);
}

/* --- advanced filters ---------------------------------------------------- */

/* Rendered open by PHP; JS collapses it. [hidden] above wins over this display. */
.filters,
.advanced-filters {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  padding: 14px;
  border-radius: var(--r-card);
  background: var(--bg);
  border: 1px solid var(--border);
}

.search-buttons,
.search-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}
.btn-search,
.search-submit { flex: 1 1 auto; }
.btn-clear { flex: 0 0 auto; min-width: 96px; }

.filter-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--brand);
}

/* ------------------------------------------------------------ 11. results */

.results-bar,
.results-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin: 24px 0 12px;
}
.results-bar h2,
.results-heading h2 { margin: 0; font-size: 24px; }
.results-bar > div,
.results-heading > div { min-width: 0; }

.results-count {
  font-family: var(--font-ui);
  font-size: 17px;
  font-weight: 500;
  color: var(--muted);
  line-height: 1.6;
}
.results-count strong {
  font-family: var(--font-display);
  font-size: 24px;
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  margin-right: 4px;
}

.sort-pill,
.sort-control {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 0 8px 0 14px;
  border-radius: var(--r-field);
  background: var(--tint);
  color: var(--brand-ink);
  font-size: 16px;
  font-weight: 600;
}
.sort-pill .icon,
.sort-control .icon { width: 16px; height: 16px; }
.sort-pill select,
.sort-control select {
  min-height: 40px;
  padding: 0 28px 0 4px;
  border: 0;
  background: transparent;
  color: inherit;
  font-size: 16px;
  font-weight: 600;
  width: auto;
}

/* --- grid --------------------------------------------------------------- */

.card-grid,
.provider-grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
  gap: var(--grid-gap);
  margin: 0 0 24px;
  padding: 0;
  list-style: none;
}

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

.provider-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  border-radius: var(--r-card);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
  overflow: hidden;
  transition: transform .22s var(--ease-rise), box-shadow .22s ease;
}
.provider-card:hover { transform: translateY(-3px); box-shadow: var(--sh-lift); }
.provider-card a { color: inherit; text-decoration: none; }
.provider-card a:hover .card-name,
a.card-name:hover { color: var(--brand-ink); }

.card-media,
.portrait {
  position: relative;
  aspect-ratio: 3 / 4;
  width: 100%;
  overflow: hidden;
  background-color: #E8A6BE;
  background-image: var(--pf-0);
  background-origin: border-box;
  background-size: cover;
}
.card-media > img,
.portrait > img,
.portrait-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
}

/* portrait field variants — class, legacy class and data-attribute forms, because
   the markup for these is authored in a sibling file */
.pf-0, .portrait-0, [data-pf="0"] { background-image: var(--pf-0); }
.pf-1, .portrait-1, [data-pf="1"] { background-image: var(--pf-1); }
.pf-2, .portrait-2, [data-pf="2"] { background-image: var(--pf-2); }
.pf-3, .portrait-3, [data-pf="3"] { background-image: var(--pf-3); }
.pf-4, .portrait-4, [data-pf="4"] { background-image: var(--pf-4); }
.pf-5, .portrait-5, [data-pf="5"] { background-image: var(--pf-5); }

/* decorative glyph inside an outlined circle */
.card-glyph,
.portrait-placeholder,
.portrait-emblem {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  /* the comp sizes this ring at ~32% of the card width, so it scales with the column
     count instead of staying a fixed 56px dot on a 250px card. */
  width: clamp(56px, 32%, 96px);
  aspect-ratio: 1;
  height: auto;
  border-radius: var(--r-pill);
  border: 1px solid rgba(255,255,255,.4);
  color: rgba(255,255,255,.55);
  pointer-events: none;
}
.card-glyph .icon { width: 44%; height: 44%; }
.portrait-placeholder {
  flex-direction: column;
  gap: 2px;
  width: auto;
  height: auto;
  padding: 14px 18px;
  color: rgba(255,255,255,.88);
  text-align: center;
}
.portrait-placeholder span {
  font-family: var(--font-display);
  font-size: 28.5px;
  font-weight: 700;
  line-height: 1.45;
}
.portrait-placeholder small { font-size: 14.5px; color: rgba(255,255,255,.88); }

/* full-card hit area, under the buttons */
.card-link,
.photo-hitbox {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: 0;
}

/* verified pill — demo rows only (spec 7); never rendered for a real profile */
/* Both badges are painted OVER .card-link, which is the card's whole hitbox, and
   the card keeps cursor:pointer underneath them — so a click on the ยืนยันแล้ว or
   โปรโมท pill was swallowed and the profile did not open, while the hand cursor
   promised it would. Neither badge is interactive; they are labels. They are
   also why this was invisible on the CARDS until now: no profile is verified and
   none was promoted, so neither badge renders in a grid today. It becomes real
   the first time the owner ticks ยืนยันตัวตนแล้ว or buys a slot — which is now
   possible again, since the promote buttons work.
   .card-verify is NOT dormant everywhere, though: profile.php renders one in the
   hero bar on every profile page, where it is position:static and carries text.
   pointer-events:none there costs a long-press or a text selection on a label
   nobody needs to select, which is a fair price for the card fix. */
.card-verify,
.photo-label {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 2;
  pointer-events: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: var(--r-nav);
  background: rgba(255,255,255,.94);
  color: var(--brand-ink);
  font-size: 14px;
  font-weight: 700;
  line-height: 1.6;
  white-space: nowrap;
  box-shadow: 0 2px 8px rgba(80,25,55,.18);
}
.card-verify .icon,
.photo-label .icon { width: 13px; height: 13px; }

.card-fav,
.favorite-button {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: rgba(255,255,255,.94);
  color: var(--ink-soft);
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(80,25,55,.18);
  transition: transform .18s var(--ease-rise), color .18s ease, background-color .18s ease;
}
.card-fav .icon,
.favorite-button .icon { width: 19px; height: 19px; }
/* profile.php also hangs `icon-btn icon-button` on this button, and that hover
   fills the ground with var(--brand) at the same (0,2,0) specificity — magenta
   icon on a magenta ground.  Restate the white ground so the fill cannot leak in.
   Dark mode is untouched: its `:root[data-theme=dark] .card-fav` is (0,3,0). */
.card-fav:hover,
.favorite-button:hover {
  transform: scale(1.06);
  color: var(--brand);
  background: rgba(255,255,255,.94);
}
.card-fav[aria-pressed="true"],
.favorite-button[aria-pressed="true"] { color: var(--coral); }
.card-fav[aria-pressed="true"] .icon,
.favorite-button[aria-pressed="true"] .icon { fill: var(--coral); }

.card-media-foot,
.photo-bottom {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 6px;
  padding: 10px;
  background: linear-gradient(to top, rgba(46,14,38,.5), transparent);
  background-origin: border-box;
  pointer-events: none;
}

.card-price,
.price-badge {
  display: inline-flex;
  align-items: baseline;
  gap: 2px;
  padding: 5px 11px;
  border-radius: var(--r-nav);
  background: rgba(46,27,43,.9);
  color: #FFFFFF;
  font-family: var(--font-display);
  font-size: 16.5px;
  font-weight: 700;
  line-height: 1.5;
  letter-spacing: -.005em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.card-price small,
.price-badge small {
  font-family: var(--font-ui);
  font-size: 13.5px;
  font-weight: 500;
  opacity: .88;
}

/* the one real status: hom_providers.availability */
.card-status,
.availability {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  background: rgba(255,255,255,.94);
  color: var(--ink);
  font-size: 14px;
  font-weight: 700;
  line-height: 1.6;
  white-space: nowrap;
}
/* One element-based dot, no pseudo-element fallback: every template that prints a
   status prints a real .dot node, so the ::before twin only ever showed up as a
   second grey dot on a browser without :has(). */
.dot {
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: var(--r-pill);
  background: var(--off);
}
.card-status .dot ~ .dot { display: none; }
/* State, in whichever form the markup carries it.  The dot's OWN data-state is
   what the profile page carries (.profile-status is not .card-status, so no
   ancestor form reaches it); the card carries data-status on the parent. */
.dot[data-state="available"],
.dot.ok,
.dot.available,
.card-status.available .dot,
.card-status.is-available .dot,
.card-status[data-state="available"] .dot,
[data-status="available"] .dot { background: var(--ok); }

.dot[data-state="appointment"],
.dot.busy,
.dot.appointment,
.card-status.appointment .dot,
.card-status.is-appointment .dot,
.card-status[data-state="appointment"] .dot,
[data-status="appointment"] .dot { background: var(--busy); }

.dot[data-state="unavailable"],
.dot.off,
.dot.unavailable,
.card-status.unavailable .dot,
.card-status.is-unavailable .dot,
.card-status[data-state="unavailable"] .dot,
[data-status="unavailable"] .dot { background: var(--off); }

.card-body,
.card-content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  padding: 12px 12px 14px;
  min-width: 0;
}

.card-name {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 18.5px;
  font-weight: 600;
  line-height: 1.45;
  letter-spacing: 0;
  color: var(--ink);
  /* Thai: dictionary line-breaking, never mid-word */
  word-break: normal;
  overflow-wrap: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  transition: color .18s ease;
}

.card-age,
.age-label {
  font-size: 15.5px;
  font-weight: 500;
  color: var(--muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* .card-name is a -webkit-box for line clamping, so the age span butts straight
   against the name ("คุณมาลี ว.32 ปี") without this separator. */
.card-age::before { content: "·"; margin: 0 5px; color: var(--border); }

.card-place,
.district-label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  background: var(--tint);
  color: var(--brand-ink);
  font-size: 14.5px;
  font-weight: 600;
  line-height: 1.6;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.card-place .icon,
.district-label .icon { width: 13px; height: 13px; flex: none; }

.card-services {
  margin: 0;
  max-width: 100%;
  font-size: 15.5px;
  line-height: 1.7;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* rating / slot row — demo rows only (spec 7) */
.card-rating {
  display: flex;
  align-items: center;
  gap: 4px;
  width: 100%;
  font-size: 15.5px;
  line-height: 1.7;
  color: var(--muted);
}
.card-rating strong {
  color: var(--ink);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.star {
  display: inline-flex;
  width: 14px;
  height: 14px;
  flex: none;
  color: var(--star);
  fill: var(--star);
}
.star .icon { width: 14px; height: 14px; fill: var(--star); }
.rating-count { color: var(--muted); font-variant-numeric: tabular-nums; }
.card-slot {
  margin-left: auto;
  padding-left: 6px;
  color: var(--brand-ink);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --------------------------------------------------- 12. notices + states */

.notice,
.demo-notice {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 0 0 16px;
  padding: 14px 16px;
  border-radius: var(--r-card);
  /* a tinted ground, not a coloured edge bar — this world speaks in pills and
     tinted fills, and a 4px accent border is the stock callout it replaces. */
  border: 1px solid color-mix(in srgb, var(--brand) 18%, var(--border));
  background: color-mix(in srgb, var(--brand) 6%, var(--card));
  color: var(--ink-soft);
  font-size: 17px;
  line-height: 1.7;
}
.notice strong,
.demo-notice strong {
  display: block;
  color: var(--ink);
  font-family: var(--font-ui);
  font-weight: 700;
}
.notice .icon,
.demo-notice .icon { width: 20px; height: 20px; color: var(--brand); flex: none; margin-top: 3px; }
.notice ul { margin: 6px 0 0; }
.notice p { margin: 4px 0 0; }
.notice span { display: block; }

.notice.warning { border-left-color: var(--warn); background: rgba(255,159,28,.08); }
.error-notice { border-left-color: var(--danger); background: rgba(196,42,85,.07); }
.success-notice { border-left-color: var(--ok); background: rgba(52,199,123,.09); }

.empty-state,
.admin-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  max-width: 520px;
  margin: 24px auto 32px;
  padding: 36px 24px;
  border-radius: var(--r-panel);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
  text-align: center;
}
.empty-state > .icon {
  width: 56px;
  height: 56px;
  padding: 14px;
  border-radius: var(--r-pill);
  background: var(--tint);
  color: var(--brand);
}
.empty-state h1,
.empty-state h3 { margin: 0; }
.empty-state p { color: var(--muted); margin: 0; }
.empty-state .btn,
.empty-state .button { margin-top: 8px; }

/* ------------------------------------------------------- 13. pagination */

.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin: 8px 0 32px;
  font-size: 17px;
}
.pagination a,
.pagination span,
.pagination button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  min-height: 40px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-nav);
  background: var(--card);
  color: var(--ink-soft);
  font-size: 17px;
  font-weight: 600;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.pagination a:hover { background: var(--tint); color: var(--brand-ink); border-color: var(--tint); }
.pagination .active,
.pagination [aria-current="page"] {
  background-color: #B8318A;
  background-image: var(--grad-action-safe);
  background-origin: border-box;
  border-color: transparent;
  color: #FFFFFF;
}
.pagination [aria-disabled="true"],
.pagination .disabled {
  opacity: .45;
  pointer-events: none;
  background: transparent;
}
.pagination .page-info,
.pagination-info {
  border: 0;
  background: transparent;
  color: var(--muted);
  font-weight: 500;
}

/* ------------------------------------------------------------ 14. toast */

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--bottomnav-h) + 16px + env(safe-area-inset-bottom));
  z-index: 60;
  /* It sits at z-index 60 over a bar at 45 and overlapped it: for the 3.2s the
     toast is up, the left 33px of the profile page's ติดต่อ LINE button belonged
     to the toast. Measured on a phone: tap the heart, then elementFromPoint 16px
     into that button returns div#toast instead of the button. It is a message,
     not a control — nothing in it is clickable — so it should never take a tap. */
  pointer-events: none;
  transform: translateX(-50%);
  max-width: min(92vw, 420px);
  padding: 12px 18px;
  border-radius: var(--r-btn);
  background: #2E1B2B;
  color: #FFFFFF;
  font-size: 16.5px;
  font-weight: 600;
  line-height: 1.6;
  text-align: center;
  box-shadow: var(--sh-panel);
  animation: hom-toast .32s var(--ease-rise) both;
}
:root[data-theme="dark"] .toast { background: #3E2B48; }

@keyframes hom-toast {
  from { opacity: .2; transform: translate(-50%, 10px); }
  to { opacity: 1; transform: translate(-50%, 0); }
}

/* ------------------------------------------------------------ 15. footer */

.site-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  max-width: var(--shell);
  margin: 32px auto 0;
  padding: 20px var(--gutter) 32px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 17px;
}
.site-footer div { display: flex; flex-direction: column; }
.site-footer strong {
  font-family: var(--font-display);
  font-size: 19px;
  color: var(--ink);
}
.site-footer a { color: var(--brand-ink); text-decoration: none; font-weight: 600; }
.site-footer a:hover { text-decoration: underline; }

/* ------------------------------------------------------------ 16. profile */

.profile-container,
.profile-page main { padding-bottom: 32px; }

.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  margin: 12px 0 4px;
  color: var(--brand-ink);
  font-size: 19px;
  font-weight: 600;
  text-decoration: none;
}
.back-link:hover { text-decoration: underline; }

.profile-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
  margin-bottom: 24px;
}

.profile-gallery {
  position: relative;
  min-width: 0;
}
.profile-gallery .portrait,
.profile-gallery .card-media,
.profile-media {
  border-radius: var(--r-panel);
  box-shadow: var(--sh-card);
  aspect-ratio: 4 / 5;
}
.profile-gallery .favorite-button,
.profile-gallery .card-fav { top: 14px; right: 14px; }

.gallery-thumbs {
  display: flex;
  gap: 8px;
  margin-top: 10px;
  overflow-x: auto;
  min-width: 0;
  scrollbar-width: none;
  padding: 2px;
}
.gallery-thumbs::-webkit-scrollbar { display: none; }
.gallery-thumb {
  flex: none;
  width: 60px;
  height: 72px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--r-tile);
  background: var(--tint);
  overflow: hidden;
  cursor: pointer;
}
.gallery-thumb img { width: 100%; height: 100%; object-fit: cover; }
.gallery-thumb.active { border-color: var(--brand); }

.profile-info { min-width: 0; }

.profile-eyebrow {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}
.profile-code {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .08em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.profile-info h1 {
  margin: 0 0 10px;
  font-size: clamp(24px, 3.4vw, 32px);
  line-height: 1.45;
  letter-spacing: -.01em;
}

.profile-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 14px;
}
.profile-pills span,
.service-tags span {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 13px;
  border-radius: var(--r-chip);
  background: var(--tint);
  color: var(--brand-ink);
  font-size: 15.5px;
  font-weight: 600;
  line-height: 1.6;
}
.service-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.service-tags .icon { width: 14px; height: 14px; }

.profile-price {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 12px;
  color: var(--muted);
  font-size: 16.5px;
}
.profile-price strong {
  font-family: var(--font-display);
  font-size: 34.5px;
  font-weight: 700;
  line-height: 1.45;
  letter-spacing: -.015em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.profile-status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 18px;
  padding: 9px 15px;
  border-radius: var(--r-chip);
  background: var(--tint);
  color: var(--ink-soft);
  font-size: 16.5px;
  font-weight: 600;
}
.profile-status .icon { width: 17px; height: 17px; color: var(--brand); }

.profile-section {
  margin: 0 0 20px;
  padding: 18px;
  border-radius: var(--r-card);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
}
.profile-section h2 {
  margin: 0 0 8px;
  font-size: 20px;
}
.profile-section p {
  color: var(--ink-soft);
  font-size: 18px;
  line-height: 1.75;
  max-width: 68ch;
}
.profile-section .icon { width: 16px; height: 16px; color: var(--brand); margin-right: 5px; }

.profile-cta {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 18px;
  border-radius: var(--r-panel);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-panel);
}
.profile-cta .btn,
.profile-cta .button { width: 100%; }
.profile-cta p { margin: 0; color: var(--muted); font-size: 17px; line-height: 1.6; text-align: center; }

.profile-bottom-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 45;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px var(--gutter) max(10px, env(safe-area-inset-bottom));
  background: var(--card);
  border-top: 1px solid var(--border);
  box-shadow: 0 -6px 20px rgba(140,45,95,.1);
}
.profile-bottom-bar div { display: flex; align-items: baseline; gap: 4px; color: var(--muted); font-size: 17px; }
.profile-bottom-bar strong {
  font-family: var(--font-display);
  font-size: 24px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
body:has(.profile-bottom-bar) { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }
body:has(.bottom-nav) .profile-bottom-bar { bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom)); }

/* -------------------------------------------------------------- 17. guide */

.guide-container { padding-block: 16px 32px; }
.guide-container h1 {
  font-size: clamp(26px, 4.4vw, 38px);
  line-height: 1.45;
  letter-spacing: -.01em;
  margin: 8px 0 10px;
}
.guide-intro {
  max-width: 62ch;
  color: var(--ink-soft);
  font-size: 18px;
  line-height: 1.75;
  margin-bottom: 24px;
}
.guide-steps {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  margin-bottom: 24px;
}
.guide-steps section {
  padding: 20px;
  border-radius: var(--r-card);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
}
.guide-steps section > span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-bottom: 10px;
  border-radius: var(--r-tile);
  /* 15px/700 is body text, not large text: the bare --grad-mark leaves white at
     3.2:1 on its coral start.  Spec 6 — every gradient carrying copy gets a scrim. */
  background-image: var(--grad-mark-safe);
  background-origin: border-box;
  color: #FFFFFF;
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.guide-steps h2 { margin: 0 0 6px; font-size: 21px; }
.guide-steps p { margin: 0; color: var(--ink-soft); font-size: 17.5px; line-height: 1.75; }

/* ----------------------------------------------------- 18. contact dialog */

.contact-dialog {
  width: min(100%, 480px);
  max-width: 100%;
  max-height: 88vh;
  padding: 0;
  border: 0;
  border-radius: var(--r-panel);
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--sh-panel);
  overflow: visible;
}
/* scoped to [open] so the closed dialog keeps the UA display:none */
.contact-dialog[open] {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 22px 20px 24px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.contact-dialog::backdrop { background: rgba(46,14,38,.55); }
.contact-dialog h2 { margin: 0; font-size: 24px; }
.contact-dialog > p { margin: 0; color: var(--muted); font-size: 17px; }
.contact-dialog form[method="dialog"] { display: flex; justify-content: flex-end; margin: 0; }
.dialog-close {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 2;
}
.dialog-brand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--r-tile);
  background-image: var(--grad-mark);
  background-origin: border-box;
  color: #FFFFFF;
}
/* profile.php puts both classes on the SAME element, so this must be a compound
   selector: as a descendant it never fired and --sh-card stayed on the tile. */
.dialog-brand.brand-mark,
.dialog-brand.brand-symbol { box-shadow: none; }
.contact-dialog .btn,
.contact-dialog .button { width: 100%; }
.contact-dialog textarea { background: var(--bg); }
/* 24 ก.ย. 2569: the profile's own dialog on the sign-in ramp — labels 19,
   controls 20 at 56px. By id, not by class: site.js buildConfirm() hands
   .contact-dialog to the confirm dialogs on the admin and billing pages too,
   and .field / select / .button are every form on the site. The caret is
   restated because its offset counts the label and the select's height, and
   both grew: 1.6em of 19px label + the 8px gap is where the select starts, and
   22.5 lands the chevron on the middle of a 56px box. */
#contact-dialog h2 { font-size: 26px; line-height: 1.35; }
#contact-dialog > .dialog-lead { font-size: 19px; line-height: 1.6; }
#contact-dialog .notice { margin: 0; font-size: 18px; line-height: 1.6; }
#contact-dialog .field { gap: 8px; font-size: 19px; }
#contact-dialog select { min-height: 56px; padding: 12px 40px 12px 16px; font-size: 20px; line-height: 1.5; }
#contact-dialog textarea { padding: 12px 16px; font-size: 19px; line-height: 1.6; }
#contact-dialog .field:has(select)::after { top: calc(1.6em + 8px + 22.5px); right: 18px; }
#contact-dialog .button { min-height: 56px; font-size: 20px; }
#contact-dialog .dialog-close { width: 44px; height: 44px; min-width: 44px; }
#contact-dialog .dialog-close .icon { width: 22px; height: 22px; }

/* -------------------------------------------------------------- 19. admin */
/* Same world as the front of the site, in a working-tool register: the warm
   gradient is spent only on the brand tile and the one primary action per
   screen, and every other surface is white on the near-white ground with the
   plum hairline. No hero — a screen that is read all day must not shout.
   The bar copies the measured front-of-site geometry exactly (full bleed, 63px
   tall, brand at 32px, actions ending 32px from the right) so moving between
   the catalogue and the admin never feels like two different products. */

.admin-page { background: var(--bg); }

.admin-header {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--card);
  border-bottom: 1px solid var(--border);
}
.admin-header-inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px 12px;
  width: 100%;
  padding: 8px var(--gutter);
}
.admin-brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  color: var(--ink);
  text-decoration: none;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 700;
}
.admin-brand .brand-text small { display: none; }

/* the one word that says which side of the site you are standing on */
.admin-tag {
  display: inline-flex;
  align-items: center;
  /* 22 คือกล่องของ 14.5 ตัวหนังสือโตขึ้นกล่องต้องโตตาม ไม่งั้นหางอักษรไทยล้น
     พื้นสีออกมา (overflow เป็น visible จึงไม่ถูกตัด แต่เห็นว่าล้น) */
  height: 26px;
  padding: 0 9px;
  border-radius: var(--r-micro);
  background: var(--tint);
  color: var(--brand-ink);
  font-family: var(--font-ui);
  font-size: 18px;
  font-weight: 700;
  white-space: nowrap;
}

/* second row on narrow shells, centred inside the bar from 1100px up.
   The strip scrolls, so it carries the right-edge fade every scrolling strip
   in this world carries, and min-width:0 so it cannot widen the page. */
.admin-nav {
  grid-column: 1 / -1;
  grid-row: 2;
  justify-content: flex-start;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
  padding: 1px;
  mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 28px), transparent);
}
.admin-nav::-webkit-scrollbar { display: none; }
.admin-nav .icon { width: 17px; height: 17px; }

/* actions stay on the bar's first row; only the nav drops to a second one */
.admin-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  grid-row: 1;
  grid-column: 2;
}
.admin-actions form { margin: 0; }

.admin-main {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding: 16px var(--gutter) 48px;
}

.admin-title {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 16px;
}
.admin-title h1 { margin: 0 0 2px; font-size: 25.5px; }
.admin-title p { margin: 0; color: var(--muted); font-size: 17px; }
.admin-title-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 14px; }

/* stat tiles: icon chip, number, label — the card language of the catalogue */
.admin-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}
.stat {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  border-radius: var(--r-card);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
}
.stat-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: var(--r-tile);
  background: var(--tint);
  color: var(--brand-ink);
}
.stat-mark .icon { width: 19px; height: 19px; }
.stat-body { min-width: 0; }
.stat-body strong {
  display: block;
  font-family: var(--font-display);
  font-size: 28.5px;
  line-height: 1.35;
  font-variant-numeric: tabular-nums;
}
.stat-body small {
  display: block;
  color: var(--muted);
  font-size: 15.5px;
  line-height: 1.55;
}
.stat-live .stat-mark { background: rgba(52,199,123,.15); color: #1B7A4B; }
.stat-hidden .stat-mark { background: rgba(138,115,131,.16); color: var(--ink-soft); }

/* the find row is a panel, the same shape as the catalogue search panel */
.admin-toolbar {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  padding: 16px;
  margin-bottom: 16px;
  border-radius: var(--r-panel);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
}
.admin-toolbar .field { min-width: 0; }
.toolbar-actions { display: flex; align-items: flex-end; gap: 8px; }
.toolbar-actions .button { flex: 1 1 auto; }

.admin-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 20px;
}
.admin-list-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border-radius: var(--r-card);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
  transition: box-shadow .18s ease, border-color .18s ease;
}
.admin-list-item:hover { box-shadow: var(--sh-lift); border-color: var(--tint); }
.admin-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 64px;
  flex: none;
  border-radius: var(--r-tile);
  background-image: var(--pf-0);
  background-origin: border-box;
  color: rgba(255, 255, 255, .8);
  overflow: hidden;
}
.admin-avatar img { width: 100%; height: 100%; object-fit: cover; }
.admin-person-info { min-width: 0; }
.admin-person-info h2 {
  /* ชื่อคนคือสิ่งเดียวที่ใช้กวาดสายตาหาแถวในหน้านี้ แต่เดิม 18.5 เท่ากับป้าย
     กำกับช่องกรอกในฟอร์ม ทั้งที่มันเป็นหัวข้อของทั้งแถว */
  font-size: 21px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 3px;
  font-family: var(--font-ui);
  font-size: 18.5px;
  font-weight: 600;
  line-height: 1.45;
}
.admin-person-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 9px;
  margin: 0;
  color: var(--muted);
  font-size: 17px;
  line-height: 1.6;
}
.admin-person-meta .sep { color: var(--muted); }
.admin-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.admin-status::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--off);
}
.admin-status.is-available::before { background: var(--ok); }
.admin-status.is-appointment::before { background: var(--busy); }
.admin-item-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.admin-item-actions form { margin: 0; }

/* the long provider form: grouped sections on the left, publish rail at right */
.admin-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
.admin-column,
.admin-rail {
  display: grid;
  gap: 16px;
  min-width: 0;
}

.admin-panel {
  padding: 20px;
  border-radius: var(--r-panel);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
}
.admin-narrow { max-width: 760px; }

.panel-head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 11px;
  margin-bottom: 15px;
}
.panel-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: var(--r-tile);
  background: var(--tint);
  color: var(--brand-ink);
}
.panel-mark .icon { width: 19px; height: 19px; }
.panel-head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 700;
  line-height: 1.45;
}
.panel-head p { margin: 1px 0 0; color: var(--muted); font-size: 15.5px; line-height: 1.6; }

.admin-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 14px;
}
.admin-grid .span-2 { grid-column: 1 / -1; }
.admin-form { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
.admin-form .full { grid-column: 1 / -1; }

.form-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

/* publish rail */
.rail-checks { display: grid; gap: 10px; margin-bottom: 14px; }
.rail-check {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--r-field);
  background: var(--bg);
  border: 1px solid var(--border);
  cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease;
}
.rail-check input { margin: 3px 0 0; }
.rail-check strong { display: block; font-size: 17px; font-weight: 600; line-height: 1.5; }
.rail-check small { display: block; color: var(--muted); font-size: 15.5px; line-height: 1.6; }
.rail-check:has(input:checked) { background: var(--tint); border-color: var(--tint); }
.admin-save { display: grid; gap: 10px; }
.admin-save .button { width: 100%; }
.admin-save .form-help { margin: 0; text-align: center; }

.upload-list {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 12px;
}
.upload-item {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px;
  border-radius: var(--r-tile);
  background: var(--bg);
  border: 1px solid var(--border);
}
.upload-item img {
  width: 96px;
  height: 116px;
  object-fit: cover;
  border-radius: 8px;
}
.upload-item .check-label { font-size: 15.5px; min-height: 40px; }

.admin-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  width: 100%;
  max-width: var(--shell);
  margin: 0 auto;
  padding: 18px var(--gutter) 28px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 17px;
}

/* 24 ก.ย. 2569: the admin sign-in now wears the customer sign-in's classes
   (.login-stage / .login-panel / .login-form, section "sign in"), so the two
   doors share one stage, card, type scale and form. The old .admin-login and
   .login-card rules are gone rather than overridden: .admin-login centred with
   justify-content, which is exactly what pushed a tall card's top off-screen,
   and it still carried the plum scrim. Only the footer is the admin's own: the
   way back to the site and the theme switch on one row, under a rule. */
.login-foot.admin-login-foot {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}

/* ------------------------------------------------- 20. motion (one moment) */


/* --------------------------------------------- 21. tablet: 760px - 1023px */

@media (min-width: 760px) {
  :root {
    --gutter: 24px;
    --grid-cols: 3;
    --grid-gap: 16px;
    --overlap: 56px;
  }

  body { font-size: 18px; }

  .mobile-topbar { display: none; }
  .bottom-nav,
  .mobile-nav { display: none; }
  body:has(.bottom-nav),
  body:has(.mobile-nav) { padding-bottom: 0; }

  .site-header { display: block; }

  /* specificity has to match the phone rule that pulled the hero up */
  body:has(.mobile-topbar) .hero { margin-top: 0; }
  .hero { padding: 56px 0 calc(var(--overlap) + 44px); }
  .hero h1 { font-size: 36.5px; }
  .hero p { font-size: 19px; }

  .search-panel { padding: 18px; gap: 14px; }
  .search-field,
  .search-input { min-height: 54px; }

  .filters,
  .advanced-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); padding: 16px; }

  .form-two { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  .profile-layout { grid-template-columns: minmax(0, 340px) minmax(0, 1fr); gap: 24px; align-items: start; }
  .profile-gallery { position: sticky; top: 84px; }
  .profile-bottom-bar { display: none; }
  body:has(.profile-bottom-bar) { padding-bottom: 0; }

  .guide-steps { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }

  .admin-form,
  .admin-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .admin-toolbar { grid-template-columns: minmax(0, 1fr) 220px auto; align-items: end; padding: 18px; }
  .admin-list-item { grid-template-columns: auto 1fr auto auto; }
  .admin-item-actions { grid-column: auto; flex-wrap: nowrap; }
  .admin-panel { padding: 24px; }
  .admin-header .icon-btn { width: 36px; height: 36px; min-width: 36px; }
  .admin-header .icon-btn .icon { width: 18px; height: 18px; }

  .toast { bottom: 24px; }

  .contact-dialog[open] { padding: 26px 24px 28px; }
}

/* ---------------------------------------------------- 22. desktop: 1024px+ */

@media (min-width: 1024px) {
  :root {
    --gutter: 24px;
    --grid-cols: 4;
    --grid-gap: 20px;
    --overlap: 72px;
  }

  .header-inner { padding: 12px var(--gutter); }
  .main-nav a,
  .desktop-nav a { padding: 8px 16px; font-size: 18px; }

  .hero { padding: 72px 0 calc(var(--overlap) + 56px); }
  .hero h1 { font-size: 42px; max-width: 20ch; }
  .hero p { font-size: 16.5px; }

  .search-panel { padding: 20px; }
  .filters,
  .advanced-filters { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  .results-bar,
  .results-heading { margin-top: 32px; }

  .profile-layout { grid-template-columns: minmax(0, 420px) minmax(0, 1fr); gap: 32px; }

  .card-body,
  .card-content { padding: 14px 14px 16px; }
}

/* ---------------------------------------------------------------------------
   Admin bar at desktop width: the same full-bleed geometry the front of the
   site was measured into, so the two shells line up glyph for glyph. The nav
   only joins the bar once brand + three items + actions fit on one row.
   ------------------------------------------------------------------------- */
@media (min-width: 1100px) {
  .admin-header-inner {
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: 12px;
    padding: 0 32px;
    min-height: 63px;
  }
  .admin-nav {
    grid-column: 2;
    grid-row: 1;
    justify-content: center;
    overflow: visible;
    mask-image: none;
  }
  .admin-actions { grid-column: 3; }
  .admin-actions { padding-right: 14px; }

  .admin-layout { grid-template-columns: minmax(0, 1fr) 330px; gap: 20px; }
  .admin-rail { position: sticky; top: 80px; }
}

/* ------------------------------------------------ 23. phone-only fixtures */

@media (max-width: 759.98px) {
  .site-footer { text-align: left; }
  .contact-dialog {
    /* bottom sheet on a phone */
    width: 100%;
    max-width: 100%;
    margin: auto auto 0;
    border-radius: var(--r-panel) var(--r-panel) 0 0;
    max-height: 92vh;
  }
  .contact-dialog[open] { padding-bottom: max(24px, env(safe-area-inset-bottom)); }
  .admin-list-item { grid-template-columns: auto 1fr; }
  .admin-list-item > .badge { justify-self: start; }

  /* The bar has room for the wordmark, its tag and two round actions and no
     more: the site link would squeeze the wordmark until "หอม" split across
     two lines, so on a phone it is carried by the admin footer instead. */
  .admin-header .button-small { display: none; }
  .admin-header .icon-btn { width: 36px; height: 36px; min-width: 36px; }
  .admin-header .icon-btn .icon { width: 18px; height: 18px; }
  .admin-brand .brand-text strong { white-space: nowrap; }
}

/* ------------------------------------------------- 24. reduced motion */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .provider-card:hover { transform: none; }
  .card-fav:hover,
  .favorite-button:hover { transform: none; }
}

/* --------------------------------------------------- 25. dark refinements */

:root[data-theme="dark"] body { background: var(--bg); }
:root[data-theme="dark"] .search-panel,
:root[data-theme="dark"] .provider-card,
:root[data-theme="dark"] .profile-section,
:root[data-theme="dark"] .profile-cta,
:root[data-theme="dark"] .guide-steps section,
:root[data-theme="dark"] .admin-panel,
:root[data-theme="dark"] .admin-toolbar,
:root[data-theme="dark"] .admin-list-item,
:root[data-theme="dark"] .stat,
:root[data-theme="dark"] .admin-stats > div,
:root[data-theme="dark"] .empty-state,
:root[data-theme="dark"] .notice,
:root[data-theme="dark"] .demo-notice { background: var(--card); border-color: var(--border); }

:root[data-theme="dark"] .card-status,
:root[data-theme="dark"] .availability,
:root[data-theme="dark"] .card-verify,
:root[data-theme="dark"] .photo-label,
:root[data-theme="dark"] .card-fav,
:root[data-theme="dark"] .favorite-button {
  background: rgba(32,22,38,.92);
  color: var(--ink);
}
:root[data-theme="dark"] .card-verify,
:root[data-theme="dark"] .photo-label { color: var(--brand-ink); }
:root[data-theme="dark"] .card-price,
:root[data-theme="dark"] .price-badge { background: rgba(10,4,12,.82); }

:root[data-theme="dark"] .site-header,
:root[data-theme="dark"] .bottom-nav,
:root[data-theme="dark"] .mobile-nav,
:root[data-theme="dark"] .admin-header,
:root[data-theme="dark"] .profile-bottom-bar { background: var(--card); }

:root[data-theme="dark"] .search-near,
:root[data-theme="dark"] .search-location { border-left-color: rgba(255,255,255,.14); }

:root[data-theme="dark"] .notice.warning { background: rgba(240,180,92,.1); }
:root[data-theme="dark"] .error-notice { background: rgba(255,124,156,.1); }
:root[data-theme="dark"] .success-notice { background: rgba(69,214,140,.1); }

/* ------------------------------------------- 26. geometry, from the source */
/* Everything below replaces the patch blocks that accumulated while the design
   was being read off screenshots. Each value here is quoted from the owner's
   .dc.html source or measured from it rendered at 1425px with PSL Kanda loaded.
   Where we deviate on purpose, the reason is stated at the rule. */

/* --- brand mark ---------------------------------------------------------- */
/* The source draws the glyph as a pure-CSS teardrop, not an icon: a square with
   three round corners, rotated 45°. It is the shape the owner chose (Q6). */
.brand-mark,
.brand-symbol {
  border-radius: var(--r-tile);
  background-image: var(--grad-mark);
  background-origin: border-box;
  box-shadow: none;
}
.brand-mark .icon,
.brand-symbol .icon { display: none; }
.brand-mark::after,
.brand-symbol::after {
  content: "";
  width: 14px;
  height: 14px;
  border-radius: 50% 50% 50% 0;
  background: #FFD9A8;
  transform: rotate(-45deg);
}

/* --- account chip -------------------------------------------------------- */
/* Round, and filled with the avatar gradient — not the flat crimson that was
   read off a screenshot. No accounts exist yet, so it stays inert. */
.avatar-btn {
  border-radius: 50%;
  background-image: var(--grad-avatar);
  background-origin: border-box;
  background-color: #C13584;
  border-color: transparent;
  color: var(--on-grad);
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 600;
}
.avatar-btn:hover { background-image: var(--grad-avatar); box-shadow: var(--sh-lift); }
.mobile-topbar .avatar-btn { background: rgba(255, 255, 255, .22); background-image: none; }

/* --- filter toggle beside the chip row ----------------------------------- */
.chip-line { display: flex; align-items: center; gap: 8px; min-width: 0; }
.chip-line .chip-row { flex: 1 1 auto; min-width: 0; margin: 0; }
.chip-toggle {
  flex: none;
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 15px;
  border: 1px solid var(--border);
  border-radius: var(--r-chip);
  background: var(--card);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 18px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: background .18s var(--ease-rise), border-color .18s var(--ease-rise);
}
.chip-toggle:hover { background: var(--tint); border-color: var(--border); }
.chip-toggle .icon { width: 16px; height: 16px; }
.chip-toggle .filter-dot {
  position: absolute;
  top: 5px;
  right: 8px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background-image: var(--grad-action);
  background-origin: border-box;
}
@media (max-width: 559px) { .chip-toggle span { display: none; } .chip-toggle { padding: 0 11px; } }

/* The "not yet checked" counterpart to ยืนยันแล้ว. It appears on the profile
   page only, where someone is deciding — a card in a grid of twenty would be
   a wall of grey. Neutral on purpose: not-yet-checked is not a verdict. */
.card-verify-off {
  background: rgba(255, 253, 251, .92);
  color: var(--muted);
}
.card-verify-off .icon { color: var(--muted); }
:root[data-theme="dark"] .card-verify-off { background: rgba(32, 22, 38, .92); }

/* --- card surfaces ------------------------------------------------------- */
/* The status line on a portrait carries no chip in the source — it is plain
   near-white text with a 5px dot, sitting on the photo scrim. */
.card-status {
  background: none;
  border-radius: 0;
  padding: 0;
  gap: 4px;
  font-size: 17px;
  font-weight: 500;
  color: var(--on-grad-2);
  backdrop-filter: none;
}
.card-status .dot { width: 5px; height: 5px; }
.card-place,
.district-label { border-radius: var(--r-micro); }

/* --- nav items ----------------------------------------------------------- */
/* Every item carries a glyph, and the active one is underlined INSIDE its pill
   at bottom:3px, wiping in horizontally with scaleX. */
.main-nav a,
.desktop-nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  overflow: hidden;
}
.main-nav a .icon,
.desktop-nav a .icon { width: 17px; height: 17px; flex: none; }
.main-nav a::after,
.desktop-nav a::after {
  content: "";
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 3px;
  height: 2.5px;
  border-radius: 2px;
  background-image: var(--grad-underline);
  background-origin: border-box;
  transform: scaleX(0);
  transition: transform .22s ease;
}
.main-nav a.active::after,
.desktop-nav a.active::after { transform: scaleX(1); }

/* The count badge. The source shapes it as a pale pink chip with a plum
   hairline and dark ink — not a solid magenta dot with a white ring. It shows
   only when the count is real: the favourites counter hides itself at zero and
   nothing else in this build has a number to show. */
.badge-count {
  position: absolute;
  top: -4px;
  right: -4px;
  display: inline-grid;
  place-items: center;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border: 1.5px solid #A32E8C;
  border-radius: var(--r-micro);
  background: #E8B4C8;
  color: #3A1140;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  box-shadow: none;
}

/* --- search field focus -------------------------------------------------- */
/* The ring belongs to the whole field, not the inner input: drawn on the input
   it cut across the pill and left the trailing label stranded outside it. */
.search-field input:focus,
.search-field input:focus-visible,
.search-input input:focus,
.search-input input:focus-visible {
  outline: none;
  box-shadow: none;
}
.search-field:focus-within,
.search-input:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 24%, transparent);
}
:root[data-theme="dark"] .search-field:focus-within,
:root[data-theme="dark"] .search-input:focus-within {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 40%, transparent);
}

/* ---------------------------------------------------------------------------
   27. desktop — the measured shell

   The source's bands carry no page gutter at all: each sets max-width:1300px
   and margin:0 auto, so below a 1300px viewport they run edge to edge. That
   reads as broken at 1100px, so the bands here use width:min(1300px, 100% -
   48px) instead — the one deliberate deviation in this section, and invisible
   at 1348px and above.
   ------------------------------------------------------------------------- */
@media (min-width: 1024px) {
  :root {
    --gutter: 24px;
    --grid-cols: 4;
    --grid-gap: 20px;
    --topbar-h: 71px;
  }

  /* top bar: 16 + 38 + 16 + 1px border = 71px */
  .site-header {
    background: var(--nav-bg);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    z-index: 40;
  }
  .site-header .header-inner {
    max-width: none;
    margin-inline: 0;
    padding: 16px 48px;
    min-height: 0;
    gap: 28px;
  }
  .site-header .brand { gap: 11px; min-height: 38px; }
  .site-header .brand-mark { width: 38px; height: 38px; }
  .site-header .brand-text strong {
    font-family: var(--font-display);
    font-size: 28px;
    font-weight: 600;
    line-height: 1;
    letter-spacing: 0;
  }

  .main-nav,
  .desktop-nav { gap: 4px; padding-left: 14px; justify-content: center; }
  .main-nav a,
  .desktop-nav a {
    height: 38px;
    min-height: 0;
    padding: 0 15px;
    font-size: 19px;
    font-weight: 500;
    color: var(--nav-off);
    background: transparent;
    transition: background .22s ease, color .22s ease, transform .16s ease;
  }
  .main-nav a:hover,
  .desktop-nav a:hover { background: var(--tint); color: var(--nav-on); transform: translateY(-1px); }
  .main-nav a.active,
  .desktop-nav a.active { background: var(--nav-on-bg); color: var(--nav-on); font-weight: 700; }

  .site-header .header-actions { gap: 10px; padding-right: 0; }
  .site-header .icon-btn,
  .site-header .icon-button {
    width: 38px;
    height: 38px;
    min-width: 38px;
    border-radius: var(--r-nav);
    border: 1px solid var(--border);
    background: transparent;
    color: var(--ink-soft);
  }
  .site-header .icon-btn:hover,
  .site-header .icon-button:hover { background: var(--tint); border-color: var(--border); color: var(--ink-soft); }
  .site-header .icon-btn .icon { width: 18px; height: 18px; }
  .site-header .saved-link .icon { fill: currentColor; stroke: none; }
  .site-header .avatar-btn { width: 38px; height: 38px; min-width: 38px; border-radius: 50%; border-color: transparent; }

  /* hero: the band's height is 56 + headline + 72, with no top padding of its
     own. The headline's 56px inset is measured from the viewport edge, not the
     content column, which is how the source places it. */
  .hero {
    padding: 0 0 72px;
    background-image: var(--scrim-hero), var(--grad-hero);
    background-origin: border-box;
  }
  .hero-inner {
    max-width: none;
    margin-inline: 0;
    padding: 0;
  }
  .hero h1 {
    max-width: 960px;
    margin: 0;
    padding: 56px 56px 0;
    font-family: var(--font-display);
    font-size: 54px;
    font-weight: 600;
    line-height: 1.2;
    color: var(--on-grad);
    text-wrap: pretty;
  }
  .hero p { display: none; }

  /* The source has no page shell at all: each band sets its own width and
     centres itself. The panel's 1300px is a CONTENT width over content-box
     padding, so its visual box is 1300 + 2x26 = 1352, and its inner edge lands
     exactly on the grid's 1300px column. */
  .shell.main-container { max-width: none; padding-inline: 0; }
  .search-panel {
    width: min(1352px, 100% - 48px);
    max-width: none;
    margin: -64px auto 0;
    padding: 22px 26px 24px;
    border-radius: var(--r-panel);
    box-shadow: var(--sh-panel);
    gap: 0;
  }
  .search-field {
    min-height: 52px;
    height: 52px;
    padding: 0 16px;
    border-radius: var(--r-field);
    background: var(--tint);
    border: 1px solid var(--border);
  }
  .search-field input { height: 50px; padding-block: 0; font-size: 21px; }
  .chip-line { margin-top: 0; }
  /* only what actually differs at this width; the rest is shared */
  .btn-search,
  .search-submit { flex: 1; box-shadow: 0 10px 22px -10px rgba(58, 17, 64, .7); }
  .btn-clear:hover { background: var(--tint); border-color: var(--border); }

  /* results bar + grid share the content column with the panel */
  .results-bar,
  .card-grid,
  .pagination,
  .pagination-info,
  .shell.main-container > .demo-notice,
  .shell.main-container > .notice,
  .shell.main-container > .empty-state {
    width: min(1300px, 100% - 48px);
    max-width: none;
    margin-inline: auto;
    padding-inline: 0;
  }
  .results-bar { align-items: baseline; padding-block: 26px 14px; }
  .results-bar p { margin: 0; }
  .results-count { font-size: 21px; color: var(--ink-soft); }
  .results-count strong {
    font-family: var(--font-display);
    font-size: 23px;
    font-weight: 600;
    color: var(--ink);
  }
  .sort-pill,
  .sort-control {
    height: 30px;
    min-height: 30px;
    padding: 0 12px;
    border-radius: var(--r-field);
    background: var(--pill-bg);
    border: 1px solid var(--border);
    color: var(--brand-ink);
    font-size: 19px;
    font-weight: 600;
  }
  .card-grid { grid-template-columns: repeat(4, 1fr); gap: 20px; padding-bottom: 56px; }

  /* card interior */
  .provider-card { border-radius: var(--r-card); }
  .card-name { font-family: var(--font-display); font-size: 22px; font-weight: 600; line-height: 1.3; }
  .card-body { padding: 12px 12px 14px; gap: 5px; }
  .card-verify { top: 8px; left: 8px; height: 22px; padding: 0 8px 0 6px; border-radius: var(--r-nav); background: rgba(255, 253, 251, .92); color: #3A1140; font-size: 16px; font-weight: 600; }
  .card-fav,
  .favorite-button { top: 8px; right: 8px; width: 30px; height: 30px; background: rgba(255, 253, 251, .92); }
  .card-price,
  .price-badge { bottom: 8px; left: 8px; height: 22px; padding: 0 9px; border-radius: var(--r-nav); background: rgba(58, 17, 64, .72); color: var(--on-grad-2); font-size: 17px; font-weight: 600; }
  .card-status { bottom: 9px; right: 8px; }
}

/* ---------------------------------------------------------------------------
   28. phone — the measured bottom bar

   8px top + 65px of tab + 10px floor + 1px border = 84px (was 24 / 98 until
   23 ก.ย. 2569, when the owner asked for a tighter bar).
   ------------------------------------------------------------------------- */
@media (max-width: 759.98px) {
  .bottom-nav,
  .mobile-nav {
    /* 24px is what the comp reserves, and the comp is a mockup in a drawn phone
       frame where the home indicator is a picture. On a real handset the OS
       reserves its own strip and reports it as env(safe-area-inset-bottom) —
       about 34px on an iPhone — so a FLAT 24 puts the labels underneath it.
       That is exactly what the owner photographed: icons visible, labels sliced
       off at the screen edge. max() keeps the comp's 24 on a phone that has no
       inset and grows to the real one on a phone that does. */
    padding: 8px 8px max(10px, env(safe-area-inset-bottom));
    gap: 4px;
    background: var(--nav-bg);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-top: 1px solid var(--border);
  }
  .bottom-nav a,
  .mobile-nav a {
    height: 65px;                /* 1 border + 8 top + 65 + 10 floor = 84 */
    min-height: 0;
    gap: 6px;
    padding: 0;
    justify-content: center;
    border-radius: var(--r-btn);
    /* แท็บสูง 65px ตายตัว: 65 - 22 ไอคอน - 6 ช่องไฟ = 21px ให้ป้าย
       flex-shrink ปริยายหนีบป้ายให้เตี้ยกว่าเนื้อหาตัวเอง แล้ว overflow:hidden
       ก็ตัดส่วนเกินทิ้งเงียบๆ ทุกแท็บทุกหน้า  16px x 1.3 = 20.8px พอดีในช่องที่มี */
    font-size: 16px;
  }
  .bottom-nav a span,
  .mobile-nav a span { flex: none; line-height: 1.3; }
  .search-panel { border-radius: var(--r-panel-phone); padding: 16px 16px 18px; margin-top: -58px; }
  .search-field { min-height: 46px; height: 46px; padding: 0 14px; border-radius: 14px; }
}

/* ---------------------------------------------- 29. the room and the gate */
/* The only two screens behind a sign-in. Geometry follows the source's global
   room: a 760px reading column, 34px avatars, bubbles that lose one corner on
   the side they are spoken from. */

.chat-shell {
  display: flex;
  flex-direction: column;
  min-height: calc(100dvh - var(--topbar-h) - var(--bottomnav-h));
}

.chat-head {
  background-image: var(--scrim-band), var(--grad-hero);
  background-origin: border-box;
  color: var(--on-grad);
}
.chat-head-inner {
  display: flex;
  align-items: center;
  gap: 11px;
  width: min(760px, 100% - 32px);
  margin-inline: auto;
  padding: 16px 0 14px;
}
.chat-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 13px;
  background: rgba(255, 255, 255, .16);
  border: 1px solid rgba(255, 255, 255, .22);
  color: var(--on-grad);
}
.chat-mark .icon { width: 21px; height: 21px; }
.chat-title { flex: 1; min-width: 0; }
.chat-title h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--on-grad);
}
.chat-sub {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 1px 0 0;
  font-size: 15px;
  color: var(--on-grad);
}
.chat-sub .dot { width: 6px; height: 6px; background: #5BE39A; }

.chat-me { flex: none; position: relative; }
.chat-name-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 30px;
  padding: 0 12px;
  border-radius: var(--r-field);
  background: var(--glass-on-grad);
  border: 1px solid rgba(255, 255, 255, .26);
  color: var(--on-grad);
  font-size: 17px;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}
.chat-name-btn::-webkit-details-marker { display: none; }
.chat-name-btn .icon { width: 14px; height: 14px; }
.chat-me[open] .chat-name-btn .icon { transform: rotate(180deg); }
.chat-me-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 20;
  width: min(300px, calc(100vw - 32px));
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px;
  border-radius: var(--r-panel);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-overlay);
  color: var(--ink);
}
.chat-nick-form { display: flex; flex-direction: column; gap: 10px; }
.chat-signout { margin: 0; padding-top: 10px; border-top: 1px solid var(--border); }
.chat-signout .text-link { background: none; border: 0; padding: 0; cursor: pointer; font: inherit; }

/* --- the stream ---------------------------------------------------------- */

.chat-room {
  flex: 1;
  width: min(760px, 100% - 32px);
  margin-inline: auto;
  padding: 14px 0 18px;
}
.chat-system {
  width: fit-content;
  max-width: 100%;
  margin: 0 auto 16px;
  padding: 7px 14px;
  border-radius: 13px;
  background: var(--tint);
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 15px;
  line-height: 1.5;
  text-align: center;
}
.chat-system strong { color: var(--ink); font-weight: 600; }

.chat-stream {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 11px;
}
.chat-msg { display: flex; gap: 9px; align-items: flex-start; }
.chat-msg.is-mine { justify-content: flex-end; }

.chat-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 50%;
  background-image: var(--grad-avatar);
  background-origin: border-box;
  color: var(--on-grad);
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 600;
  line-height: 1;
}

.chat-body {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  max-width: min(78%, 620px);
  align-items: flex-start;
}
.chat-msg.is-mine .chat-body { align-items: flex-end; }
.chat-meta { display: flex; align-items: baseline; gap: 7px; margin: 0; }
.chat-msg.is-mine .chat-meta { flex-direction: row-reverse; }
.chat-who { font-size: 15.5px; font-weight: 700; color: var(--ink); }
.chat-when { font-size: 14px; color: var(--muted); }

.chat-bubble {
  margin: 0;
  padding: 10px 13px;
  border-radius: 16px 16px 16px 5px;
  background: var(--tint);
  border: 1px solid var(--border);
  color: var(--ink);
  font-size: 17.5px;
  line-height: 1.55;
  /* a pasted URL with no spaces must not push the column wider than the screen */
  overflow-wrap: anywhere;
}
.chat-msg.is-mine .chat-bubble {
  border-radius: 16px 16px 5px 16px;
  background: none;
  background-image: linear-gradient(120deg, #C8385F 0%, #C13584 60%, #8E45E8 100%);
  background-origin: border-box;
  border-color: transparent;
  color: var(--on-grad);
}
.chat-empty { margin: 32px 0; text-align: center; color: var(--muted); }

/* --- composer ------------------------------------------------------------ */

.chat-composer {
  position: sticky;
  bottom: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0;
  padding: 10px max(16px, calc((100% - 760px) / 2)) 16px;
  border-top: 1px solid var(--border);
  background: var(--nav-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.chat-composer input[name="body"] {
  flex: 1;
  min-height: 46px;
  border-radius: var(--r-field);
}
.chat-composer .button { flex: none; min-width: 46px; padding-inline: 14px; }

/* --- gates --------------------------------------------------------------- */

.chat-gate {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  /* margin-inline:auto on a column flex item cancels stretch, so without an
     explicit width the gate shrinks to its longest line — 245px on a phone. */
  width: min(460px, 100% - 32px);
  margin-inline: auto;
  padding: 56px 0 72px;
  text-align: center;
}
.chat-gate h2 { margin: 0; font-size: 25px; }
.chat-gate p { margin: 0; color: var(--ink-soft); line-height: 1.75; }
.gate-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 62px;
  height: 62px;
  border-radius: 20px;
  background-image: var(--grad-mark);
  background-origin: border-box;
  color: var(--on-grad);
  margin-bottom: 4px;
}
.gate-mark .icon { width: 28px; height: 28px; }

/* --- sign in ------------------------------------------------------------- */

/* 24 ก.ย. 2569: the owner found both screens small and hard to follow. One
   column, read top to bottom: who (brand) / what (title + one line) / the form /
   the other door (sign in <-> sign up) / the fine print. Type on a fixed ~1.2
   ramp from 17 to 34.5px; nothing a visitor must read is under 16.5px. The two
   notes the sign-up form printed UNDER its button now sit above it. */
.login-stage {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  padding: 40px var(--gutter);
  background-image: var(--grad-hero);
  background-origin: border-box;
}
/* margin:auto, not align-items:center. A centred flex item taller than its box
   overflows both ways, so the long sign-up card lost its top edge off-screen with
   nothing to scroll back to. Auto margins centre it when there is room and fall
   back to the stage's padding when there is not. */
.login-panel {
  display: flex;
  flex-direction: column;
  gap: 24px;
  width: min(100%, 520px);
  margin: auto;
  /* side padding slides from 20 to 36 with the screen instead of stepping at the
     phone breakpoint, where it used to make the column WIDER just below 560px */
  padding: 36px clamp(20px, 6vw, 36px) 32px;
  border-radius: var(--r-modal);
  background: var(--card);
  box-shadow: var(--sh-panel);
}
.login-brand { align-self: flex-start; }
.login-head { display: flex; flex-direction: column; gap: 10px; }
.login-panel h1 { margin: 0; font-size: 34.5px; line-height: 1.35; }
.login-lede { margin: 0; color: var(--ink-soft); font-size: 19px; line-height: 1.65; }
.login-panel .notice { margin: 0; font-size: 18px; line-height: 1.6; }
.login-panel .form-help { margin: 0; font-size: 17px; line-height: 1.6; }
/* a bare link in the card (the code step's เริ่มใหม่) would otherwise stretch
   across the whole column, focus ring and all */
.login-panel > .text-link { align-self: flex-start; }

.login-form { display: flex; flex-direction: column; gap: 20px; margin: 0; }
.login-form .field { gap: 8px; font-size: 19px; }
.login-form .field > span { color: var(--ink); }
.login-form input { min-height: 56px; padding: 12px 16px; font-size: 20px; }
.login-form .field small { font-size: 17px; line-height: 1.55; }
.login-form .button { min-height: 56px; margin-top: 4px; font-size: 20px; }

/* the two things to know before signing up: a tinted box, not a coloured edge */
.login-note {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px 18px;
  border-radius: var(--r-field);
  background: var(--tint);
}
.login-note-title {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  color: var(--ink);
  font-size: 18.5px;
  font-weight: 700;
}
.login-note-title .icon { flex: none; width: 20px; height: 20px; color: var(--brand-ink); }
.login-note ul { display: grid; gap: 10px; margin: 0; padding-left: 22px; }
.login-note li { color: var(--ink-soft); font-size: 17.5px; line-height: 1.6; }
.login-note .text-link { display: inline; min-height: 0; font-size: 17.5px; text-decoration: underline; }

/* the other door: a prompt and a full-width secondary button, set apart by a rule */
.login-switch {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 22px;
  border-top: 1px solid var(--border);
  color: var(--ink-soft);
  font-size: 18px;
  text-align: center;
}
.login-switch .button { width: 100%; min-height: 54px; font-size: 19px; }

.login-foot { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }
.login-fine { margin: 0; color: var(--muted); font-size: 16.5px; line-height: 1.65; }
.login-foot .text-link { font-size: 18px; }

.login-line { width: 100%; min-height: 56px; font-size: 20px; }
.login-or {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0;
  color: var(--muted);
  font-size: 17px;
}
.login-or::before,
.login-or::after { content: ""; flex: 1; height: 1px; background: var(--border); }

@media (max-width: 559.98px) {
  .login-stage { padding: 20px 12px; }
  .login-panel { gap: 22px; padding-top: 28px; padding-bottom: 26px; border-radius: var(--r-panel-phone); }
  .login-panel h1 { font-size: 31.5px; }
}

/* From 760 the only bar above the room is the site header (--header-h, 71 at
   its tallest). The shell used to subtract --topbar-h (56 here) and, below
   1024, a tab bar that is hidden at these widths: at 768 that left the
   composer floating 72px above the bottom of the screen. */
@media (min-width: 760px) {
  .chat-shell { min-height: calc(100dvh - var(--header-h)); }
}
@media (min-width: 1024px) {
  .chat-head-inner { padding: 20px 0 18px; }
}

/* --- admin: moderation list ---------------------------------------------- */

.switch-missing { color: var(--warn); font-weight: 600; }

.mod-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.mod-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--r-card);
  background: var(--bg);
  border: 1px solid var(--border);
}
/* removed = struck through and muted, not faded: opacity .55 took the badge to
   2.36 and the timestamp to 2.62 */
.mod-row.is-removed .mod-body { color: var(--muted); text-decoration: line-through; text-decoration-color: var(--muted); }
.mod-main { min-width: 0; }
.mod-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 9px;
  margin: 0 0 4px;
  font-size: 15.5px;
  color: var(--muted);
}
.mod-meta strong { color: var(--ink); font-size: 16.5px; font-weight: 600; }
.mod-body { margin: 0; color: var(--ink); font-size: 17.5px; line-height: 1.6; overflow-wrap: anywhere; }
.mod-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.mod-actions form { margin: 0; }

@media (min-width: 760px) {
  .mod-row { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
  .mod-actions { flex-wrap: nowrap; }
}

/* --- demonstration numbers ------------------------------------------------ */
/* The owner chose to keep the comp's ratings, review counts, distances and free
   times on example rows (15 ก.ย. 2569) rather than cut them, on the condition
   that the screen says plainly what they are. This is that condition. */

.demo-flag { color: var(--ink); font-weight: 700; }

.demo-tag {
  display: inline-flex;
  align-items: center;
  height: 20px;
  padding: 0 8px;
  border-radius: var(--r-micro);
  background: var(--tint);
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 14.5px;
  font-weight: 600;
  white-space: nowrap;
}

/* the service area, where the comp put a distance we have no way to measure */
.search-area {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  padding-left: 12px;
  border-left: 1px solid var(--border);
  color: var(--brand-ink);
  font-size: 17px;
  font-weight: 600;
  white-space: nowrap;
}
.search-area .icon { width: 15px; height: 15px; }

/* ------------------------------------------------ 30. the search screen */
/* The comps give filtering a screen instead of a drawer. It owns no results:
   submitting hands the same query string to the catalogue. */

.search-screen { padding-block: 20px 48px; }
.search-screen-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 20px;
}
.search-screen-head h1 { margin: 0 0 2px; font-size: 26.5px; }
.search-screen-head p { margin: 0; color: var(--muted); font-size: 17px; }

.search-form { display: flex; flex-direction: column; gap: 16px; margin: 0; }

.filter-block {
  padding: 18px;
  border-radius: var(--r-panel);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
}
.filter-block h2 { margin: 0 0 12px; font-size: 19px; font-weight: 600; }

/* A radio that reads as a card. The input stays in the accessibility tree and
   keeps keyboard focus; only its own box is hidden. */
.pick { position: relative; display: block; min-width: 0; cursor: pointer; }
.pick input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
}
.pick span {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 10px 14px;
  border-radius: var(--r-field);
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--ink-soft);
  font-size: 17.5px;
  font-weight: 500;
  line-height: 1.4;
  text-align: center;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}
.pick:hover span { background: var(--tint); }
.pick input:checked + span {
  background: var(--pill-bg);
  border-color: color-mix(in srgb, var(--brand) 42%, transparent);
  color: var(--brand-ink);
  font-weight: 600;
}
.pick input:focus-visible + span {
  outline: 3px solid var(--violet);
  outline-offset: 2px;
}
.pick-inline span { min-height: 38px; padding: 8px 15px; }

.filter-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px;
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}
.filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}
.filter-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.filter-checks { display: grid; gap: 10px; margin-bottom: 12px; }

.search-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.search-actions .btn-search { flex: 1 1 220px; }

@media (min-width: 760px) {
  .filter-block { padding: 22px; }
  .search-screen { padding-block: 28px 64px; }
}

/* ------------------------------------------ 31. the phone, from the source */
/* The desktop shell was rebuilt from the owner's source; the phone was not, and
   it showed. In the comp the header is not a bar sitting above the hero — it is
   inside the gradient, drawn in translucent white on it: a 46px circle holding
   the drop, a 41px wordmark, the tagline under it, and four 34px circles on the
   right. The headline below is 26px at REGULAR weight, not a bold 26px.
   Every value here is measured off HOM Home Mobile.dc.html rendered at 390px.
   The one thing not carried over is the comp's fake 9:41 status row: a browser
   already sits under the real one, so the header starts at the top instead. */

@media (max-width: 759.98px) {
  /* The bar overlays the band, so the band's top padding has to clear it:
     24 top + the 41px wordmark and its tagline = about 96. */
  /* 100 ไม่ใช่ 96: คำโปรยใต้ชื่อเว็บโตขึ้น คอลัมน์ตราจึงสูงกว่า 72px เดิม
     ค่านี้กับ min-height ของ .brand ต้องขยับพร้อมกันเสมอ เพราะ hero ดึงตัวเองขึ้น
     มาทับด้วยค่านี้พอดี */
  /* 88: 12px top padding (was 24 — the owner asked for less empty space above
     the brand, 23 ก.ย. 2569) + the 76px brand column */
  :root { --topbar-h: 88px; }

  .mobile-topbar {
    align-items: flex-start;
    gap: 10px;
    min-height: 0;
    padding: 12px 22px 0;
  }

  /* the brand: a translucent disc on the gradient, not a filled tile */
  .mobile-topbar .brand { gap: 10px; align-items: center; min-height: 0; }
  .mobile-topbar .brand-mark,
  .mobile-topbar .brand-symbol {
    width: 46px;
    height: 46px;
    border-radius: 50%;
    background-image: none;
    background-color: rgba(255, 255, 255, .14);
    border: 1px solid rgba(255, 255, 255, .35);
    box-shadow: none;
  }
  .mobile-topbar .brand-mark::after,
  .mobile-topbar .brand-symbol::after { width: 16px; height: 16px; }

  .mobile-topbar .brand-text { gap: 2px; }
  .mobile-topbar .brand-text strong {
    font-family: var(--font-display);
    font-size: 41px;
    font-weight: 600;
    line-height: 1.25;
    letter-spacing: 0;
    color: var(--on-grad);
  }
  .mobile-topbar .brand-text small {
    font-size: 16px;
    /* ชื่อเว็บสั้นทำให้ clamp ของ wordmark ขึ้นถึงเพดาน 41px  41 x 1.25 = 51.25
       + ช่องไฟ 2 + คำโปรย ต้องไม่เกิน min-height ของ .brand ไม่งั้นแถบสูงเกิน
       --topbar-h ที่ hero ดึงขึ้นมาทับพอดี */
    line-height: 1.25;
    color: var(--on-grad);
  }

  /* four discs, the same treatment as the brand */
  .mobile-topbar .header-actions { gap: 6px; padding-top: 4px; padding-right: 0; }
  .mobile-topbar .icon-btn,
  .mobile-topbar .icon-button,
  .mobile-topbar .avatar-btn {
    width: 34px;
    height: 34px;
    min-width: 34px;
    border-radius: 50%;
    background: rgba(255, 255, 255, .12);
    background-image: none;
    border: 1px solid rgba(255, 255, 255, .28);
    color: var(--on-grad-2);
  }
  .mobile-topbar .icon-btn .icon,
  .mobile-topbar .icon-button .icon { width: 18px; height: 18px; }
  .mobile-topbar .icon-btn:hover,
  .mobile-topbar .icon-button:hover { background: var(--glass-on-grad-hover); }
  .mobile-topbar .badge-count { border-color: rgba(255, 255, 255, .5); }

  /* the band: a top-weighted deep-red scrim under the brand and tagline (16px,
     needs 4.5:1 over the band's orange top), fading out before the headline */
  body:has(.mobile-topbar) .hero { margin-top: calc(-1 * var(--topbar-h)); }
  .hero {
    padding: var(--topbar-h) 0 72px;
    background-image: var(--scrim-hero-phone), var(--grad-hero);
    background-origin: border-box;
  }
  .hero-inner { padding-inline: 22px; }
  .hero h1 {
    max-width: 250px;
    margin: 22px 0 0;
    font-family: var(--font-display);
    font-size: 28.5px;
    font-weight: 400;
    line-height: 1.5;
    color: var(--on-grad-3);
  }
  .hero p { display: none; }
}

/* The panel, its chips and its buttons are the same object at every width in
   the source — only the panel radius, the field radius and the gutter move.
   They were trapped inside the desktop query, which is why the phone kept the
   40px chips and the 13px labels the comp never had. */
.chip span {
  min-height: 32px;
  height: 32px;
  padding: 0 15px;
  border-radius: var(--r-chip);
  font-size: 20px;
  font-weight: 500;
  line-height: 1;
}
.chip-row { gap: 8px; padding: 14px 4px 4px; margin-inline: -4px; }
.btn-search,
.search-submit {
  min-height: 46px;
  height: 46px;
  border-radius: var(--r-btn);
  font-size: 23px;
  letter-spacing: .02em;
  color: var(--on-grad-3);
}
.btn-clear {
  flex: none;
  width: 88px;
  min-width: 88px;
  min-height: 46px;
  height: 46px;
  padding-inline: 0;
  border-radius: var(--r-btn);
  background: transparent;
  border: 1px solid var(--border);
  color: var(--brand-ink);
  font-size: 22px;
  font-weight: 500;
}
.search-buttons { gap: 10px; margin-top: 12px; }

/* ---------------------------------------------------------------------------
   32. the type scale and the control shapes, from the source

   Measured element by element against all three comps rendered locally with
   the real PSL Kanda: HOM Home Mobile (a 326px canvas), HOM Home Tablet (the
   desktop layout scaled 0.924 into a 1024 frame) and HOM Home Desktop at 1440.
   Every size below is identical in all three comps — the source has ONE type
   scale, not one per breakpoint — so it belongs here in the shared base and
   not in a media query. That is exactly how the card text ended up two to
   three points small at every width: the values were written at ~12px in the
   base and only the name was corrected, and only at 1024+.

   Numbers on the right are what the source measures.
   ------------------------------------------------------------------------- */

/* --- the hero headline --------------------------------------------------- */
/* The generic h1 rule tightens every heading by -.01em. The source tracks the
   display face at normal: at 52px that is 6px of drift across the first line
   (261.1px in the comp against 254.8px here, same string, same face). */
.hero h1 { letter-spacing: normal; }

/* --- the search row ------------------------------------------------------ */
.search-field { min-height: 54px; height: 54px; }                /* 54 */
.search-field input { font-size: 21px; }                         /* 18, at EVERY width */

/* The source hangs the location on the right of the field as quiet muted text,
   with no rule between it and the input and no weight on it. */
.search-area {
  padding-left: 10px;                                            /* 10 */
  border-left: 0;
  color: var(--muted);
  font-size: 18px;                                               /* 15 */
  font-weight: 400;                                              /* 400 */
  line-height: normal;
}

/* ours, not the source's — but it stands in the chip row, so it takes the chip's type */
.chip-toggle { font-size: 20px; font-weight: 500; }

/* --- the results bar ----------------------------------------------------- */
.results-count { font-size: 21px; font-weight: 400; line-height: normal; }   /* 18 / 400 */
.results-count strong { font-size: 23px; font-weight: 600; }     /* 20 / 600 */

/* The pill is 30px tall with a 16px label. A select left at its own 40px
   minimum is taller than the pill that holds it. */
/* line-height ต้องประกาศตรงนี้ ไม่งั้นป้ายรับ 1.75 มาจาก body แล้วล้นเม็ดที่สูง
   30px  normal ของ PSL คือ 1.19 ซึ่งพอดีและยังเหลือที่ */
.sort-pill,
.sort-control {
  min-height: 30px; height: 30px; padding: 0 12px; font-size: 19px;
  line-height: normal;
}
.sort-pill select,
.sort-control select {
  min-height: 0;
  height: 28px;
  font-size: 19px;                                               /* 16, not 13 */
  font-weight: 600;
}

/* --- the card ------------------------------------------------------------ */
/* The illustration ring is a fixed circle in the source — 72px on five of the
   six cards and 76px on the one that holds the drop. It does not scale with
   the column, which is what put a 96px ring on a 310px card at 1440 and a
   56px one at 390. */
.card-glyph,
.portrait-placeholder,
.portrait-emblem { width: 72px; }                                /* 72 */

/* All four overlays sit 8px in from the media box, at every width. */
.card-fav,
.favorite-button { top: 8px; right: 8px; width: 30px; height: 30px; }
.card-fav .icon,
.favorite-button .icon { width: 18px; height: 18px; }            /* 18 */
.card-verify { top: 8px; left: 8px; }
.card-price,
.price-badge {
  bottom: 8px;
  left: 8px;
  /* 22 ในแบบ คู่กับตัวอักษร 14px  ตอนนี้ 17px กินความสูงราว 20px ที่ line-height
     normal ของ PSL เหลือขอบข้างละ 1px ป้ายราคาเลยดูเหมือนตัวอักษรชนขอบ */
  height: 28px;
  padding: 0 9px;                                                /* 0 9 */
  font-size: 18px;                                               /* 14 / 600; 17 until 24 ก.ย. */
  font-weight: 600;
  line-height: normal;
  letter-spacing: normal;
}
/* The source sets the price as one string. The unit is not a smaller,
   lighter, faded subscript — it is the same 14px as the number. */
.card-price small,
.price-badge small {
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  opacity: 1;
}

.card-name {
  font-family: var(--font-display);
  font-size: 23px;                                               /* 19 / 600, at EVERY width; 22 until 24 ก.ย. */
  font-weight: 600;
  line-height: 1.3;
}
/* the four rows are 6px apart in the source; the desktop block had tightened
   them to 5 to buy back the height the oversized leading was costing */
.card-body { gap: 6px; }
.card-place,
.district-label {
  padding: 2px 7px;                                              /* 2 7 */
  font-size: 18px;                                               /* 14 / 400; 17 until 24 ก.ย. */
  font-weight: 400;
  line-height: normal;
}
.card-services { font-size: 18px; line-height: 1.5; }            /* 15 / 400, lh 1.5 */
.card-rating { font-size: 18px; line-height: normal; }           /* 15 */
.card-rating strong { font-weight: 600; }                        /* 600, not 700 */
.rating-count { font-size: 18px; font-weight: 400; }             /* 15 / 400 */
.card-slot { font-size: 17px; font-weight: 400; }                /* 14 / 400 */
.card-status,
.availability,
.card-status-label,
.card-verify { line-height: normal; }
/* At 320 two cards leave 138px each, and price + status at 18 / 17 need about
   150 of it: the status ran 7-16px off the card's right edge (it was already
   1-10px off at the old 17 / 16). One step down below 360 and it fits. */
@media (max-width: 359.98px) {
  .card-price,
  .price-badge { padding: 0 7px; font-size: 16px; }
  .card-status { font-size: 15px; }
}

/* The source leaves every one of these at the face's own leading and only
   names a ratio where it wants one (the name at 1.3, the service line at 1.5).
   Ours had 1.6 and 1.7 on rows the source leaves alone, which stacked ~11px of
   extra height into a card body the source draws at 132px. */

/* --- the phone bottom bar ------------------------------------------------ */
/* The source tints the whole 65px tab when it is current and leaves the icon
   bare at 22px. Ours drew a 34px icon standing on its own rounded tile, which
   reads as a second, smaller button inside the tab. */
@media (max-width: 759.98px) {
  .bottom-nav a,
  .mobile-nav a { padding: 8px 0; font-weight: 500; }            /* 500 */
  .bottom-nav a .icon,
  .bottom-nav a .nav-icon,
  .mobile-nav a .icon {
    width: 22px;                                                 /* 22 */
    height: 22px;
    padding: 0;
    background: transparent;
    border-radius: 0;
  }
  .bottom-nav a.active,
  .mobile-nav a.active { background: rgba(193, 53, 132, .11); font-weight: 700; }  /* 700 */
  .bottom-nav a.active .icon,
  .bottom-nav a.active .nav-icon,
  .mobile-nav a.active .icon { background: transparent; }

  .search-field { min-height: 48px; height: 48px; }              /* 48 */

  /* ข้อความชี้แนะ "ชื่อหมอนวด ย่าน หรือบริการ" กว้างเกินช่องที่มีบนจอ 375px
     ตัว "ร" ท้ายสุดหายไป  วัดจาก scrollWidth ของ <input> จริง ไม่ใช่จากแคนวาส
     ซึ่งตอบว่าพอดีทั้งที่ไม่พอ
     กฎนี้ต้องอยู่ตรงนี้ เพราะ .search-field input ของหมวด 32 อยู่หลังบล็อกมือถือ
     ของหมวด 28 ความเจาะจงเท่ากัน ของที่มาทีหลังจึงชนะ */
  .search-field input { font-size: 19px; }

  /* the phone header's language pill carries the same 16/600 as the avatar
     beside it in the source, not a small bold caps label */
  .mobile-topbar .language-label { font-size: 19px; font-weight: 600; }
}

/* ---------------------------------------------------------------------------
   33. the six card figures, from the source

   The comp does not put one glyph on every card. It draws a DIFFERENT figure in
   each of the six gradients, built out of plain boxes — no icon font, no svg:
   a drop inside a ring, three bars, three rings, a head over shoulders, three
   drops in a row, and a capsule inside a dashed ring. We were stamping the same
   outlined droplet on all six, which is why the block interiors read as one
   repeated placeholder instead of the source's set.

   Every number here is lifted from the comp's inline styles. `.card-glyph` now
   carries three empty spans; each variant uses as many of them as its figure
   needs and the base rule below strips the ring the old single-glyph version
   drew, so a variant only ever adds what it wants.
   ------------------------------------------------------------------------- */

.card-glyph {
  width: auto;
  height: auto;
  aspect-ratio: auto;
  border: 0;
  border-radius: 0;
  background: none;
  color: inherit;
}
.card-glyph > span { display: block; flex: none; }

/* 0 — a drop inside a thin ring */
.pf-0 .card-glyph {
  /* 74 of ring plus its own hairline: the comp measures this at 76 across */
  width: 76px;
  height: 76px;
  border: 1px solid rgba(255, 255, 255, .55);
  border-radius: 50%;
}
.pf-0 .card-glyph > span:first-child {
  width: 30px;
  height: 30px;
  border-radius: 50% 50% 50% 0;
  background: rgba(255, 255, 255, .75);
  transform: rotate(-45deg);
}
.pf-0 .card-glyph > span + span { display: none; }

/* 1 — three bars standing on a shared baseline */
.pf-1 .card-glyph { gap: 7px; align-items: flex-end; }
.pf-1 .card-glyph > span { width: 9px; border-radius: 5px; }
.pf-1 .card-glyph > span:nth-child(1) { height: 34px; background: rgba(255, 255, 255, .55); }
.pf-1 .card-glyph > span:nth-child(2) { height: 54px; background: rgba(255, 255, 255, .75); }
.pf-1 .card-glyph > span:nth-child(3) { height: 26px; background: rgba(255, 255, 255, .45); }

/* 2 — three rings, the innermost filled */
.pf-2 .card-glyph { width: 72px; height: 72px; }
.pf-2 .card-glyph > span { position: absolute; border-radius: 50%; }
.pf-2 .card-glyph > span:nth-child(1) { inset: 0; border: 1px solid rgba(255, 255, 255, .4); }
.pf-2 .card-glyph > span:nth-child(2) { inset: 14px; border: 1px solid rgba(255, 255, 255, .55); }
.pf-2 .card-glyph > span:nth-child(3) { inset: 28px; background: rgba(255, 255, 255, .75); }

/* 3 — a head over shoulders over a rule */
.pf-3 .card-glyph { flex-direction: column; align-items: center; gap: 6px; }
.pf-3 .card-glyph > span:nth-child(1) { width: 26px; height: 26px; border-radius: 50%; background: rgba(255, 255, 255, .75); }
.pf-3 .card-glyph > span:nth-child(2) { width: 52px; height: 14px; border-radius: 0 0 26px 26px; background: rgba(255, 255, 255, .45); }
.pf-3 .card-glyph > span:nth-child(3) { width: 64px; height: 1px; background: rgba(255, 255, 255, .4); }

/* 4 — three drops in a row, the middle one fainter */
.pf-4 .card-glyph { gap: 5px; align-items: center; }
.pf-4 .card-glyph > span {
  width: 18px;
  height: 18px;
  border-radius: 50% 50% 50% 0;
  transform: rotate(-45deg);
}
.pf-4 .card-glyph > span:nth-child(1) { background: rgba(255, 255, 255, .7); }
.pf-4 .card-glyph > span:nth-child(2) { background: rgba(255, 255, 255, .5); }
.pf-4 .card-glyph > span:nth-child(3) { background: rgba(255, 255, 255, .7); }

/* 5 — a capsule inside a dashed ring, with a dot above it */
.pf-5 .card-glyph { width: 70px; height: 70px; }
.pf-5 .card-glyph > span:nth-child(1) {
  position: absolute;
  inset: -1px;              /* the dashed ring sits outside the 70px box, as in the comp */
  border: 1px dashed rgba(255, 255, 255, .35);
  border-radius: 50%;
}
.pf-5 .card-glyph > span:nth-child(2) { width: 14px; height: 44px; border-radius: 7px; background: rgba(255, 255, 255, .7); }
.pf-5 .card-glyph > span:nth-child(3) {
  position: absolute;
  top: 6px;
  left: 50%;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .5);
  transform: translateX(-50%);
}

/* ---------------------------------------------------------------------------
   34. the glyphs, from the source

   The comp does not draw everything with an icon set. Some marks are svg, some
   are plain CSS boxes, and the two kinds are not interchangeable at the same
   nominal size. These are the ones that were drawn with the wrong instrument,
   at the wrong size, or in the wrong weight.
   ------------------------------------------------------------------------- */

/* The nav glyphs carry a heavier stroke than the rest of the set. */
.main-nav a .icon,
.desktop-nav a .icon,
.bottom-nav a .icon,
.bottom-nav a .nav-icon,
.mobile-nav a .icon { stroke-width: 1.9; }

/* The pin in the search field: the comp's is 13 across by 17 tall, not a 20px
   square, which is why the field read as icon-first. */
.search-field > .icon { width: 17px; height: 17px; }

/* The sort control: a funnel of three shortening rules, 14px at stroke 2, and a
   caret in the pill's own colour rather than the body ink. */
.sort-pill .icon,
.sort-control .icon { width: 14px; height: 14px; stroke-width: 2; }
.sort-pill::after {
  width: 7px;
  height: 7px;
  border-right-width: 1.6px;
  border-bottom-width: 1.6px;
  border-right-color: currentColor;
  border-bottom-color: currentColor;
  opacity: .7;
}

/* The location label keeps its divider. The source sets
   `border-left: 1px solid rgba(122,47,140,.13); padding-left: 10px` on it — the
   previous round read a computed shorthand that comes back empty when the four
   sides differ, and removed a rule the comp does have. */
.search-area {
  border-left: 1px solid var(--border);
  padding-left: 10px;
}

/* The bell, as the comp builds it: a domed cap closed by a straight base. */
.bell-btn { position: relative; }
.bell-dome {
  display: block;
  width: 12px;
  height: 11px;
  border: 1.4px solid currentColor;
  border-bottom: 0;
  border-radius: 6px 6px 0 0;
}
.bell-base {
  position: absolute;
  bottom: 12px;
  width: 16px;
  height: 1.4px;
  background: currentColor;
}

/* The saved heart is solid in the comp, and so is the one on the card: the card's
   reads as a tinted silhouette when idle and turns full magenta when pressed. */
.card-fav .icon,
.favorite-button .icon { fill: currentColor; stroke: none; }
.card-fav,
.favorite-button { color: rgba(122, 47, 140, .62); }   /* idle heart: .4 was 1.96 on its disc */
.card-fav[aria-pressed="true"],
.favorite-button[aria-pressed="true"] { color: #D62976; }
.card-fav[aria-pressed="true"] .icon,
.favorite-button[aria-pressed="true"] .icon { fill: currentColor; }

/* The district chip sits on a translucent wash of the brand, not on the opaque
   tint — over a white card the two are close, but the comp's keeps its edge. */
.card-place,
.district-label { background: rgba(193, 53, 132, .1); }

/* The free slot is quiet in the comp, not a second accent beside the rating. */
.card-slot { color: var(--muted); }

/* The scrim under the price and the status is 46px tall in the comp. */
.card-media-foot,
.photo-bottom { height: 46px; align-items: flex-end; padding: 0 8px 8px; }

/* The status dot is a soft mint in the comp when the person is taking work and a
   soft clay when they are not — two quiet tones on a saturated photo, not the
   signal-green and amber of a system status light. */
[data-status="available"] .dot { background: #9BD4A8; }
[data-status="appointment"] .dot,
[data-status="unavailable"] .dot { background: #E0A6A6; }
/* ---------------------------------------------------------------------------
   35. the search panel and the phone shell, from the source

   Two regions the earlier rounds only half-carried. The panel was rebuilt at
   1024 and up, and the phone was left with whatever the shared base happened to
   hold, so every value the source keeps constant across its three canvases —
   the field's gap and hairline, the CTA's drop, the chip palette — is right at
   one width and wrong at the other two. The phone shell was measured off the
   comp's geometry but never off its colour table: the bar and its tabs are
   still painted in the catalogue's body ink instead of the source's own nav
   ramp, and dark theme never reached them at all.

   Where we differ from the source on purpose, the reason is stated at the rule.
   ------------------------------------------------------------------------- */

/* --- two tokens the source separates and we had conflated ---------------- */

/* The panel's drop is themed in our tree (--sh-panel is rgba(58,17,64,.45) in
   light and rgba(0,0,0,.65) in dark, site.css:132 / :195) where the source
   hardcodes one literal for both. The phone's drop is a DIFFERENT geometry from
   the wide one — `0 18px 40px -18px` at Mobile:110 against `0 22px 50px -24px`
   at Desktop:103 and Tablet:104 — so it needs its own token rather than a
   literal, or the phone would lose the tree's dark adjustment. */
:root { --sh-panel-phone: 0 18px 40px -18px rgba(58, 17, 64, .45); }
:root[data-theme="dark"] { --sh-panel-phone: 0 18px 40px -18px rgba(0, 0, 0, .65); }

/* The source's `pillFg` is #B0308A in light and #F19AC8 in dark (Mobile:1795 /
   :1802). Neither of our existing tokens carries both: --brand-ink is
   #B0308A / #F7A8D4 (right in light, but its dark value is the source's navOn,
   not its pillFg) and --brand is #C13584 / #F19AC8 (right in dark only). Giving
   pillFg its own token is the only way to land both themes; --brand-ink keeps
   its other callers untouched. */
:root { --pill-fg: #B0308A; }
:root[data-theme="dark"] { --pill-fg: #F19AC8; }

/* --- the search panel ---------------------------------------------------- */

/* The source draws the panel's edge as the second layer of its shadow —
   `0 0 0 1px borderCol` (Desktop:103, Tablet:104, Mobile:110) — which paints
   outside the padding box. We drew the same hairline as a border, and under our
   own `box-sizing: border-box` a border eats inwards: the content column came
   out 1298px at 1440 where the source gives it 1300, and every column inside it
   inherited the two missing pixels. Painting the ring restores the width and
   the comp's two-layer edge at the same time. The dark-refinements rule at
   site.css:2554-2566 sets border-COLOR only, so it does not resurrect the
   border once the width is zero. */
.search-panel {
  border: 0;
  box-shadow: var(--sh-panel), 0 0 0 1px var(--border);
}

/* The source's panel is a plain block. The 14px between the field and the first
   chip is the chip row's own top padding (site.css:3536), and the 12px above
   the buttons is that row's margin (site.css:3560) — the panel itself spaces
   nothing. Ours is a flex column that added a 12px (phone) or 14px (tablet) gap
   on top of both, so the field stood 26px off its chips where the comp stands
   14. The desktop block had already zeroed the gap at site.css:2886; this moves
   that decision to the base, and hands our filter drawer — the one child the
   source has no counterpart for — the same 12px the source puts above a
   stacked row, so it does not butt against the chip line once the gap is gone. */
.search-panel { gap: 0; }
.filters,
.advanced-filters { margin-top: 12px; }

/* The field is one object at every width in the source: 10px between the pin,
   the input and the area label, on a hairline of the same borderCol the panel
   is ringed in (Desktop:104 / Tablet:105 / Mobile:111, all `gap:10px` and
   `border:1px solid {{ borderCol }}`). Both values were written only into the
   1024 block, so below it the field ran a notch tight and edgeless — a tinted
   slab rather than a bordered one. border-COLOR, not the shorthand, so the
   focus-within border at site.css:958 still wins on focus. */
.search-field,
.search-input { gap: 10px; border-color: var(--border); }

/* The source's pin is a 13x17 CSS figure: a ring, a dot and a tail, all three
   in pillFg (Desktop:105-109). We keep the svg — one element, one
   accessible-name-free shape, and it scales with the label — and the last round
   already sized it at 17 (site.css:3830). Only the colour was still wrong: it
   was on --brand, which is two steps brighter than everything else in the field
   in light and is the wrong pink in dark.

   NOT carried over: the source's literal 13x17 box. Our pin is an svg on a
   square viewBox, so a 13x17 box scales the drawn mark to min(13,17)/24 and
   letterboxes it — the glyph would come out about 9.6 x 11.2 against the
   source's 13 x 17, i.e. a NEW and visible mismatch traded for an invisible
   one. At 17 square the drawn mark measures about 12.5 x 14.7, which lands the
   source's width. Matching both would need preserveAspectRatio on icon(), and
   that attribute is shared by every glyph in the set. */
.search-field > .icon { color: var(--pill-fg); }

/* The chip row bleeds 4px into the panel's padding in the source and pads the
   same 4px back (`margin:0 -4px;padding-left:4px;padding-right:4px`,
   Desktop:114 / Mobile:121 / Tablet:115), so the first chip's ink lands flush
   under the field's left edge while its focus ring still has somewhere to go.
   `.chip-line .chip-row` (site.css:2640), our own wrapper for the filter
   button, zeroed that margin at a higher specificity and pushed the whole row
   4px inward. The row's own padding cancels the bleed at the toggle end, so the
   8px between the row and the toggle is unchanged. */
.chip-line .chip-row { margin-inline: -4px; }

/* The source hides the row's scrollbar and stops there — its whole treatment is
   `.hom-scroll{scrollbar-width:none}` plus a zero-size ::-webkit-scrollbar
   (Desktop:22-23), and the substring "mask" occurs zero times in any of the
   three home comps or in screen-search.html / screen-results.html. Our fade
   dims the last chip at the right edge even when the row has nothing to scroll,
   which reads as a disabled chip. (.district-chips shares the rule and is not
   emitted by any template; listing it keeps the pair together.) */
.chip-row,
.district-chips {
  -webkit-mask-image: none;
  mask-image: none;
}

/* An idle chip is set in the source's chipOffFg — #6A4A72 light / #DCC4E4 dark
   (Mobile:1797 / :1804) — which is the body ink. Ours put every one of the
   seven in the brand's text accent, so the row read as seven small links and
   the selected chip had no colour left to earn. The token was already defined
   for exactly this, in both themes, and unused: --chip-off-fg is var(--ink-soft)
   = #6A4A72 at site.css:71 and #DCC4E4 at site.css:179. */
.chip span { color: var(--chip-off-fg); }

/* The selected chip takes the source's own chip gradient — not the action
   gradient the buttons use. Mobile:1812 returns
   `linear-gradient(100deg,#8E45E8,#C13584 55%,#F5695A)` for an on chip, against
   the buttons' `...#C13584 52%,#F58B4C 100%`: the chip's coral end is darker
   than the button's orange one, so the label gains contrast rather than losing
   it. That label is #FFF6F2 in the same line — the near-white the source sets on
   every label that sits on a gradient — and the chip carries no box-shadow at
   all (Desktop:114-120 has none). Both values already exist as --grad-chip
   (site.css:93) and --on-grad-3 (site.css:87). */
.chip input:checked + span {
  background-image: var(--grad-chip);
  background-origin: border-box;
  color: var(--on-grad-3);
  box-shadow: none;
}

/* Housekeeping forced by the line above: the chip's focus ring is drawn on the
   label as outline + halo (site.css:282-286), and `.chip input:checked + span`
   has identical specificity, so whichever comes last owns box-shadow. It was
   already losing the halo to --sh-card; now it would lose it to `none`.
   Re-asserting the light halo here restores it. The dark halo at site.css:287
   is (0,4,2) and already wins on its own. */
.chip input:focus-visible + span { box-shadow: 0 0 0 5px rgba(255, 255, 255, .9); }

/* The source gives the chips no hover at all. It is not an omission — it hangs
   `style-hover` on the two buttons directly below them (Desktop:125-126) and
   deliberately not on these — so the tint we faded in on pointer-over is ours,
   and it made a hovered chip read as half-selected next to a real one.
   --chip-off is var(--tint) in both themes (site.css:70 / :178), i.e. exactly
   the idle background at site.css:1057, so this is a true no-op rather than a
   second colour. Declared as background-color so the selected chip keeps its
   gradient while it is hovered; the focus ring and the pointer still answer for
   the control. */
.chip:hover span { background-color: var(--chip-off); }

/* The search button carries the source's own deep drop at every width, not the
   generic card shadow — `box-shadow:0 10px 22px -10px rgba(58,17,64,.7)` inline
   on the CTA at Desktop:125, Tablet:126 and Mobile:132, and the same literal on
   the search screen's panel CTA at screen-search.html:125. It lived only in the
   1024 block (site.css:2899-2900). Its hover holds that shadow still and
   brightens the gradient underneath it instead of growing a larger one:
   `style-hover="filter:brightness(1.08)"` on the same three lines. */
.btn-search,
.search-submit { box-shadow: 0 10px 22px -10px rgba(58, 17, 64, .7); }
.btn-search:hover,
.search-submit:hover {
  box-shadow: 0 10px 22px -10px rgba(58, 17, 64, .7);
  filter: brightness(1.08);
}

/* The clear button's hover in the source is a 6% plum wash —
   `style-hover="background:rgba(107,31,58,.06)"` at Desktop:126 / Mobile:133 —
   not the opaque chip tint we were filling it with (site.css:433, and
   site.css:2901 at 1024+).

   The dark line is ours on purpose. The source hardcodes that one literal for
   both branches of theme(), where on its own dark card (#251530) it is
   invisible; mirroring it in white at the same weight keeps the control
   answering in the dark. This is the same call the tree already made for
   --sh-panel, which the source also hardcodes and we themed. */
.btn-clear:hover { background: rgba(107, 31, 58, .06); border-color: var(--border); }
:root[data-theme="dark"] .btn-clear:hover { background: rgba(255, 255, 255, .06); }

/* --- the phone shell ----------------------------------------------------- */

@media (max-width: 759.98px) {
  /* The phone's panel drop is not the wide one scaled: Mobile:110 gives
     `0 18px 40px -18px`, where Desktop:103 and Tablet:104 give
     `0 22px 50px -24px`. The base rule above carries the wide value, so the
     phone needs its own — with the ring repeated, because box-shadow does not
     cascade layer by layer. */
  .search-panel { box-shadow: var(--sh-panel-phone), 0 0 0 1px var(--border); }

  /* The source sets 8px between the brand group and the four discs
     (Mobile:82, the outer header row). Ours held them 10 apart — which is the
     gap INSIDE the brand group at Mobile:83, carried out one level by mistake
     when the header was rebuilt. */
  .mobile-topbar { gap: 8px; }

  /* The wordmark tracks at .01em in the source (Mobile:88). The generic heading
     rule pulls every display string 1% tight, and the previous round corrected
     that to zero rather than to the source's own positive value. */
  .mobile-topbar .brand-text strong { letter-spacing: .01em; }

  /* The language disc is the one of the four the source outlines a shade
     stronger — `rgba(255,255,255,.32)` at Mobile:103 against `.28` on the heart,
     bell and theme discs at Mobile:94 / :97 / :102 — because it carries a word
     instead of a glyph. */
  .mobile-topbar .lang-btn { border-color: rgba(255, 255, 255, .32); }

  /* TH is set in #FFF3EE at Mobile:103, the same near-white as the glyphs in
     the discs beside it, and the button already holds that value as
     --on-grad-2 (site.css:3496). The generic .language-label rule
     (site.css:816) is written for a label on a white bar, so on the gradient it
     was putting the catalogue's muted grey at about 2:1. */
  .mobile-topbar .language-label { color: inherit; }

  /* The bell is the same two boxes at both widths, but its base line sits 9px
     up inside the phone's 34px disc (Mobile:99) against 12px inside the
     desktop's 38px one (Desktop:51). We had carried only the desktop number
     (site.css:3865), which pulled the base into the dome on the phone. */
  .mobile-topbar .bell-base { bottom: 9px; }

  /* The source's bar is a translucent pane over a 12px blur, closed by a
     hairline on top and nothing else — Mobile:311 carries no box-shadow. Ours
     still held a lifted drop from the days it was an opaque white bar
     (site.css:840), and it reads as a second, softer border under the real one. */
  .bottom-nav,
  .mobile-nav { box-shadow: none; }

  /* Dark theme: the blanket surface rule at site.css:2554-2566 repaints every
     piece of chrome in the opaque card colour, and its three-part selector
     outranked the phone bar's own --nav-bg at site.css:2960. The bar is the one
     surface that has to stay translucent, because the catalogue scrolls
     underneath it; --nav-bg is already rgba(20,11,25,.95) in dark
     (site.css:168), the source's own value at Mobile:1794. */
  :root[data-theme="dark"] .bottom-nav,
  :root[data-theme="dark"] .mobile-nav { background: var(--nav-bg); }

  /* An idle tab is the source's navOff — rgba(58,17,64,.6) light and
     rgba(245,220,240,.58) dark (Mobile:1804 / :1797) — not the catalogue's
     --muted, which is a lighter, cooler grey mixed for body text on a card. The
     token exists for this (site.css:65 / :175) and was reaching only the
     desktop bar. */
  .bottom-nav a,
  .mobile-nav a { color: var(--nav-off); }

  /* The label sits on the face's 1.4, not 1.5 (Mobile:314). At the source's
     15px that is a 21px line, and the extra point and a half was being taken
     out of the gap between the glyph and the word inside a fixed 65px tab:
     8 + 22 + 6 + 21 + 8 is exactly 65, and 22.5 overflows it. */
  .bottom-nav a,
  .mobile-nav a { line-height: 1.4; }

  /* The current tab's wash is themed in the source: rgba(241,154,200,.17) on a
     dark bar against rgba(193,53,132,.11) on a light one (Mobile:1798 / :1805).
     Ours was written as the light literal (site.css:3700), so in dark theme the
     current tab all but disappeared. The themed token was already defined
     (site.css:67 / :176) and used only by the desktop bar. */
  .bottom-nav a.active,
  .mobile-nav a.active { background: var(--nav-on-bg); }
}

/* ---------------------------------------------------------------------------
   35. the header row and the results bar, from the source

   Two regions the earlier passes corrected in one place only. The wide shell in
   section 27 was measured against the desktop comp and got most of this right,
   but it got it right inside `@media (min-width: 1024px)`, and for these two
   objects the source carries ONE set of values across the artboards: the sort
   pill is identical at 326px and at 1440px, and so is the pill's wash and
   hairline. So the phone was still wearing the old base — a muted count, an
   opaque borderless pill — and the header carried two rules that outranked the
   ones describing the comp.

   Where the artboards genuinely DIFFER (the count badge, the results bar's
   inset, the content column) the difference is reproduced, not flattened: the
   badge is 16px on the wide comps and 18px on the phone, and the bar is inset
   22px on the phone against 48px on the tablet.

   Scoping note: `.results-bar` / `.results-count` are also the admin list
   header (public/admin/index.php:131). Everything here that moves the BOX is
   scoped to `.main-container` so the admin keeps exactly the geometry it has
   today; only the two type corrections are left global, because they are right
   wherever the class is used.
   ------------------------------------------------------------------------- */

/* --- the header actions -------------------------------------------------- */
/* The account chip is the one control in the row the source fills: a 135deg
   crimson-to-magenta gradient, no border, the wordmark's own serif on top
   (comp "HOM Home Desktop (1).dc.html":56 — `border-radius:50%;background:
   linear-gradient(135deg,#F5515F,#C13584);…;color:#FFF8F4`, and it is a <span>
   with no border and no hover at all). Section 26 says exactly that on
   .avatar-btn, but the wide shell paints every .icon-btn inside the header
   transparent from a two-class selector (site.css:2833), and the chip is an
   .icon-btn too — so the one-class rule lost and the avatar showed as an empty
   outline with a letter in it. Restated here at the weight that was winning.
   The hover arm is included because site.css:2843 repaints the chip on hover
   AND puts the 1px border back; the comp's chip has neither, so border-color
   is cleared there too — the drafter's version left the hover border in. */
.site-header .avatar-btn,
.site-header .avatar-btn:hover {
  background-image: var(--grad-avatar);
  background-origin: border-box;
  background-color: #C13584;
  border-color: transparent;
  color: var(--on-grad);
}

/* The moon is a point smaller than the heart beside it in the source — 17
   against 18 (comp :47 heart `width="18"`, :1817 themeIcon `width: 17`) — and
   it carries the nav's 1.9 stroke, not the 1.7 that icon() emits for the rest
   of the set. It is the only header glyph that does. */
.site-header .theme-toggle .icon { width: 17px; height: 17px; stroke-width: 1.9; }

/* Both wide comps close the icon run with a 1px x 24px hairline before the
   account chip — Desktop:55 / Tablet:56, `<span style="width:1px;height:24px;
   background:{{ borderCol }}"></span>`, a flex item in the gap:10px row. The
   phone bar has none, so the span is emitted into the wide header's actions
   only. flex:none so the flex row cannot collapse it. */
.header-divider {
  flex: none;
  width: 1px;
  height: 24px;
  background: var(--border);
}

/* The count badge. Section 26 already rewrote it as the pale pink chip with a
   plum hairline that the source draws, but on a bare .badge-count, and the
   round-button block at site.css:475 sizes any badge inside an .icon-btn from
   two classes — which is every badge we actually render. The solid magenta
   18px dot kept winning. Only the properties the two rules disagree on are
   listed; the 1.5px #A32E8C hairline already comes from section 26.
   These are the WIDE comp's numbers (Desktop:52 — top/right -4, 16x16,
   padding 0 3px, font-size 10px). */
.icon-btn .badge-count {
  top: -4px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  background: #E8B4C8;
  color: #3A1140;
  font-size: 13px;
  line-height: 1;
  font-variant-numeric: normal;
}

/* …but the phone comp draws the same chip one step larger: Mobile:100 —
   `top:-5px;right:-5px;min-width:18px;height:18px;padding:0 4px;
   border-radius:9px;background:#E8B4C8;border:1.5px solid #A32E8C;
   font-size: 14px;color:#3A1140`. Without this arm the rule above would ship
   the desktop chip onto the phone, which is a NEW mismatch, and it would also
   outrank two older phone rules by source order: site.css:688 (a white pill
   with #B02F74 ink) and site.css:3504 (a white hairline). Both of those were
   compensating for the OLD solid-magenta badge sitting on the hero gradient;
   the comp's chip is opaque #E8B4C8 with #3A1140 ink (about 9:1) and needs no
   compensation, so they are superseded on purpose, not by accident. */
@media (max-width: 759.98px) {
  .mobile-topbar .icon-btn .badge-count {
    top: -5px;
    right: -5px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    border-radius: 9px;
    border-color: #A32E8C;
    background: #E8B4C8;
    color: #3A1140;
    font-size: 14px;
  }
}

/* Hovering a nav item in the source lifts it and lays the tint behind it and
   does nothing else: Desktop:40-43 carry `style-hover="background:{{ subtleBg }};
   transform:translateY(-1px)"` with no colour key, so an inactive tab keeps its
   idle ink under the pointer and the accent stays the current tab's alone.
   Ours promoted the label to the active colour, so a hovered tab and the real
   one read as two current tabs. The wide header is visible from 760px up
   (site.css:2414), and the idle colour differs between the two bands —
   --ink-soft below 1024, --nav-off at 1024+ — so the fix is stated twice, each
   time as "hover equals idle". The drafter patched the 1024 band only, which
   left the identical defect alive between 760 and 1023. */
.main-nav a:hover,
.desktop-nav a:hover { color: var(--ink-soft); }
.main-nav a.active:hover,
.desktop-nav a.active:hover { color: var(--brand-ink); }

@media (min-width: 1024px) {
  .main-nav a:hover,
  .desktop-nav a:hover { color: var(--nav-off); }
  .main-nav a.active:hover,
  .desktop-nav a.active:hover { color: var(--nav-on); }
}

/* --- the results bar ----------------------------------------------------- */
/* The source carries the whole rhythm of this row as padding — 26/14 in the
   wide comps (Desktop:130 `margin:0 auto;padding:26px 0 14px`), 22/12 on the
   phone (Mobile:137 `padding:22px 22px 12px`) — and gives it no margin at all,
   so it sits flush under the panel. Ours added 24px of margin (32 in the wide
   shell, site.css:2474) on top of that padding and pushed the count more than
   twice as far down as the source puts it. The count and the pill also sit on
   a shared baseline at every width, not centred; only the wide query had been
   told.

   The phone's 22/12 becomes the shared base, so the 760-1023 band — which has
   no artboard of its own — inherits a sane rhythm instead of falling back to
   the 24px margin. The 26/14 is then restated for 1024+, because section 27's
   copy of it sits EARLIER in the file at the same specificity and would lose
   to a base padding-block declared here. */
.main-container .results-bar {
  margin-block: 0;
  align-items: baseline;
  padding-block: 22px 12px;
}
@media (min-width: 1024px) {
  .main-container .results-bar { padding-block: 26px 14px; }
}

/* The phone comp insets this row 22px against the 16px gutter the grid below
   it uses (Mobile:137 `padding:22px 22px 12px` against Mobile:147
   `padding:0 16px 26px`): the count and the pill stand a little inside the
   cards. Our shell already contributes --gutter (16px on the phone), so 6px
   here lands the content edge at 22px. Confined to the phone band: --gutter is
   24px from 760 up, where the extra 6px would only knock the bar out of line
   with the grid. */
@media (max-width: 759.98px) {
  .main-container .results-bar { padding-inline: 6px; }
}

/* The count is a bare span in the source. Ours is a paragraph, and the generic
   paragraph rule (site.css:247) hangs 12px under it — zeroed at 1024+ inside
   the desktop query, still there on the phone, where it lifted the bar's
   baseline off the padding it is supposed to sit on. Left unscoped: a <p> in a
   flex row should not hang a margin anywhere the class is used. */
.results-bar p { margin: 0; }

/* The sentence is body ink at every width in the source — tBody #6A4A72 at
   Desktop:1770 / Mobile:1802, bound at Desktop:131 / Mobile:138. tMuted is for
   the service line and the free slot, tBody for anything that is a sentence.
   The wide query had already been corrected and the phone was still muted. */
.results-count { color: var(--ink-soft); }
/* The number is a word inside that sentence, separated from the noun after it
   by one ordinary space: the comp's <strong> (Desktop:131) declares only
   font-family / font-size / font-weight / color, so margin-right computes to 0
   and font-variant-numeric to normal. Ours padded it with a further 4px and
   forced tabular figures, which widened the same gap twice. */
.results-count strong { margin-right: 0; font-variant-numeric: normal; }

/* The tablet artboard holds this row and the grid under it on a 928px column
   inside a 1024 frame — `max-width:928px;margin:0 auto` at Tablet:131 and
   Tablet:140, i.e. 48px each side, the same inset the header uses at
   Tablet:33 `padding:16px 48px` — and 1300 inside 1440 is that formula with
   room to spare. Ours subtracted one gutter where the source subtracts two, so
   at 1024 the column came out 976px instead of 928 and the page kept only 24px
   of margin exactly where it matters most. (The drafter's note said "the
   column ran 48px wide"; that was a slip — it ran 976px.)

   The panel moves with it. The comp's panel is a CONTENT width of 928 (tablet)
   / 1300 (desktop) over content-box padding of 26px a side — the comp sets no
   global box-sizing — so its visual box is always the grid's column + 52. That
   is min(1352px, 100% - 44px): 980 at 1024 and 1352 at 1440, both exactly what
   the artboards measure, and the panel's inner edge lands on the grid's edge at
   every width instead of only above 1348. Without this the new column would
   leave the panel hanging 24px proud at 1024.

   Scoped to .main-container so the admin's results bar and pagination keep the
   width they have today — they are not in any comp and halving their column
   would only deepen an inset they already should not have. */
@media (min-width: 1024px) {
  .main-container .results-bar,
  .main-container .card-grid,
  .main-container .pagination,
  .main-container .pagination-info,
  .shell.main-container > .demo-notice,
  .shell.main-container > .notice,
  .shell.main-container > .empty-state { width: min(1300px, 100% - 96px); }

  .search-panel { width: min(1352px, 100% - 44px); }
}

/* --- the sort pill ------------------------------------------------------- */
/* The pill is a translucent wash of the brand over a hairline at every width in
   the source — Mobile:139 is byte-identical to Desktop:132:
   `background:{{ pillBg }};border:1px solid {{ borderCol }};color:{{ pillFg }}`,
   and pillBg/borderCol are our --pill-bg/--border in BOTH theme tables
   (Desktop:1762-1771). Ours had the wide shell right and the base wrong — the
   opaque tint, and no border at all — so on a phone the pill lost its edge and
   sat a shade heavier than the chips above it. box-sizing is border-box
   globally (site.css:200-202), so the hairline does not grow the 30px pill. */
.sort-pill,
.sort-control {
  background: var(--pill-bg);
  border: 1px solid var(--border);
}
/* The source has one hover state for it, and it swaps no colour: Desktop:132
   `style-hover="filter:brightness(.97)"` — the whole control dims by a hair. */
.sort-pill:hover,
.sort-control:hover { filter: brightness(.97); }
/* Inside, the source runs glyph, label and caret on a single 6px gap with
   nothing else between them (Desktop:1026-1030: gap:6px, padding:0 12px).
   Ours is a real <select>, so the caret is drawn on the pill and the select has
   to reserve room for it — but it was reserving 28px for a 7px mark, and
   another 4px sat between the funnel and the label, which pushed the label off
   centre and made the pill read wider than the source's. Trimmed to the width
   the caret actually occupies, with the caret moved out to the pill's own 12px
   inset: the mark then sits in exactly the slot a 7px in-flow item would take
   at the end of the row, and the label clears it by ~5.5px against the comp's
   6px. */
.sort-pill select,
.sort-control select { padding: 0 14px 0 0; }
.sort-pill::after { right: 12px; }


/* ---------------------------------------------------------------------------
   35. the card, from the source — the media overlays and the body rows

   Every value below is read off the card template the three comps share (HOM
   Home Desktop (1).dc.html lines 140-166, and its twins at Mobile 147-173 and
   Tablet 141-167). That template is one block of inline styles with no
   breakpoint of its own: the source draws this card the same way at 390 and at
   1440.

   Two habits put us out of step with it. The first is that the corrected
   overlay values were written into the 1024px block in section 27 and never
   into the base, so a phone kept being served the pre-comp draft of the same
   chip. The second is that the overlays were treated as page furniture and
   re-painted for dark mode in section 25 — but the source hard-codes one set of
   colours for them in both branches of theme(), because the ground they sit on
   is a photograph, not the page.
   ------------------------------------------------------------------------- */

/* --- the overlays on the media box --------------------------------------- */

/* The verified pill is a 22px capsule whose label is 13/600 in the source's
   deep plum, not our brand magenta at 11/700, and it carries no drop shadow:
   the chip already reads against a gradient and the source lets it. The
   `:not(.card-verify-off)` keeps the profile page's neutral "ยังไม่ยืนยัน" twin
   out of the repaint — that pill is ours, and grey on purpose. */
.card-verify {
  height: 22px;
  padding: 0 8px 0 6px;
  font-size: 16px;
  font-weight: 600;
  box-shadow: none;
}
.card-verify:not(.card-verify-off) {
  background: rgba(255, 253, 251, .92);
  color: #3A1140;
}

/* …and grey is what it has to be given back. Section 26 sets that at line 2676,
   but the 1024px block in section 27 restates `.card-verify { color: #3A1140 }`
   at the same specificity further down the file, so above 1024px the "not yet
   checked" pill has been wearing the verified pill's plum. Restated here, after
   both, in each branch. Not a comp value — the comp has no such pill; it is the
   rule the `:not()` above assumes is already holding. */
.card-verify-off { color: var(--muted); }

/* The source draws the tick as a 17px magenta disc with a white ✓ centred in
   it, not as a stroked check floating loose in the pill. PSL Kanda carries no ✓
   glyph (section 1), so the mark stays an svg and the disc is painted on the
   svg's own box; the stroke is thickened because the set's 1.7 scaled into an
   11px content box lands under a pixel and disappears. */
.card-verify:not(.card-verify-off) .icon {
  width: 17px;
  height: 17px;
  padding: 3px;
  border-radius: 50%;
  background: #C13584;
  color: #FFFFFF;
  stroke-width: 2.8;
}

/* The same warm near-white under the heart, and the same absence of a shadow.
   The hover rule has to restate the ground: it is a two-class selector, so
   without this it would put the old flat white back under the pointer. The
   hover's lift and its magenta ink stay — the comps are static and declare no
   hover state anywhere, so there is nothing there to match or to contradict. */
.card-fav,
.favorite-button {
  background: rgba(255, 253, 251, .92);
  box-shadow: none;
}
.card-fav:hover,
.favorite-button:hover { background: rgba(255, 253, 251, .92); }

/* The price chip: the source's plum at .72 over the photo and its warm
   near-white ink — the pair that until now only reached a desktop viewport. It
   is set in the body face as one unbroken string, not in the display serif,
   not spaced apart into a number and a unit, and with no tabular figures, which
   the source never asks for on a chip that stands alone. */
.card-price,
.price-badge {
  gap: 0;
  align-items: center;
  background: rgba(58, 17, 64, .72);
  color: var(--on-grad-2);
  font-family: var(--font-ui);
  font-variant-numeric: normal;
}

/* The scrim is the token we already carry for it — the source's plum, where
   ours had drifted a few points colder. The source hangs the price and the
   status off the media box itself, 8px in from its bottom corners; our wrapper
   sits them on its own bottom edge instead of centring them in the 46px band,
   which puts each one back where the source has it. */
.card-media-foot,
.photo-bottom {
  align-items: flex-end;
  padding: 0 8px 8px;
  background: var(--grad-scrim);
  background-origin: border-box;
}

/* The source lifts the status line one pixel off that edge so it reads level
   with the price beside it, which has a capsule around it and does not. */
.card-status { margin-bottom: 1px; }

/* On the photo the source runs two tones, not one per state: near-white text
   with a sage dot while the person is taking work, warm cream with a dusty rose
   dot while they are not. --ok / --busy / --off are the page's signal colours
   and are far too saturated to sit on a gradient; they stay where they belong,
   on the profile page's own status line, and only the card overlay is retoned.
   All four values are literals in the source and do not move with the theme,
   so the dot rules below are written once and serve both. */
.card-status[data-status="appointment"],
.card-status[data-status="unavailable"] { color: #FFEDD8; }
.card-status[data-status="available"] .dot { background: #9BD4A8; }
.card-status[data-status="appointment"] .dot,
.card-status[data-status="unavailable"] .dot { background: #E0A6A6; }

/* --- the overlays do not follow the theme -------------------------------- */
/* theme() renders this card template from one string in both of its branches:
   every colour on the four overlays is a literal, because they are painted on a
   gradient that does not change with the page. Section 25 had been treating
   them as surfaces and darkening them, which in dark mode wrapped a near-black
   capsule around the status line the source draws as bare text and turned the
   heart from its quiet plum into page ink. */
:root[data-theme="dark"] .card-verify:not(.card-verify-off),
:root[data-theme="dark"] .photo-label {
  background: rgba(255, 253, 251, .92);
  color: #3A1140;
}
:root[data-theme="dark"] .card-verify-off { color: var(--muted); }
:root[data-theme="dark"] .card-fav,
:root[data-theme="dark"] .favorite-button {
  background: rgba(255, 253, 251, .92);
  color: rgba(122, 47, 140, .62);
}
:root[data-theme="dark"] .card-fav[aria-pressed="true"],
:root[data-theme="dark"] .favorite-button[aria-pressed="true"] { color: #D62976; }
:root[data-theme="dark"] .card-price,
:root[data-theme="dark"] .price-badge { background: rgba(58, 17, 64, .72); }
:root[data-theme="dark"] .card-status { background: none; color: var(--on-grad-2); }
:root[data-theme="dark"] .card-status[data-status="appointment"],
:root[data-theme="dark"] .card-status[data-status="unavailable"] { color: #FFEDD8; }

/* --- the four rows in the card body -------------------------------------- */

/* The source spaces the rows at 5 and then pushes the rating row down a further
   2, so the three text rows sit tight and the row of numbers reads as its own
   band under them. Section 32 flattened that into an even 6 and dropped the
   nudge, which is a point of air in the wrong place on every card. The nudge is
   scoped to the card body: profile.php hangs .card-rating on its own rating
   line, which the source lays out differently and which is not part of this. */
.card-body { gap: 5px; }
.card-body .card-rating { margin-top: 2px; }

/* The service line is tBody in the source — the middle ink, one step darker
   than the muted grey we had on it, which is the colour the source reserves for
   the review count and the free-slot time. It also wraps: the source gives it
   no truncation at all, and both sides already cap the string at two services,
   so the ellipsis was only ever hiding the back half of a long Thai service
   name in a card that had room to grow. */
.card-services {
  color: var(--ink-soft);
  white-space: normal;
  overflow: visible;
}

/* The district chip gets the same release, for the same reason and only on the
   card: the source declares no white-space, no overflow and no text-overflow on
   it, so a long district wraps instead of losing its tail. The profile page's
   copy keeps the nowrap — there the chip sits in a row beside the profile code,
   and it is the one place that still carries a pin inside it, which is why the
   inline-flex and the 4px gap on the shared rule stay where they are. */
.card-body > .card-place {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

/* The district chip is pillFg on pillBg, and in the source's dark branch those
   two part company from the nav accent: pillFg stays #F19AC8 while navOn moves
   to #F7A8D4. --pill-fg and --pill-bg were written for this chip and then never
   wired to it, so the chip had been borrowing --brand-ink and coming out a
   shade pinker than the source in dark, over a literal light-branch wash.
   Light is unchanged — there pillFg and navOn are the same value. */
:root[data-theme="dark"] { --pill-fg: #F19AC8; }
.card-place,
.district-label {
  background: var(--pill-bg);
  color: var(--pill-fg);
}

/* ---------------------------------------------------------------------------
   36. the profile screen, from the source

   The comp lays a profile out as one sheet under a tall gradient hero, not as
   the gallery-beside-facts page we had. Every value here is measured off the
   comp's own detail screen — reachable only by clicking a card in any of the
   .dc.html files, which is why it was missed for so long.

   What the source shows and we do not: a booking CTA on a free slot, a review
   list with named reviewers, and two counters beside the rating. The layout is
   built for all of it; the figures come from demo_metrics(), which returns []
   for every real person, so a named provider gets the same sheet with those
   blocks simply absent.
   ------------------------------------------------------------------------- */

.profile-container .profile-layout { display: block; }

/* --- the hero ------------------------------------------------------------ */
.profile-hero {
  position: relative;
  height: 270px;
  margin-inline: -18px;
  overflow: hidden;
  background-image: var(--pf-0);
  background-origin: border-box;
}
.profile-slides {
  position: absolute;
  inset: 0;
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.profile-slides::-webkit-scrollbar { display: none; }
.profile-slide {
  position: relative;
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
}
.profile-slide img { width: 100%; height: 100%; object-fit: cover; }

/* the initial in its ring — 112 across, the letter at 52 */
.profile-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 112px;
  height: 112px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .5);
  background: rgba(255, 255, 255, .1);
  font-family: var(--font-display);
  font-size: 54px;
  font-weight: 600;
  line-height: 1;
  color: rgba(255, 255, 255, .9);
}

.profile-hero-scrim {
  position: absolute;
  inset: auto 0 0;
  height: 90px;
  background: linear-gradient(to top, rgba(28, 10, 32, .55), transparent);
  background-origin: border-box;
  pointer-events: none;
}
.profile-dots {
  position: absolute;
  bottom: 14px;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  gap: 6px;
  pointer-events: none;
}
.profile-dots span {
  width: 5px;
  height: 5px;
  border-radius: 3px;
  background: rgba(255, 253, 251, .5);
}
.profile-dots span.on { width: 20px; background: rgba(255, 253, 251, .95); }

.profile-hero-bar {
  position: absolute;
  top: 16px;
  left: 18px;
  right: 18px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}
.profile-hero-bar .hero-right { display: flex; align-items: center; gap: 8px; }
.profile-hero-bar .hero-btn {
  position: static;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;                      /* 34 until 24 ก.ย.: under the 44px tap target */
  height: 44px;
  flex: none;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 253, 251, .92);
  color: rgba(122, 47, 140, .62);
  cursor: pointer;
  box-shadow: none;
}
.profile-hero-bar .hero-btn .icon { width: 21px; height: 21px; }
/* this rule sits after .card-fav[aria-pressed="true"] at the same specificity and
   used to repaint the pressed heart plum, so pressed and idle looked the same */
.profile-hero-bar .hero-btn[aria-pressed="true"] { color: #D62976; }
.profile-hero-bar .card-verify { position: static; }
/* The card's pill is 16px in a 22px capsule and stays that way on the cards;
   on the profile it is the identity claim, so it reads at the label size. */
.profile-hero-bar .card-verify { height: 30px; padding: 0 11px 0 7px; gap: 5px; font-size: 18px; }
.profile-hero-bar .card-verify:not(.card-verify-off) .icon { width: 19px; height: 19px; }
.profile-hero-bar .card-verify-off .icon { width: 17px; height: 17px; }

/* --- the sheet ----------------------------------------------------------- */
.profile-sheet {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 18px 0 0;
}
.profile-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
.profile-id { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.profile-sheet h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 33.5px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: normal;
  color: var(--ink);
}
.profile-meta { margin: 0; font-size: 19px; color: var(--ink-soft); }
.profile-rate {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 3px;
}
.profile-rate strong {
  font-family: var(--font-display);
  font-size: 29.5px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--ink);
}
.profile-rate span { font-size: 18px; color: var(--muted); }

/* the three counters */
.profile-stats { display: flex; gap: 8px; padding: 4px 0 2px; }
.profile-stats .stat {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: 11px 10px;
  border-radius: 14px;
  background: var(--tint);
}
.profile-stats .stat-value {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 24px;
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.profile-stats .stat-value .star { width: 18px; height: 18px; }
.profile-stats .stat-value .star .icon { width: 18px; height: 18px; }
.profile-stats .stat-label { font-size: 18px; line-height: 1.35; color: var(--muted); }
.profile-demo-tag { margin: -4px 0 0; font-size: 17px; color: var(--muted); }

/* headings, chips and prose */
.profile-h {
  margin: 0;
  padding-top: 12px;                /* 6 until 24 ก.ย.: too little between sections once the prose went to 19-20 */
  font-family: var(--font-display);
  font-size: 23px;
  font-weight: 600;
  color: var(--ink);
}
.profile-h-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding-top: 10px;
}
.profile-h-row .profile-h { padding-top: 0; }
.profile-h-note { font-size: 18px; color: var(--muted); }
.profile-chips { display: flex; flex-wrap: wrap; gap: 7px; }
.profile-chips span {
  display: flex;
  align-items: center;
  /* 29 คู่กับตัวอักษร 16px  ตอนนี้ 19px กินบรรทัดสูงกว่ากล่อง ชิปจึงต้องโตตาม
     24 ก.ย. 2569: ไม่ล็อกความสูงแล้ว ที่ 34px บรรทัด 33.25px เต็มกล่องพอดี ชื่อบริการ
     ยาว ๆ ที่ตัดบรรทัดจะล้นลงไปทับแถวถัดไป ให้ชิปโตตามบรรทัดแทน */
  min-height: 38px;
  padding: 4px 15px;
  border-radius: 19px;
  line-height: 1.35;
  background: rgba(193, 53, 132, .1);
  color: var(--brand-ink);
  font-size: 19px;
  font-weight: 600;
}
.profile-slots span { font-variant-numeric: tabular-nums; }
.profile-about {
  margin: 0;
  font-size: 20px;
  line-height: 1.7;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.profile-empty {
  margin: 0;
  padding: 14px 16px;
  border-radius: var(--r-card);
  background: var(--tint);
  font-size: 19px;
  line-height: 1.6;
  color: var(--ink-soft);
}

.profile-sheet .profile-status {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 19px;
}
.profile-sheet .profile-status .dot { width: 9px; height: 9px; }
.profile-sheet .profile-status .profile-code {
  margin-left: auto;
  font-size: 17px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.profile-sheet .profile-pills { display: flex; flex-wrap: wrap; gap: 7px; }
.profile-sheet .profile-pills span {
  padding: 3px 10px;
  border-radius: var(--r-micro);
  background: var(--tint);
  font-size: 17px;
  color: var(--ink-soft);
}
/* 24 ก.ย. 2569: the 12px top inset that sat here made the card 12/18/18 around
   a 56px button; it has the base 18 on every side now.
   The demo notice and the 404 are shared rules (.notice, .empty-state), so the
   profile's sizes are scoped to its own container. */
.profile-container > .demo-notice { font-size: 18px; line-height: 1.65; }
.profile-container > .demo-notice > div > strong { font-size: 19px; }
.profile-container > .demo-notice > .icon { width: 22px; height: 22px; margin-top: 4px; }
.profile-container .empty-state h1 { font-size: 31.5px; line-height: 1.35; }
.profile-container .empty-state p { font-size: 19px; line-height: 1.6; }
.profile-container .empty-state .button { min-height: 56px; font-size: 20px; }

/* From 760 up the sheet stops running the full width: the source's is a phone
   column, and a 31px name over a 1300px measure reads as a banner, not a name. */
@media (min-width: 760px) {
  .profile-hero { margin-inline: 0; border-radius: var(--r-panel); }
  /* The same measure as โปรไฟล์ของฉัน: the source gives its two detail screens
     one column, and two different widths for the same kind of page is exactly
     the inconsistency the owner was pointing at. */
  .profile-hero,
  .profile-sheet { max-width: 820px; margin-inline: auto; }
}

/* ---------------------------------------------------------------------------
   37. the card is one target, and the profile column is a column

   Two things the owner asked for after seeing the source side by side.

   The whole card opens the profile in the comp — it carries `cursor: pointer`
   on the outer box, not on a hitbox over the photograph. Ours covered the media
   and left the lower third inert, so half of a card did nothing and the only
   way in from the text was the name itself. The hitbox now spans the card and
   the favourite button is lifted clear of it so it still takes its own click.
   ------------------------------------------------------------------------- */

.provider-card { position: relative; cursor: pointer; }
/* The hitbox is a sibling of the media rather than a child of it: .card-media
   clips its own children so it can round the gradient, and a hitbox inside it
   could never reach past the photograph. */
.provider-card > .card-link {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
}
.provider-card .card-fav,
.provider-card .favorite-button { z-index: 4; }
.provider-card .card-name a { position: relative; z-index: 2; }

/* Above the phone the therapist page becomes the same centred 820 column as
   โปรไฟล์ของฉัน, and the page container IS that column — centring a narrow block
   inside the catalogue's 1300 shell left the two detail screens seven pixels
   apart on the same screen, which is the misalignment the owner saw. */
main.profile-container { padding-inline: 18px; }   /* the source's inset, at every width */
@media (min-width: 760px) {
  main.profile-container { max-width: 820px; }
  .profile-hero,
  .profile-sheet { max-width: none; }
}

/* ---------------------------------------------------------------------------
   38. โปรไฟล์ของฉัน — the visitor's own screen, from the source

   Measured off the account screen in the comps, reached by clicking the avatar
   chip (desktop) or the fifth tab (phone). Every number below is the source's.

     band        full bleed, --grad-hero, padding 18px each side, 28 at the foot
     identity    a centred 820 column: a 74px ring, the name block, an action
     column      the same centred 820, its children inset 18 — so 784 of content
     stats       three cards riding 18px UP over the band, gap 9, radius 16
     lists       radius 18 with 6 of padding, rows 47 tall at radius 13
     sign out    52 tall at radius 15, in muted ink on nothing

   The band is printed before <main> so it can reach both edges without having
   to escape the page container with viewport units, which overshoot by the
   width of the scrollbar.
   ------------------------------------------------------------------------- */

.account-hero {
  position: relative;
  /* 25 / 28 until 24 ก.ย. 2569: the cards ride 18px up over the foot, so 28
     left 10px of band under the badge and the name block looked jammed in.
     46 - 18 leaves 28 under it, and 30 on top balances that. */
  padding: 30px 18px 46px;
  background-image: var(--scrim-band), var(--grad-hero);
  background-origin: border-box;
  color: var(--on-grad);
}
.account-ident {
  display: flex;
  align-items: center;
  gap: 14px;
  /* 784, the column's content width — 820 with no inset hung the avatar and
     the แก้ไข chip 18px outside the cards on each side */
  max-width: 784px;
  margin-inline: auto;
}
.account-avatar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .18);
  border: 1px solid rgba(255, 255, 255, .4);
  font-family: var(--font-display);
  font-size: 37px;
  font-weight: 600;
  line-height: 1;
  color: var(--on-grad);
}
.account-who {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.account-who strong {
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 600;
  line-height: 1.2;
  color: var(--on-grad);
}
.account-sub { font-size: 18px; line-height: normal; color: var(--on-grad); }
.account-badge {
  align-self: flex-start;
  display: flex;
  align-items: center;
  height: 30px;
  padding: 0 12px;
  border-radius: 15px;
  background: var(--glass-on-grad);   /* white glass lightened the band under white text */
  font-size: 17px;
  font-weight: 700;
  line-height: 1.2;                   /* it inherited 1.75, a line box taller than the pill */
  color: var(--on-grad);
}
.account-edit {
  flex: none;
  display: inline-flex;
  align-items: center;
  height: 44px;
  padding: 0 18px;
  border-radius: 22px;
  background: var(--glass-on-grad);
  border: 1px solid rgba(255, 255, 255, .32);
  color: var(--on-grad);
  font-size: 18px;
  font-weight: 600;
  text-decoration: none;
}
.account-edit:hover { background: var(--glass-on-grad-hover); }

/* --- the column ---------------------------------------------------------- */
/* One measure for every block on the page, which is what the owner was asking
   for: the identity row above and everything below share the same 820, and the
   18px inset gives all of them the same 784 of content. */
main.account-container {
  max-width: 820px;
  padding-inline: 18px;
  padding-bottom: 26px;
}

.account-stats {
  display: flex;
  gap: 9px;
  margin-top: -18px;                          /* the cards ride up over the band */
  position: relative;
  z-index: 2;
}
.account-stat {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 0;
  padding: 14px 8px;
  border-radius: 16px;
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: 0 12px 28px -22px rgba(58, 17, 64, .6);
  color: inherit;
  text-decoration: none;
}
a.account-stat { transition: transform .18s var(--ease-rise); }
a.account-stat:hover { transform: translateY(-2px); }
.account-stat .stat-value {
  font-family: var(--font-display);
  font-size: 28.5px;
  font-weight: 600;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.account-stat .stat-label { font-size: 18px; line-height: normal; color: var(--muted); text-align: center; }

.account-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 18px;
  padding: 6px;
  border-radius: 18px;
  background: var(--card);
  border: 1px solid var(--border);
}
.account-list + .account-list { margin-top: 14px; }
.account-list-title {
  padding: 12px 12px 6px;
  font-size: 18px;
  font-weight: 700;
  /* no tracking: uppercase does nothing to Thai, and .05em only pulled the
     clusters apart in a face that is already narrow */
  color: var(--muted);
}

.account-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 13px 12px;
  border: 0;
  border-radius: 13px;
  background: none;
  color: var(--ink);
  font-family: inherit;
  font-size: 20px;
  /* The source leaves these rows at the face's own leading; the page default of
     1.75 makes a 47px row 54 and the whole card seven pixels taller per row.
     The floor holds the row at the source's 47: its glyphs are a shade taller
     than ours, and without it the leading alone lands two pixels short. */
  line-height: normal;
  min-height: 52px;                 /* 47 until 24 ก.ย. — 20px text on the 50-56 control ramp */
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.account-row:hover { background: var(--tint); }
.account-row.is-off { cursor: default; }
.account-row.is-off:hover { background: none; }
.account-row .row-glyph {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  color: var(--brand-ink);
}
.account-row .row-glyph .icon { width: 20px; height: 20px; }
.account-row .row-glyph-text { font-size: 18px; font-weight: 700; }
.account-row .row-label { flex: 1; min-width: 0; }
/* Where a value sits beside it, the label keeps its own width and the value
   does the wrapping. With a zero basis the label gave way first, and a long
   nickname in the ห้องรวม row squeezed it to one letter per line. Rows with
   no value keep the rule above, so a long label can still wrap. */
.account-row .row-label:has(+ .row-value) { flex: 1 0 auto; }
.account-row .row-value {
  /* allowed to shrink and wrap: the ห้องรวม row prints the nickname, up to 24
     characters, and a long Latin one pushed the row out of its card at 393 */
  flex: 0 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  text-align: right;
  font-size: 18px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.account-row .row-chevron { width: 17px; height: 17px; flex: none; color: var(--muted); }

/* the one switch on the page that does something */
.account-switch {
  flex: none;
  position: relative;
  width: 44px;
  height: 26px;
  border-radius: 13px;
  background: rgba(107, 31, 58, .16);
  box-shadow: inset 0 0 0 1.5px rgba(107, 31, 58, .55);   /* off track: 1.35 -> 3:1, fill unchanged */
  transition: background .18s ease;
}
.account-switch > span {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #FFFFFF;
  box-shadow: 0 2px 6px rgba(24, 10, 30, .3);
  transition: left .18s ease;
}
.account-row[aria-pressed="true"] .account-switch { background: linear-gradient(120deg, #F5515F, #C13584); box-shadow: none; }
.account-row[aria-pressed="true"] .account-switch > span { left: 20px; }

.account-signout { margin-top: 16px; }
.account-signout button {
  width: 100%;
  height: 52px;
  border-radius: 15px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--muted);
  font-family: var(--font-ui);
  font-size: 19px;
  font-weight: 600;
  cursor: pointer;
}
.account-signout button:hover { background: var(--tint); color: var(--ink-soft); }

.account-note {
  margin: 18px 0 0;
  font-size: 18px;
  line-height: 1.7;
  color: var(--muted);
  text-wrap: pretty;
}

/* On the phone the band runs under the translucent top bar, the same way the
   catalogue's does, and the identity row clears it instead of the desktop
   header. */
@media (max-width: 759.98px) {
  body:has(.mobile-topbar) .account-hero {
    margin-top: calc(-1 * var(--topbar-h));
    padding-top: calc(var(--topbar-h) + 16px);
  }
  .account-avatar { width: 66px; height: 66px; font-size: 31.5px; }
  .account-who strong { font-size: 29px; }
}

/* --- หอม พลัส, and the two row states the source does not have -------------- */
/* The tier card is the source's, to the value: 15 of padding at radius 18, a
   46px mark at radius 14 on the brand gradient, and a wash of the same three
   colours behind it. The button is inert — the card describes a plan, and there
   is nothing yet to sign up for or to charge it to. */
.account-plus {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 14px;
  padding: 15px;
  border-radius: 18px;
  background: linear-gradient(120deg, rgba(245, 81, 95, .14) 0%, rgba(193, 53, 132, .16) 55%, rgba(142, 69, 232, .16) 100%);
  background-origin: border-box;
  border: 1px solid rgba(193, 53, 132, .3);
}
.plus-mark {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  border-radius: 14px;
  background: var(--grad-mark);
  background-origin: border-box;
  color: var(--on-grad);
  font-size: 25px;
  line-height: 1;
}
.plus-text {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.plus-text strong { font-size: 19px; font-weight: 700; line-height: normal; color: var(--ink); }
.plus-text span { font-size: 16px; line-height: normal; color: var(--muted); }
.plus-cta {
  flex: none;
  height: 36px;
  padding: 0 14px;
  border: 0;
  border-radius: 18px;
  background: linear-gradient(120deg, #F5515F, #C13584);
  background-origin: border-box;
  color: var(--on-grad);
  font-family: var(--font-ui);
  font-size: 17px;
  font-weight: 700;
  cursor: pointer;
}
.plus-cta[disabled] { opacity: .5; cursor: not-allowed; }

/* A row that leads nowhere keeps the source's shape but not its affordances. */
.account-row.is-off { cursor: default; color: var(--ink); }
.account-row .row-glyph-star { color: var(--star); }
.account-row .row-glyph-star .icon { fill: var(--star); }
.account-switch.is-off { opacity: .45; }

.account-edit.is-off { opacity: .55; cursor: not-allowed; }

.account-signout a,
.account-signout button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 54px;
  border-radius: 15px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--muted);
  font-family: var(--font-ui);
  font-size: 20px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.account-signout button[disabled] { opacity: .55; cursor: not-allowed; }

/* ---------------------------------------------------------------------------
   39. the room, from the source

   Measured off the comp's room, which is reached by opening แชท and clicking
   the pinned entry — it only renders after a nickname is given, which is why it
   went unmeasured until now.

     band      --grad-hero, padding 16px 18px 14px, a 34px round back button and
               a 40px globe tile at radius 13
     stream    gap 11, inset 18, in the 760 column the head already uses
     message   a 34px avatar, 9 from a column that stacks the name, the badge
               and the time over a bubble at radius 16 16 16 5
     mine      no avatar, right-aligned, the action gradient, corner 16 16 5 16
     composer  a 34px avatar, a 42px field at radius 21, a 42px round send

   There is no typing indicator: the comp shows one, and showing it would mean
   inventing who is typing. Everything else is the source's.
   ------------------------------------------------------------------------- */

.chat-head { background-image: var(--scrim-band), var(--grad-hero); }
.chat-head-inner { gap: 11px; padding: 16px 0 14px; }
.chat-mark {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 13px;
  background: rgba(255, 255, 255, .16);
  border: 1px solid rgba(255, 255, 255, .22);
  color: var(--on-grad);
}
.chat-mark .icon { width: 23px; height: 23px; stroke-width: 1.9; }
/* 24, not 23: at 24px it is large text (3:1), so the band's scrim can stay lighter */
.chat-title h1 { font-size: 29px; font-weight: 600; line-height: 1.3; }
.chat-sub { font-size: 17.5px; line-height: 1.5; }

/* --- the stream ---------------------------------------------------------- */
.chat-stream { gap: 14px; padding: 14px 0; }

.chat-system {
  align-self: flex-start;
  padding: 9px 16px;
  border-radius: 13px;
  background: var(--tint);
  border: 1px solid var(--border);
  font-size: 17px;
  line-height: 1.6;
  color: var(--ink-soft);
}

.chat-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background-image: var(--grad-avatar);
  background-origin: border-box;
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 600;
  line-height: 1;
  color: var(--on-grad);
}
.chat-body { gap: 3px; }
.chat-meta { flex-wrap: wrap; align-items: baseline; gap: 2px 8px; margin: 0; }
.chat-who {
  font-size: 17.5px;
  font-weight: 700;
  line-height: 1.4;
  color: var(--ink);
  text-decoration: underline rgba(193, 53, 132, .4);
  text-underline-offset: 3px;
}
.chat-when { font-size: 16px; line-height: 1.4; color: var(--muted); }

.chat-bubble {
  margin: 0;
  max-width: 78%;
  padding: 10px 13px;
  border-radius: 16px 16px 16px 5px;
  background: var(--tint);
  border: 1px solid var(--border);
  font-size: 17.5px;
  line-height: 1.55;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.chat-msg.is-mine .chat-bubble {
  /* transparent, not 0: with no border mine came out 2px smaller than theirs
     for the same line. border-box origin keeps the gradient under it. */
  border: 1px solid transparent;
  border-radius: 16px 16px 5px 16px;
  background-image: linear-gradient(120deg, #C8385F 0%, #C13584 60%, #8E45E8 100%);
  background-origin: border-box;
  color: var(--on-grad);
}

/* --- the composer -------------------------------------------------------- */
.chat-composer {
  gap: 10px;
  /* The side padding is §29's again: it keeps the avatar, field and send on the
     room's 760 column. A flat 16 here stretched the field to 1314px at 1440
     while the messages ran 760. The phone block sets its own bottom. */
  padding: 12px max(16px, calc((100% - 760px) / 2)) calc(16px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  background: var(--nav-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.chat-avatar-me { width: 40px; height: 40px; font-size: 21px; }
.chat-composer input[name="body"] {
  height: 52px;
  min-height: 52px;
  padding: 0 18px;
  border-radius: 26px;
  background: var(--tint);
  border: 1px solid var(--border);
  font-size: 19.5px;
}
.chat-send {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  min-width: 52px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background-image: linear-gradient(120deg, #F5515F 0%, #C13584 60%, #8E45E8 100%);
  background-origin: border-box;
  color: var(--on-grad);
  cursor: pointer;
  box-shadow: 0 10px 22px -14px rgba(193, 53, 132, .95);
}
.chat-send .icon { width: 22px; height: 22px; stroke-width: 2; }
.chat-send[disabled] { opacity: .55; cursor: progress; }

/* The gate screens sit in the same column and keep the room's own proportions. */
.chat-gate .gate-mark { width: 66px; height: 66px; border-radius: 20px; }

/* 24 ก.ย. 2569: the room on the site's reading ramp. The owner asked for the
   room to be bigger and clearer; six of its text styles were under 16px (the
   time at 14). Messages 19, names 17.5, times 16, notes 17-17.5, controls
   19-20 at 50-56px, and every tap target at least 44.
   .chat-bubble is scoped through .chat-msg because the admin reuses it for
   applications and reports; site.js render() builds li.chat-msg too, so
   messages that arrive by poll get the same size as the ones printed. */
@media (min-width: 760px) { .chat-title h1 { font-size: 32px; } }
.chat-sub .dot { width: 8px; height: 8px; }
.chat-name-btn { height: auto; min-height: 44px; padding: 0 14px; gap: 6px; font-size: 18px; line-height: 1.3; }
.chat-name-btn .icon { width: 16px; height: 16px; }
/* A long nickname: the chip gives way, not the title. .chat-me was flex:none
   and the chip nowrap, so a 24-character name pushed "tongpok100" under the
   glass chip, and at 320 past the screen edge. Capped at 45% of the row, the
   name ellipsizes (chat.php wraps it in .chat-name-text) and the title keeps
   the 105px its one unbreakable word needs down to 360. */
.chat-me { flex: 0 1 auto; min-width: 0; max-width: 45%; }
.chat-name-btn { max-width: 100%; }
.chat-name-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 359.98px) { .chat-mark { display: none; } }
/* above the phone composer (20), still under the tab bar (40) */
.chat-me-panel { z-index: 30; width: min(340px, calc(100vw - 32px)); gap: 14px; padding: 18px; }
.chat-me-panel .field { gap: 8px; font-size: 18.5px; }
.chat-me-panel .field > span { line-height: 1.5; }
.chat-me-panel .field input { min-height: 52px; padding: 11px 16px; font-size: 19px; }
.chat-me-panel .field small,
.chat-me-panel .form-help { font-size: 17px; line-height: 1.55; }
.chat-me-panel .button { min-height: 50px; padding: 10px 18px; font-size: 19px; }
.chat-signout .text-link { font-size: 19px; min-height: 44px; }
/* inline, so the one link does not stretch its line of the note to 44px */
.chat-system .text-link { display: inline; min-height: 0; text-decoration: underline; text-underline-offset: 3px; }
.chat-msg { gap: 10px; }
.chat-msg .chat-bubble { padding: 11px 15px; font-size: 19px; line-height: 1.6; }
/* a 24-character name with no spaces ran 11px past a 320 screen */
.chat-who { overflow-wrap: anywhere; }
.chat-body { max-width: min(84%, 640px); }
.chat-room .chat-empty { margin: 32px 0; font-size: 19px; }
/* flash, error and the runtime stop notice sit in the room's column, not
   edge to edge */
.chat-shell > .notice { width: min(760px, 100% - 32px); margin: 14px auto 0; font-size: 18px; line-height: 1.6; }
.chat-shell > #chat-stopped { margin: 0 auto 12px; }
.chat-gate p { font-size: 19px; line-height: 1.65; }
.chat-gate .button { min-height: 54px; padding: 0 26px; font-size: 20px; }
.chat-gate .text-link { font-size: 19px; min-height: 44px; }
/* the composer's avatar is decoration; at 320 the field needs its 40px */
@media (max-width: 379.98px) { .chat-avatar-me { display: none; } }

/* ---------------------------------------------------------------------------
   40. the search screen, from the source

   Measured off the comp's ค้นหา screen: a centred 900 column at gap 16, a
   location card, then one filter card holding groups at gap 18 over a
   clear/apply footer.

     card      radius 18 over 16/15 of padding, white on the page's hairline
     head      the title at 18/700 display, a caret's worth of space beside it
     label     11.5/700 uppercase at .05em, in muted ink
     check     a 2-up grid at gap 8; each card 11/12 of padding at radius 14
               with a 19px box at radius 7 — brand-filled when on, hairline off
     pill      34 tall, 0 13, radius 17, 13.5/500
     footer    a 46px outline clear beside a 46px gradient apply

   The controls with nothing behind them keep their place and lose their state:
   ระยะทาง needs a position nothing here measures, and three of the four
   attributes need reviews that do not exist. A filter that looks live and
   changes nothing is worse than one that says it is waiting.
   ------------------------------------------------------------------------- */

/* 900 of column plus the page's own 18 each side: the source caps the COLUMN
   at 900 and hangs the gutter outside it, so capping the two together would
   leave the card 36 short. The 25 on top plus our 71px header is the source's
   96 from the top of the page. */
main.search-screen {
  max-width: 936px;
  padding: 25px 18px 20px;
}
.search-column { display: flex; flex-direction: column; gap: 16px; }

.search-where {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 14px;
  border-radius: 14px;
  background: var(--card);
  border: 1px solid var(--border);
}
.search-where > .icon { width: 17px; height: 17px; flex: none; color: #D62976; stroke-width: 1.9; }
/* flex:1 matters now that this holds a <select> rather than a short string:
   without it the column takes its basis from content, the select's width:100%
   resolves against nothing, and the card collapses to the width of its icon. */
.search-where > span { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.search-where small {
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: normal;
  color: var(--muted);
}
.search-where strong { font-size: 18px; font-weight: 700; line-height: normal; color: var(--ink); }

.filter-card {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 16px 15px;
  border-radius: 18px;
  background: var(--card);
  border: 1px solid var(--border);
}
.filter-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.filter-card-head h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 700;
  line-height: normal;
  letter-spacing: normal;
  color: var(--ink);
}
.filter-card-body { display: flex; flex-direction: column; gap: 18px; }

.filter-group { display: flex; flex-direction: column; gap: 8px; }
.filter-label {
  font-size: 14.5px;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  line-height: normal;
  color: var(--muted);
}
.filter-note { margin: 0; font-size: 15.5px; line-height: 1.55; color: var(--muted); }

/* --- the attribute cards -------------------------------------------------- */
.check-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.check-card {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 11px 12px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: transparent;
  font-size: 17px;
  font-weight: 500;
  line-height: normal;
  color: var(--ink-soft);
  cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}
.check-card input { position: absolute; opacity: 0; width: 0; height: 0; }
.check-box {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 19px;
  height: 19px;
  border-radius: 7px;
  border: 1.5px solid var(--check-ring);   /* --border was 1.24 */
  background: transparent;
  transition: background .18s ease, border-color .18s ease;
}
.check-box .icon { width: 11px; height: 11px; stroke-width: 3.4; color: #FFFFFF; opacity: 0; }
.check-card:has(input:checked),
.check-card.on {
  border-color: rgba(193, 53, 132, .42);
  background: rgba(193, 53, 132, .1);
  font-weight: 600;
  color: var(--brand-ink);
}
.check-card:has(input:checked) .check-box,
.check-card.on .check-box {
  border-color: transparent;
  background: linear-gradient(135deg, #F5515F, #C13584);
  background-origin: border-box;
}
.check-card:has(input:checked) .check-box .icon,
.check-card.on .check-box .icon { opacity: 1; }
.check-card:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }

/* --- the pills ------------------------------------------------------------ */
.pill-row { display: flex; flex-wrap: wrap; gap: 7px; }
.pill {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 13px;
  border-radius: 17px;
  border: 1px solid var(--border);
  background: transparent;
  font-size: 16.5px;
  font-weight: 500;
  line-height: normal;
  color: var(--ink-soft);
  cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}
.pill input { position: absolute; opacity: 0; width: 0; height: 0; }
.pill:has(input:checked),
.pill.on {
  border-color: rgba(193, 53, 132, .42);
  background: rgba(193, 53, 132, .1);
  font-weight: 600;
  color: var(--brand-ink);
}
.pill:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }

/* A control that is waiting on data it does not have yet. */
.pill.is-off,
.check-card.is-off { opacity: .45; cursor: not-allowed; }

/* --- the plain fields ----------------------------------------------------- */
.filter-input {
  width: 100%;
  height: 42px;
  padding: 0 14px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--tint);
  font-family: var(--font-ui);
  font-size: 17.5px;
  color: var(--ink);
}
.filter-input:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.filter-pair { display: flex; gap: 8px; }
.filter-pair label { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.filter-pair span { font-size: 15.5px; line-height: normal; color: var(--muted); }

/* --- the footer ----------------------------------------------------------- */
.filter-actions { display: flex; gap: 9px; }
.filter-clear {
  flex: none;
  display: inline-flex;
  align-items: center;
  height: 46px;
  padding: 0 18px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--ink-soft);
  font-size: 17.5px;
  font-weight: 600;
  text-decoration: none;
}
.filter-clear:hover { background: var(--tint); }
.filter-apply {
  flex: 1;
  height: 46px;
  border: 0;
  border-radius: 14px;
  background: linear-gradient(120deg, #F5515F 0%, #C13584 55%, #7B2FA8 100%);
  background-origin: border-box;
  color: var(--on-grad);
  font-family: var(--font-ui);
  font-size: 18px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 10px 24px -14px rgba(193, 53, 132, .9);
}

@media (max-width: 559.98px) {
  .check-grid { grid-template-columns: 1fr; }
}

/* --- the location card became the province chooser ------------------------ */
/* The source's card shows a position it has measured. Nothing here measures
   one, so the card holds the choice a visitor actually has to make: which of
   the 77 จังหวัด to look in. The geometry stays the source's — the select is
   stripped back to look like the value it replaced. */
label.search-where { cursor: pointer; }
.search-where select {
  appearance: none;
  width: 100%;
  max-width: 100%;
  text-overflow: ellipsis;
  /* A select carries a control height of its own; left alone it makes the card
     86 tall where the source's is 58. This is the height of the line of text it
     replaced. */
  height: 20px;
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font-ui);
  font-size: 18px;
  font-weight: 700;
  line-height: normal;
  color: var(--ink);
  cursor: pointer;
}
.search-where select:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
.search-where .where-caret { width: 15px; height: 15px; flex: none; color: var(--muted); }

/* The chip on the right of the search field is a control, not a caption: the
   source draws it as something you tap to change where you are looking, so it
   opens the province chooser and names the province in force. It is NOT a GPS
   reading — see DESIGN.md for what a real "ใกล้ฉัน" would need. */
a.search-area {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  text-decoration: none;
  cursor: pointer;
}
a.search-area:hover { color: var(--brand-ink); }
a.search-area .icon { width: 14px; height: 14px; flex: none; }

/* ---------------------------------------------------------------------------
   41. พื้นที่ให้บริการ — the location index

   The pattern the owner pointed at: one page per site that lists every province
   with someone in it, and under each the เขต or อำเภอ they are in, as links into
   a filtered catalogue. It reuses the search screen's column and pills, so the
   two halves of "where" — browsing and filtering — look like one idea.
   ------------------------------------------------------------------------- */

main.location-screen { max-width: 936px; padding: 25px 18px 32px; }
.location-head { margin-bottom: 4px; }
.location-head h1 {
  margin: 0 0 4px;
  font-family: var(--font-display);
  font-size: 28.5px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: normal;
  color: var(--ink);
}
.location-head p { margin: 0; font-size: 18px; line-height: 1.6; color: var(--ink-soft); }
.location-total { margin: 14px 0 0; font-size: 17px; color: var(--muted); }
.location-total strong { color: var(--ink); font-variant-numeric: tabular-nums; }

.location-block {
  margin-top: 16px;
  padding: 16px 15px;
  border-radius: 18px;
  background: var(--card);
  border: 1px solid var(--border);
}
.location-block-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}
.location-block-head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 700;
  line-height: normal;
  color: var(--ink);
}
.location-block-head h2 a { color: inherit; text-decoration: none; }
.location-block-head h2 a:hover { color: var(--brand-ink); }
.location-block-head span { font-size: 17px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* the pills here are links, so they carry a count and no input */
a.pill { text-decoration: none; }
a.pill:hover { border-color: rgba(193, 53, 132, .42); background: rgba(193, 53, 132, .1); color: var(--brand-ink); }
.pill-count {
  /* 2px read as part of the name in Thai — เขตพญาไท1 rather than a name and a
     count. This is the gap the eye needs to separate them. */
  margin-left: 7px;
  font-size: 17px;
  font-weight: 500;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
a.pill:hover .pill-count { color: inherit; }

.location-foot { margin: 20px 0 0; }

/* The source's label on the chip is ใกล้ฉัน. When a province is filtering, its
   name rides beside the label so the control still reports what it is doing. */
.search-area-at {
  padding-left: 6px;
  margin-left: 6px;
  border-left: 1px solid var(--border);
  font-weight: 600;
  color: var(--brand-ink);
}

/* ---------------------------------------------------------------------------
   42. ใกล้ฉัน

   The chip on the search field sorts the catalogue by real distance. The
   position is read from the browser, used in the page, and never sent anywhere,
   so the only thing the server ever knows is that someone loaded the page.
   ------------------------------------------------------------------------- */

a.search-area.is-on { color: var(--brand-ink); font-weight: 600; }
a.search-area.is-on .icon { color: var(--brand); }
/* no fade while locating: it took the label to 1.81 and the province name to 2.60,
   and the control is still live (a second tap calls locate() again). The note
   below the search panel already says it is waiting. */
a.search-area.is-busy { cursor: progress; }

.near-note {
  margin: 0 0 12px;
  padding: 10px 14px;
  border-radius: var(--r-field);
  background: var(--tint);
  border: 1px solid var(--border);
  font-size: 16.5px;
  line-height: 1.55;
  color: var(--ink-soft);
}
.near-note[data-tone="warn"] { color: var(--brand-ink); }
.near-note[data-tone="wait"] { color: var(--muted); }   /* opacity .8 took --ink-soft to 4.09 */

/* A measured distance is the brand's, an example one stays muted like the rest
   of the demo figures — so the two are never mistaken for each other. */
.card-km.is-real { color: var(--brand-ink); font-weight: 600; }

/* ---------------------------------------------------------------------------
   43. the account panel in the room, now that it holds a password too

   The panel behind the nickname chip carries two forms instead of one, which
   makes it roughly 480px tall — fine on most phones and not on a short one. It
   is absolutely positioned, so nothing below it can push it back into view.
   ------------------------------------------------------------------------- */

.chat-me-panel {
  max-height: calc(100dvh - 92px);  /* the header it hangs from, plus air */
  overflow-y: auto;
}
/* on a phone the tab bar covers the bottom 84px, and 100vh is the tall
   viewport there; stop the panel above the bar so ออกจากระบบ stays reachable */
@media (max-width: 759.98px) {
  .chat-me-panel { max-height: calc(100dvh - 100px - var(--bottomnav-real)); }
}

/* ---------------------------------------------------------------------------
   44. the directory layer — promoted slots, the intake form, area pages

   Built after studying the three directories the owner pointed at. These are
   the only new surfaces: a disclosure badge on a paid card, the application
   form, and the footer's link row. Everything else reuses what is already here.
   ------------------------------------------------------------------------- */

/* Paid placement has to be visible as paid placement.
   This comment used to end "...so the two never fight for the same corner",
   which was never true: the verified tick sits top-left as well, and the two
   fought for that corner from the day this layer was written — nobody could
   see it only because promotion could not be granted. The rule below is what
   actually keeps them apart. */
.provider-card { position: relative; }
/* The two corner pills share one corner, and only one of them can have it.
   .card-verify lives in .card-media and .card-promoted in .provider-card, and
   .card-media is that card's first child at width:100% with no margin and the
   card has no padding — so the two offsets resolve against the same pixel.
   .card-verify computes to top/left 8px (the base rule says 10, and :3813
   restates 8 unconditionally, which is the one that wins) and .card-promoted
   to 10px, so they overlapped almost exactly and the higher z-index buried the
   ยืนยันแล้ว badge under โปรโมท. Nobody could reach that state until now,
   because promotion could not be granted at all.
   Measured after this rule, at 1280 and at 375: verify 9..31, promoted 45..72,
   a 14px gap and no overlap. */
.provider-card:has(.card-verify) .card-promoted { top: 44px; }
.card-promoted {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 3;
  pointer-events: none;   /* a label over the card's link; see .card-verify */
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(20, 8, 26, .62);
  color: #FFF3EC;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .01em;
  backdrop-filter: blur(3px);
}

.footer-links { display: flex; flex-wrap: wrap; gap: 14px; }

.apply-form { display: flex; flex-direction: column; gap: 18px; }
.apply-form .filter-block { display: flex; flex-direction: column; gap: 12px; }
.apply-form textarea { resize: vertical; min-height: 72px; }

/* The status filter on the admin's application queue reuses .pill. */
.pill.is-on,
a.text-link.is-on { color: var(--brand-ink); font-weight: 600; }

/* ---------------------------------------------------------------------------
   45. the search sheet, measured against the comp at 1440

   Three differences, all measured side by side rather than guessed:

     .search-where   comp 57 high, ours 59
     .check-card     comp 44 high, ours 43
     .pill           comp draws a ring before the label, ours drew nothing

   Colours were already identical in both states — unselected
   rgba(122,47,140,.13) border on transparent, selected rgba(193,53,132,.42)
   border on rgba(193,53,132,.10) with #B0308A text — so only the geometry and
   the missing mark are touched here.
   ------------------------------------------------------------------------- */

/* The comp fixes these heights rather than letting content set them, which is
   why ours drifted by a pixel or two as the Thai line box changed. */
.search-where { height: 57px; }
.check-card   { min-height: 44px; }

/* The comp writes a literal ○ / ● before each choice. PSL Kanda has neither
   glyph — the comp is silently falling back to another face for that one
   character — and this project's rule is that no symbol is ever a character.
   Drawn instead: a ring that fills in when chosen, same 8px the fallback glyph
   occupies, so the label sits where the comp puts it.

   Scoped to pills that really are a choice. `.pill` is also the area link in
   /location.php and the status filter in the admin queue, and a radio dot on a
   link would be a lie about what tapping it does. */
.pill:has(input) { position: relative; }
.pill:has(input)::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1.4px solid currentColor;
  opacity: .68;   /* .5 was 2.35 */
}
.pill:has(input:checked)::before,
.pill.on:has(input)::before {
  background: currentColor;
  opacity: 1;
}

/* ---------------------------------------------------------------------------
   46. the profile's booking action, from the comp's จองคิวนี้

   The comp gives its main profile action its own size rather than the shared
   primary-button size, and ours was inheriting the shared one:

                       comp    was
     height            50      46
     border-radius     15      14
     font-size         20      15
     padding           0 26px  10px 20px

   20px is the comp's type size for a big primary action — the ค้นหา button is
   20px too, and ours already matches there. This is the one place that was
   still on the 15px body-button scale.
   ------------------------------------------------------------------------- */

.contact-open {
  height: 50px;
  min-height: 50px;
  padding: 0 26px;
  border-radius: 15px;
  font-size: 23px;
}
/* The one in the sheet, not the phone bar's copy: the bar's 72px reservation
   is built on that one being 50. A fixed height could not hold the label if it
   wraps at 320, so this one grows. */
.profile-cta .contact-open { height: auto; min-height: 56px; padding-block: 8px; }

/* ---------------------------------------------------------------------------
   47. the room on a phone

   Measured at 390x844 signed in: the composer's bottom edge sat at 576 while
   the tab bar started at 746 — a 170px dead gap with the message input marooned
   in the middle of the screen, where the comp docks it just above the bar.

   Two things were stacking. .chat-shell reserves the bar with
   `100dvh - topbar - bottomnav`, and `body:has(.bottom-nav)` reserves it AGAIN
   with padding-bottom, so the bar's height was subtracted twice. And the topbar
   term assumes a header that is always on screen, which on a phone it is not —
   the header scrolls away.

   So on a phone the shell only has to clear the bar, and the page must not
   reserve it a second time. .chat-room already carries flex:1, so the composer
   lands on the shell's bottom edge, which is now the bar's top edge.
   ------------------------------------------------------------------------- */

@media (max-width: 759.98px) {
  /* NOT padding-bottom: 0 — that was the first attempt and it buried the site
     footer. Measured after it: 98px of the footer sat behind the bar and
     elementFromPoint on both footer links returned the tab bar, so they could
     not be tapped at any scroll position. The shell was compensated; the part
     of the page BELOW the shell was not.
     The reservation stays; what changes is that the shell stops subtracting the
     bar a second time, and stops subtracting a header that scrolls away. */
  body.chat-page { padding-bottom: var(--bottomnav-real); }

  .chat-shell { min-height: calc(100dvh - var(--bottomnav-real)); }

  /* Sticky against the BAR, not the viewport: bottom:0 would park the input
     underneath a 98px bar that sits at z-index 40 above it. */
  .chat-composer {
    bottom: var(--bottomnav-real);
    z-index: 20;
    padding-bottom: 12px;
  }
}

/* ---------------------------------------------------------------------------
   48. what the phone's stacked bars really occupy

   Found by auditing the phone breakpoint and then reproducing each one at
   390x844 and 360x640 before changing anything.

   profile.php is the only page that stacks TWO fixed bars — the ฿ / ติดต่อ LINE
   bar on top of the tab bar — but the page reserved only the price bar's 72px.
   Measured: 169px of the viewport occluded, 97px of the site footer permanently
   behind it, and elementFromPoint on "สำหรับผู้ดูแล" returned .profile-bottom-bar.
   The links were there and could not be tapped, at any scroll position.

   The stacked bar also sat on --bottomnav-h PLUS the whole safe-area inset,
   while the bar beneath it is only 74px + max(24px, inset) tall — because
   --bottomnav-h already contains 24 of that inset. On a handset that opens a
   24px transparent slit between the two bars with the page scrolling through
   it. And it kept its own home-indicator padding while floating mid-screen,
   where there is no home indicator to clear: another 24px of dead bar.
   ------------------------------------------------------------------------- */

/* Reserve what is actually there. Without a tab bar (wide screens) the price
   bar is the only one, so the base case stays as it was. */
body:has(.profile-bottom-bar):has(.bottom-nav) {
  padding-bottom: calc(var(--bottomnav-real) + 72px);
}

body:has(.bottom-nav) .profile-bottom-bar {
  /* the bar's real height, not the token plus a second helping of the inset */
  bottom: var(--bottomnav-real);
  /* lifted clear of the screen edge, so there is no home indicator to clear */
  padding-bottom: 10px;
}

/* ---------------------------------------------------------------------------
   49. two things the phone audit turned up away from the bars
   ------------------------------------------------------------------------- */

/* A flex item will not shrink below its content unless it is allowed to. The
   tagline already carries overflow:hidden and text-overflow:ellipsis, and
   neither could ever fire, so a longer site name or tagline would push the
   topbar wider than the screen. */
.brand { min-width: 0; }

/* The bubble was capped twice — min(78%, 620px) on .chat-body and another 78%
   on .chat-bubble inside it — which compounds to 61% of the row, so every
   longer message wrapped earlier than the design asks. The row keeps the cap. */
.chat-bubble { max-width: 100%; }

/* ---------------------------------------------------------------------------
   50. the brand bar belongs to the catalogue, and to nothing else

   The owner circled the หอม row stacked above the room's own "ห้องรวม หอม"
   band and asked why every screen had not been checked at once. Fair, and the
   first pass got the rule wrong: it searched the comp for panels with `top:0`
   and found two, concluding that only the profile screen covered the bar. The
   comp writes `inset:0`, which is the same thing said differently.

   Re-read properly, EVERY screen panel in the mobile comp is
   `position:absolute; inset:0` with an opaque background and a z-index above
   the brand bar, which is rendered before them:

     searchDisplay z11 · favDisplay z12 · chatDisplay z13 · resultsDisplay z14
     bookingsDisplay z15 · meDisplay z17 (bottom:98px) · sortDisplay z20
     gcRoomDisplay z25 · pDisplay z30

   So the brand bar — หอม, the tagline, the four discs — shows on the HOME
   screen and on no other. Every inner screen carries its own header instead,
   and none of them carries a theme toggle or a bell: checked, they hold a back
   arrow, plus a heart on the two screens where a heart means that item.

   Each of our pages already has its own band — .hero, .filter-card's heading,
   .chat-head, .location-head, .profile-hero, .account-hero — so hiding the bar
   leaves every screen with a header, not a bare edge.

   One deliberate difference: the comp gives รายการโปรด its own panel, while
   ours filters the catalogue in place at /?saved=1. That shares .catalog-page
   with the home screen, so it keeps the bar. Splitting it would mean a separate
   page for a filtered view of the same list.
   ------------------------------------------------------------------------- */

@media (max-width: 759.98px) {
  body:not(.catalog-page) .mobile-topbar { display: none; }

  /* Nothing pulls up under a bar that is not there. §50's first version set the
     padding but left the negative margin, which put the identity band's first
     96px off the top of the screen. */
  body:not(.catalog-page) .account-hero {
    margin-top: 0;
    padding-top: 20px;
  }
}

/* ---------------------------------------------------------------------------
   51. /search.php — filters beside the results

   The owner pointed at trongpok69's /search and asked for that shape: a column
   you work down on the left, the matches beside it on the right, so you can see
   what a filter did without being sent to another page. The SHAPE was taken and
   not their filter list — theirs ends in three sliders for body measurements,
   which belongs to what that site sells.

   One column on a phone, two from 900px. The sidebar sticks on a wide screen so
   the filters stay put while the results scroll past them.
   ------------------------------------------------------------------------- */

.search-layout {
  width: min(1352px, 100% - 48px);
  margin-inline: auto;
  padding: 22px 0 40px;
  display: grid;
  gap: 22px;
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 900px) {
  /* 336, 312 until 24 ก.ย. 2569: at the raised type (.cat-opt 18px) ไกด์ / ล่ามท้องถิ่น
     needs 89px of text and a 312px sidebar leaves each category button 82, so it
     broke onto two lines and pulled its neighbour to 67px with it. 336 leaves 95. */
  .search-layout { grid-template-columns: 336px minmax(0, 1fr); gap: 26px; }
  .search-side {
    position: sticky;
    /* Clear the header, which is ALSO sticky at top:0 and z-index 30. At top:18px
       this panel parked underneath it the moment the page scrolled, and its first
       control — ค้นหาใกล้ฉัน — lost 36 of its 46px, centre included, to the header.
       Measured on the live page at 1280px, scrolled 326px:
       elementFromPoint at the middle of that button returned div.header-inner.
       Only the bottom ~10px still took a click.
       --header-h is the TALLEST the header gets (71px at 1440; 65.8px from 900
       to 960, where this simply leaves a slightly wider gap). If the header ever
       grows, this token grows with it or the bug comes back. */
    top: calc(var(--header-h) + 18px);
    max-height: calc(100dvh - var(--header-h) - 36px);
    overflow-y: auto;
    /* Open the detail block and this scrolls, and a classic scrollbar takes its
       15-17px out of the width the category buttons were sized against. Thin
       keeps ไกด์ / ล่ามท้องถิ่น on one line with the bar showing. */
    scrollbar-width: thin;
  }
}

.search-side {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 16px;
  border-radius: var(--r-panel);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
}
/* 24 ก.ย. 2569: the search page's type ramp, raised because the owner found it
   too small to read. One step per kind of text, sidebar and results alike:
     20    section titles (.side-title)
     19    controls that carry words: selects, the query box, the three buttons
     18    choices: .cat-opt, .pill, .check-card (kept in step with each other)
     17.5  group labels (.side-label), bold
     17-18 notes and slider readouts
   Nothing in the sidebar is under 16 now; the smallest was 15.5.
   .search-near is a leftover name from the old home search bar, and section 13
   still gives it that bar's 15.5px, 6px padding and left divider. Here it is a
   .button like the two at the bottom and is sized as one — .button in the
   selector so it also outranks the dark theme's divider colour. */
.search-near { width: 100%; }
.search-side .button.search-near {
  min-height: 50px;
  padding: 10px 20px;
  gap: 8px;
  border-color: var(--border);
  font-size: 19px;
}
.search-side .search-near .icon { width: 19px; height: 19px; }
.search-side select,
.search-side input[type="search"] { font-size: 19px; }
.search-side .cat-opt { gap: 7px; padding-inline: 10px; font-size: 18px; }
.search-side .cat-opt .icon { width: 19px; height: 19px; }
.search-side .pill { height: auto; min-height: 38px; padding: 0 14px; border-radius: 19px; font-size: 18px; }
.search-side .check-card { font-size: 18px; }
.search-side .filter-note,
.search-results .filter-note { font-size: 17px; }

.side-block { display: flex; flex-direction: column; gap: 10px; }
.side-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 20px;
  font-weight: 600;
  color: var(--ink);
}
.side-title .icon { width: 19px; height: 19px; color: var(--brand-ink); }

/* the collapsible detail block — closed until asked for

   Open, this is nine groups of pills and two slider pairs: the sidebar stood
   about 1090px tall and ค้นหา sat below the fold on a phone AND on a 900px
   laptop, so the commonest search on the site — pick a province, press the
   button — cost a scroll past filters most visitors never touch. Shut, the
   whole sidebar is 467px and the button is in the first screen. search.php
   reopens it when the current search is actually using one of these filters.

   With it shut the summary is the only thing between the visitor and the
   button, so it has to READ as something you press rather than as the bare
   bold line it used to be. It is shaped as one more field — the same 46px
   box, tint, border and radius as the three selects above it — so the
   sidebar reads as four controls in a row and the chevron is the only thing
   that marks this one as different. */
.side-detail > summary {
  min-height: 52px;
  padding: 11px 14px;
  border: 1px solid var(--border);
  border-radius: var(--r-field);
  background: var(--tint);
  cursor: pointer;
  list-style: none;
  transition: border-color .18s ease;
}
.side-detail > summary::-webkit-details-marker { display: none; }
.side-detail > summary:hover { border-color: rgba(214,64,143,.35); }

/* How many of the filters inside are on, printed on the row itself. A block
   that hides is a block that can hide the reason a search found two people,
   and this is what stops that being true once somebody shuts it by hand. The
   colours are .pill.on's, because that is already what "this one is selected"
   looks like everywhere else in this sidebar. */
.side-count {
  margin-left: auto;
  min-width: 22px;
  padding: 0 7px;
  border-radius: 999px;
  /* .12 was .pill.on's own alpha, and on a pill it is fine; under a 12px
     BOLD numeral it is not. Measured in the light theme, --brand-ink on the
     chip came out 4.35:1 where AA asks 4.5 for text this size. .08 lightens
     the chip enough to read 4.59 and is the same colour to look at. */
  background: rgba(193, 53, 132, .08);
  color: var(--brand-ink);
  font-size: 16px;
  font-weight: 700;
  line-height: 23px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.side-caret { margin-left: auto; transition: transform .2s ease; }
/* the badge has taken the auto margin; the caret just follows it */
.side-count + .side-caret { margin-left: 8px; }
.side-detail[open] > summary .side-caret { transform: rotate(180deg); }
.side-detail { gap: 16px; }
.side-detail[open] { padding-bottom: 2px; }

.side-group { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.side-label { font-size: 17.5px; font-weight: 700; color: var(--muted); }

/* --- the range pairs ----------------------------------------------------- */

.range-pair { display: flex; flex-direction: column; gap: 6px; }
.range-pair input[type="range"] {
  width: 100%;
  accent-color: var(--brand-ink);
  margin: 0;
}
.range-out {
  font-size: 18px;
  color: var(--ink);
  text-align: center;
}
.range-out b { font-weight: 600; color: var(--brand-ink); }

.side-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.side-actions .button { width: 100%; min-height: 50px; font-size: 19px; }

/* --- the results column --------------------------------------------------- */

.search-results { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.results-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.results-head h1 { margin: 0; font-size: 29px; font-weight: 600; }
.search-results .results-count { margin: 0; color: var(--muted); font-size: 19px; }
.search-results .card-grid { margin-top: 2px; }
/* The catalogue's 4-up grid switches on at 1024 for a page with no sidebar.
   Here it shares the width with a 336px sidebar, and at 1024 that made four
   cards 123px wide: the price and status on the photo ran 24px past the
   card and the new type had nowhere to go. As many 200px-plus columns as fit
   instead: 2 at 900-1100, 3 around 1200, still 4 at 1440 and up. */
@media (min-width: 900px) {
  .search-results .card-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
}

@media (max-width: 759.98px) {
  .search-layout { width: min(100%, 100% - 32px); padding-top: 14px; }
  .search-side { padding: 14px; }
  .results-head h1 { font-size: 25px; }
}

/* ---------------------------------------------------------------------------
   52. the phone wordmark fits the bar, whatever the name is

   The owner renamed the site to tongpok100 and the bar came apart: the wordmark
   is 41px, that name needs 156px of it, and the mark and the four discs leave
   111px on a 375px phone.  So it wrapped -- "tongpok1" over "00" -- which made
   the bar 146.7px tall while --topbar-h, the distance the hero is pulled up by,
   is still 96px.  The extra 50px landed on the hero: the tagline sat on the
   band and the headline was pushed down inside it.

   Measured on the live page before the fix, at 375px:
     bar 146.7px tall (--topbar-h 96px), wordmark 102.5px over two lines,
     brand bottom 146.7 against hero top 50.7 -- 96px of brand on the band.

   Two things change here, and the second is the one that matters:

     1. the wordmark never wraps.  One line, and an ellipsis if even the floor
        size cannot hold it, so the bar's height cannot run away again.
     2. the type is sized to the room that is actually left.  100cqi is the
        bar's own content box, --brand-reserve is everything else in the row,
        and --brand-em is how wide the name is in em -- measured in PHP, since
        CSS cannot measure a string (app/views.php brand_em()).

   .brand keeps a 72px floor so the bar stays exactly 96px tall whatever size
   the name lands on, which is what keeps the hero's pull-up honest.

   Capacity at the 15px floor: about 14 Thai characters on a 360px phone, 18 on
   a 375, 21 on a 390, 27 on a 430.  Past that the name ellipses in the bar and
   stays whole in the page title, the footer and the link's own label.
   ------------------------------------------------------------------------- */

@media (max-width: 759.98px) {
  /* 46px mark + 10px brand gap + 10px bar gap + four 34px discs and their three
     6px gaps.  Every one of those is a fixed number on a phone, so what is left
     for the name is the bar's content box minus this. */
  .mobile-topbar {
    container-type: inline-size;
    --brand-reserve: 220px;
    /* where the text column starts: the mark plus the gap after it */
    --brand-text-left: 56px;
  }

  /* The tagline sits BELOW the discs, not beside them, so it can have the whole
     bar -- it was being held to the wordmark's column and cut there on a 360px
     phone, where "nuad phaen thai" lost its last two syllables to an ellipsis
     with 40px of empty bar to its right.  The cap keeps a 100-character tagline
     inside the screen; the ellipsis it already carried still finishes the job. */
  .mobile-topbar .brand-text small {
    width: max-content;
    max-width: calc(100cqi - var(--brand-text-left));
  }

  /* the bar is 12px of padding plus this, and --topbar-h says 88 */
  .mobile-topbar .brand { min-height: 76px; }

  .mobile-topbar .brand-text strong {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* overflow:hidden clips at the padding box, and Thai stacks marks above and
       below the line box, so the clip is held off the glyphs without moving
       them: the negative margin hands the padding back to the layout. */
    padding-block: 7px;
    margin-block: -7px;
    font-size: clamp(15px, calc((100cqi - var(--brand-reserve)) / var(--brand-em, 2)), 41px);
  }

  /* A 320px phone has 276px of bar and the comp's own furniture claims 220 of
     it, which leaves 56px for the name -- tongpok100 was cut there by 3px at
     the floor.  Below 360px the mark and the discs give a little back; nothing
     on a normal phone changes. */
  @media (max-width: 359.98px) {
    .mobile-topbar { --brand-reserve: 195px; --brand-text-left: 50px; }
    .mobile-topbar .brand-mark,
    .mobile-topbar .brand-symbol { width: 40px; height: 40px; }
    .mobile-topbar .header-actions { gap: 5px; }
    .mobile-topbar .icon-btn,
    .mobile-topbar .icon-button,
    .mobile-topbar .avatar-btn { width: 30px; height: 30px; min-width: 30px; }
    .mobile-topbar .icon-btn .icon,
    .mobile-topbar .icon-button .icon { width: 16px; height: 16px; }
  }

  /* A browser without container queries drops that declaration whole and keeps
     the 41px from section 44, where the nowrap and the ellipsis still hold the
     bar to 96px -- a cut name rather than a broken band. */

  .mobile-topbar[data-brand-em="100"] { --brand-em: 1; }
  .mobile-topbar[data-brand-em="150"] { --brand-em: 1.5; }
  .mobile-topbar[data-brand-em="200"] { --brand-em: 2; }
  .mobile-topbar[data-brand-em="225"] { --brand-em: 2.25; }
  .mobile-topbar[data-brand-em="250"] { --brand-em: 2.5; }
  .mobile-topbar[data-brand-em="275"] { --brand-em: 2.75; }
  .mobile-topbar[data-brand-em="300"] { --brand-em: 3; }
  .mobile-topbar[data-brand-em="325"] { --brand-em: 3.25; }
  .mobile-topbar[data-brand-em="350"] { --brand-em: 3.5; }
  .mobile-topbar[data-brand-em="375"] { --brand-em: 3.75; }
  .mobile-topbar[data-brand-em="400"] { --brand-em: 4; }
  .mobile-topbar[data-brand-em="425"] { --brand-em: 4.25; }
  .mobile-topbar[data-brand-em="450"] { --brand-em: 4.5; }
  .mobile-topbar[data-brand-em="475"] { --brand-em: 4.75; }
  .mobile-topbar[data-brand-em="500"] { --brand-em: 5; }
  .mobile-topbar[data-brand-em="550"] { --brand-em: 5.5; }
  .mobile-topbar[data-brand-em="600"] { --brand-em: 6; }
  .mobile-topbar[data-brand-em="650"] { --brand-em: 6.5; }
  .mobile-topbar[data-brand-em="700"] { --brand-em: 7; }
  .mobile-topbar[data-brand-em="800"] { --brand-em: 8; }
  .mobile-topbar[data-brand-em="900"] { --brand-em: 9; }
  .mobile-topbar[data-brand-em="1000"] { --brand-em: 10; }
  .mobile-topbar[data-brand-em="1200"] { --brand-em: 12; }
  .mobile-topbar[data-brand-em="1400"] { --brand-em: 14; }
  .mobile-topbar[data-brand-em="1700"] { --brand-em: 17; }
  .mobile-topbar[data-brand-em="2000"] { --brand-em: 20; }
  .mobile-topbar[data-brand-em="2300"] { --brand-em: 23; }
  .mobile-topbar[data-brand-em="2600"] { --brand-em: 26; }
  .mobile-topbar[data-brand-em="3300"] { --brand-em: 33; }
  .mobile-topbar[data-brand-em="4100"] { --brand-em: 41; }
}

/* ---------------------------------------------------------------------------
   53. การแจ้งเตือน — the bell's panel, and the page behind it

   The owner asked for the bell to work the way the design shows it, so the
   shell here is the comp's, value for value: a 420px dropdown on a wide screen
   and a full screen on a phone, with one item row shared between them. The comp
   exports that row byte-identically at all three widths, so anything that
   styled it per breakpoint would be a deviation, not an adaptation.

   Measured off HOM Home Desktop (1).dc.html and HOM Home Mobile.dc.html:

     panel     420px · top 64 right 150 · radius 18 · 1px borderCol
               shadow 0 22px 50px -18px rgba(58,17,64,.5) · max-height 70vh
     header    16px 18px · title 20/600 serif · chip 28h 0 11 r14 13/600
               close 26 circle · unread label 13 tMuted margin-top 6
     list      padding 12 · gap 8 · day heading 14/700 letter-spacing .08em
     row       13px 14px · radius 16 · gap 11 · tile 36 r12 pillBg/pillFg
               title 18/600 serif 1.35 · time 14 tMuted · body 16/1.55 tBody
               dot 7px margin-top 6 · unread #D62976, read transparent
     grounds   unread = cardBg + 0 6px 16px -12px rgba(58,17,64,.5) + hairline
               read   = rgba(122,47,140,.04) light / rgba(255,255,255,.03) dark
     phone     full screen on appBg · gradient header · title 29 · back arrow,
               no close X · chip in glass · list padding 16 14 20

   Three deliberate differences from the comp, each because the comp is a
   picture and this is a page somebody uses:

   · top/right come from custom properties, not the comp's literal right:150px.
     That number is arithmetic from ITS header (48px padding + four 38px discs
     at gap 10); site.js measures the bell that was actually clicked instead.
   · ดูการแจ้งเตือนเก่ากว่านี้ is a real link here. In the comp it is a dead
     span, and on a phone this is the only way to the older list at all.
   · there is an empty state, which the comp does not have. With no announcement
     posted and no account, an empty panel is the common case, not the edge.
   ------------------------------------------------------------------------- */

.notif-panel {
  position: fixed;
  inset: var(--notif-top, 64px) var(--notif-right, 24px) auto auto;
  z-index: 60;
  flex-direction: column;
  width: 420px;
  max-width: calc(100vw - 24px);
  max-height: 70vh;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 18px;
  background: var(--card);
  color: var(--ink);
  box-shadow: 0 22px 50px -18px rgba(58, 17, 64, .5);
  overflow: hidden;
  transform-origin: top right;
}
.notif-panel:not([open]) { display: none; }
.notif-panel[open] { display: flex; }
/* The comp's wide scrim is a fully transparent click-catcher: no wash, no blur.
   The visible one is the phone's, and it is added in the phone arm below. */
.notif-panel::backdrop { background: transparent; }

.notif-head {
  flex: none;
  padding: 16px 18px;
  background: var(--card);
  border-bottom: 1px solid var(--border);
}
.notif-head-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.notif-head h2 {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-family: var(--font-display);
  font-size: 25px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
}
.notif-dismiss { flex: none; display: flex; margin: 0; }
.notif-dismiss-back { display: none; }          /* the phone's arrow, below */

.notif-chip {
  flex: none;
  /* 28px at 16px was the smallest control on either screen and the only way to
     mark everything read; 40 is a real target and holds a 17.5px line. */
  height: 40px;
  padding: 0 15px;
  border-radius: 20px;
  border: 1px solid var(--border);
  background: var(--tint);
  color: var(--ink-soft);
  font-family: var(--font-ui);
  font-size: 17.5px;
  font-weight: 600;
  cursor: pointer;
}
.notif-chip:hover { background: var(--pill-bg); }

.notif-close {
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--tint);
  color: var(--muted);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.notif-close:hover { background: rgba(193, 53, 132, .14); }

/* The comp keeps this line inside the header block, so the hairline that closes
   the block belongs to it rather than to the row above. */
.notif-unread {
  margin: 7px 0 0;
  font-size: 17.5px;
  color: var(--muted);
}

.notif-list {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  overflow-y: auto;
  scrollbar-width: none;          /* the comp's .hom-scroll hides it */
}
.notif-list::-webkit-scrollbar { width: 0; height: 0; }
.notif-list-empty { overflow: visible; }

.notif-day {
  padding: 0 4px;
  font-size: 18px;
  font-weight: 700;
  /* .08em was the comp's, on a Latin caps label. วันนี้ / เมื่อวาน are Thai
     clusters, and tracking only pulls them apart (the same reason the
     account page's group titles dropped theirs). */
  letter-spacing: normal;
  color: var(--muted);
}
/* Every heading after the first carries the group gap the comp hard-codes on
   เมื่อวาน (padding:10px 4px 0). */
.notif-day ~ .notif-day { padding: 10px 4px 0; }

.notif-item {
  display: flex;
  gap: 12px;
  width: 100%;
  padding: 14px 15px;
  border: none;
  border-radius: 16px;
  background: rgba(122, 47, 140, .04);
  box-shadow: 0 0 0 1px var(--border);
  color: var(--ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.notif-item.is-unread {
  background: var(--card);
  box-shadow: 0 6px 16px -12px rgba(58, 17, 64, .5), 0 0 0 1px var(--border);
}
.notif-item:hover { filter: brightness(.98); }

.notif-icon {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 13px;
  background: var(--pill-bg);
  color: var(--pill-fg);
  display: flex;
  align-items: center;
  justify-content: center;
}
.notif-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
/* the icon is sized here rather than left to .icon's 1.15em, so it no longer
   moves whenever the row's text size does */
.notif-icon .icon { width: 21px; height: 21px; }
.notif-line { display: flex; align-items: baseline; gap: 8px; }
.notif-title {
  flex: 1;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--ink);
}
.notif-when { flex: none; font-size: 17px; color: var(--muted); }
.notif-body {
  font-size: 19px;
  line-height: 1.55;
  color: var(--ink-soft);
  text-wrap: pretty;
}
/* A sibling of the text column, aligned to the title's baseline by margin
   rather than by flex alignment — the comp's own 7px dot at margin-top 6. */
.notif-dot {
  flex: none;
  width: 8px;
  height: 8px;
  /* centred on the title's first line, not floated near the top: the line box
     is 22 x 1.35 = 29.7, so its middle is 14.85 and half the dot is 4. */
  margin-top: 11px;
  border-radius: 50%;
  background: transparent;
}
.notif-item.is-unread .notif-dot { background: #D62976; }

.notif-older {
  flex: none;
  padding: 13px 0 15px;
  border-top: 1px solid var(--border);
  background: var(--card);
  color: var(--muted);
  font-size: 19px;
  text-align: center;
  text-decoration: none;
}
.notif-older:hover { color: var(--brand-ink); }

.notif-empty {
  padding: 24px 18px 28px;
  text-align: center;
  color: var(--ink-soft);
}
.notif-empty p { margin: 0; }
.notif-empty p + p { margin-top: 8px; font-size: 18px; color: var(--muted); }
.notif-empty strong {
  font-family: var(--font-display);
  font-size: 23px;
  font-weight: 600;
  color: var(--ink);
}

/* The read row's ground is the one value the comp themes; everything else here
   is already a token. Written at :root[data-theme] weight because the blanket
   dark surface rules are three-compound selectors and have beaten a one-class
   panel rule in this file before (section 26). */
:root[data-theme="dark"] .notif-item { background: rgba(255, 255, 255, .03); }
:root[data-theme="dark"] .notif-item.is-unread { background: var(--card); }

/* ----------------------------------------------- the phone: a screen, not a drop */

@media (max-width: 759.98px) {
  .notif-panel {
    inset: 0;
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    border: 0;
    border-radius: 0;
    background: var(--bg);
  }
  /* This one IS visible in the comp — though only for the moment the screen
     takes over, since the panel above it covers the lot. */
  .notif-panel::backdrop { background: rgba(24, 10, 30, .42); }

  .notif-head {
    padding: calc(16px + env(safe-area-inset-top)) 20px 16px;
    border-bottom: 0;
    background-color: #D93C92;
    background-image: var(--scrim-band), var(--grad-hero);
    background-origin: border-box;
  }
  .notif-head h2 { font-size: 31.5px; color: #FFF8F4; }
  .notif-head-row { gap: 12px; }
  .notif-dismiss-back { display: flex; order: -1; }
  .notif-dismiss-x { display: none; }
  .notif-back {
    width: 44px;
    height: 44px;
    padding: 0;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, .28);
    background: rgba(255, 255, 255, .12);
    color: #FFF3EE;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
  }
  .notif-back:hover { background: var(--glass-on-grad-hover); }   /* white .22 was 2.63 */
  .notif-chip {
    height: 44px;
    padding: 0 16px;
    border-radius: 22px;
    border-color: rgba(255, 255, 255, .32);
    background: var(--glass-on-grad);
    color: #FFF3EE;
    font-size: 18px;
  }
  .notif-chip:hover { background: var(--glass-on-grad-hover); }
  .notif-unread {
    margin-top: 8px;
    font-size: 19px;
    color: var(--on-grad);
  }
  /* The header is a gradient, and violet focus loses on it — the same flip the
     phone bar already makes (section 3). The dark arm is not decoration: the
     blanket :root[data-theme="dark"] :focus-visible at the top of this file is
     (0,3,0) and beats a one-class rule no matter how late it is written, which
     would put a near-black halo on the gradient. Same shape as the chip's own
     dark rule at section 3. */
  .notif-head :focus-visible {
    outline-color: #FFFFFF;
    box-shadow: 0 0 0 5px rgba(58, 17, 64, .35);
  }
  :root[data-theme="dark"] .notif-head :focus-visible {
    outline-color: #FFFFFF;
    box-shadow: 0 0 0 5px rgba(58, 17, 64, .35);
  }
  .notif-list { padding: 16px 14px 20px; }
  .notif-older { padding-bottom: calc(15px + env(safe-area-inset-bottom)); }
}

/* --------------------------------------------------- the page behind the panel */

.notif-page {
  /* A column, not a page-wide table: these are short notices, and the panel
     they also appear in is 420px. --shell would stretch one line of Thai across
     1200px. */
  width: min(100%, 720px);
  margin-inline: auto;
  padding: 22px var(--gutter) 40px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.notif-page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
/* 31.5 / 19 are the phone panel's own head values (.notif-head h2 and
   .notif-unread below): the page and the sheet are the same screen at two
   sizes, so they read at one size. */
.notif-page-title h1 { margin: 0; font-size: 31.5px; line-height: 1.25; }
.notif-page-title p { margin: 5px 0 0; font-size: 19px; color: var(--muted); }
.notif-page-readall { flex: none; margin: 0; }
/* .button-small is 16.5px at 40px tall — under the floor, and under the 44px
   target, on the page's only control. */
.notif-page-readall .button { min-height: 44px; font-size: 18px; }
/* On the page the list is the page: no scroller, no inner padding. */
.notif-page .notif-list { padding: 0; overflow: visible; }
/* With no announcement posted this is what every visitor sees, and on the page
   it was the one block with no ground: centred text floating between a
   left-aligned head and footnote. It stands where the rows would stand, so it
   is given a row's card — the unread row's ground and hairline, without the
   lift, drawn with the same box-shadow trick so the 1px costs no layout.
   --card carries its own dark value, so this needs no dark arm.
   The panel keeps the bare version: it is already inside a card. */
/* Below 360 the time and the title cannot share a line: the text column is
   186px at 320, and "เมื่อวาน 21:40" takes 69 of it, which shredded a long
   title into four lines. The time drops under the title instead. */
@media (max-width: 359.98px) {
  .notif-line { flex-wrap: wrap; row-gap: 0; }
  .notif-title { flex-basis: 100%; }
}

.notif-page .notif-empty {
  padding: 32px 20px 36px;
  border-radius: 16px;
  background: var(--card);
  box-shadow: 0 0 0 1px var(--border);
}
.notif-foot { margin: 0; font-size: 18px; line-height: 1.65; color: var(--muted); }
/* inline, so the one link does not stretch its line of the sentence to
   .text-link's 44px floor — the same fix as .chat-system .text-link */
.notif-foot .text-link { display: inline; min-height: 0; text-decoration: underline; text-underline-offset: 3px; }
/* the column already spaces its children by 14px; .notice's own 16px bottom
   margin put 30px under the one notice that ever appears here */
.notif-page > .notice { margin: 0; font-size: 18px; line-height: 1.6; }

/* Two stacked panels in the admin used to be spaced by a style attribute, which
   this site's CSP has always refused — so they were touching. */
.admin-panel + .admin-panel { margin-top: 16px; }

/* ---------------------------------------------------------------------------
   54. เรียงลำดับตาม — the sort sheet

   The owner asked for the pill to open the way the comp draws it: a sheet, not
   a native dropdown. The reason the comp is right is that each ordering gets a
   second line saying what it does, and a <select> has nowhere to put that.

   Measured off HOM Home Desktop (1).dc.html and HOM Home Mobile.dc.html:

     scrim    rgba(24,10,30,.45) + blur(2px)
     sheet    desktop: centred, 460px, max-height 88vh, radius 26
              phone:   bottom, full width, radius 26 26 0 0 (the comp's 44px
                       bottom corners are its phone frame's, not the sheet's)
              padding 10px 14px 30px · shadow 0 -18px 44px -20px rgba(24,10,30,.6)
     grab     44x4 · radius 2 · borderCol · margin 2px 0 12px · centred
     head     baseline row · padding 0 4px 8px · title serif 23/600
     ปิด      16/600 in pillFg, no border, no ground
     row      padding 14px 16px · radius 14 · gap 12 · list gap 4
              label 19px, weight 500, 700 when chosen · note 15px tMuted
              chosen ground pillBg, tick 20px in pillFg, otherwise transparent

   The tick is in the DOM on every row and merely invisible when the row is not
   the chosen one, which is what keeps the rows the same height either way —
   the comp does the same.
   ------------------------------------------------------------------------- */

.sort-sheet {
  position: fixed;
  inset: 50% auto auto 50%;
  transform: translate(-50%, -50%);
  z-index: 60;
  flex-direction: column;
  width: 460px;
  max-width: calc(100vw - 28px);
  max-height: 88vh;
  margin: 0;
  padding: 10px 14px 30px;
  border: 0;
  border-radius: 26px;
  background: var(--card);
  color: var(--ink);
  box-shadow: 0 -18px 44px -20px rgba(24, 10, 30, .6);
  overflow-y: auto;
  overscroll-behavior: contain;   /* the page behind the scrim stays put */
  scrollbar-width: none;
}
.sort-sheet::-webkit-scrollbar { width: 0; height: 0; }
/* The UA stylesheet hides a closed dialog; a browser that does not know the
   element has no such rule, and would paint the sheet over the page from the
   first frame. Stated here rather than assumed. */
.sort-sheet:not([open]) { display: none; }
.sort-sheet[open] { display: flex; }
.sort-sheet::backdrop {
  background: rgba(24, 10, 30, .45);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}

.sort-grab {
  /* A flex item in a column that is allowed to scroll: without this the handle
     is the first thing squeezed, and on any screen where the sheet overflows it
     shrinks to 0 and disappears. */
  flex: none;
  align-self: center;
  width: 44px;
  height: 4px;
  margin: 2px 0 12px;
  border-radius: 2px;
  background: var(--border);
}

.sort-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 0 4px 8px;
}
.sort-head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 25.5px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
}
.sort-dismiss { margin: 0; }
.sort-close {
  padding: 4px;
  border: 0;
  background: transparent;
  color: var(--pill-fg);
  font-family: var(--font-ui);
  font-size: 19px;
  font-weight: 600;
  cursor: pointer;
}
.sort-close:hover { text-decoration: underline; }

.sort-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.sort-option {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 14px 16px;
  border: 0;
  border-radius: 14px;
  background: transparent;
  color: var(--ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.sort-option:hover { filter: brightness(.97); background: var(--tint); }
.sort-option.is-on { background: var(--pill-bg); }
.sort-option.is-on:hover { background: var(--pill-bg); }
.sort-option-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.sort-option-label {
  font-size: 22px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink);
}
.sort-option.is-on .sort-option-label { font-weight: 700; }
.sort-option-note {
  font-size: 18px;
  line-height: 1.45;
  color: var(--muted);
}
.sort-tick {
  flex: none;
  width: 20px;
  height: 20px;
  color: transparent;
}
.sort-option.is-on .sort-tick { color: var(--pill-fg); }
.sort-tick svg { display: block; width: 20px; height: 20px; }

/* The pill that opens it is the same pill as the select's, minus the select:
   the caret is drawn by .sort-pill::after, so the room for it is padding. */
/* The comp's own trigger is `height:30px;padding:0 12px;gap:6px` with the caret
   as the third flex child — so no room has to be reserved on the right, and no
   pseudo-element is needed. Both of those are undone here. */
.sort-pill.sort-open {
  padding-right: 12px;
  /* No border reset here: section 33 gives every .sort-pill a 1px hairline on
     purpose, and this is the only pill a browser with script ever shows. */
  font-family: var(--font-ui);
  cursor: pointer;
}
.sort-pill.sort-open::after { content: none; }
/* The comp's ▾ is 12px at .7 opacity, next to a 14px funnel. */
.sort-pill .sort-caret {
  width: 12px;
  height: 12px;
  opacity: .7;
}

@media (max-width: 759.98px) {
  /* The comp's phone sheet rises from the bottom edge. Its 44px bottom corners
     belong to the phone frame it is drawn inside, not to the sheet. */
  .sort-sheet {
    inset: auto 0 0 0;
    transform: none;
    width: 100%;
    max-width: none;
    border-radius: 26px 26px 0 0;
    padding-bottom: calc(24px + env(safe-area-inset-bottom));
  }
}


/* ---------------------------------------------------------------------------
   45. ค่าบริการลงงาน — the price list, the หมอนวด's own billing page, and the
   admin's money screen.

   One accent per package, set as --plan on the card and read by everything
   inside it, so a tier is one line to retune rather than nine. The three are
   the site's own ramp — plum, amber, violet — and not the reference's blue,
   because a blue nobody else on this site uses would read as a different
   product rather than a different tier.
   ------------------------------------------------------------------------- */

/* main., not bare — 24 ก.ย. 2569.
   page_head() puts the page class on the BODY (views.php:124) and pricing.php
   puts the SAME class on its <main>, so this rule was capping the body at
   1040px with no auto margin. Measured at 1885: body 1040 at x=0, so the site
   header, the page and the footer were all 1040 wide and flush LEFT with 845px
   of blank down the right. The owner sent a screenshot of it, then asked again
   why the desktop view does not fill the screen. billing.php wore it too.
   The padding-bottom half was already dead on the body at every width:
   body:has(.bottom-nav) is (0,1,1) and beats this (0,1,0), reserving the bar
   below 760 and zeroing it above. */
main.pricing-page, main.billing-page { max-width: 1040px; padding-bottom: 40px; }
/* and the page sits on the site's own desktop measure instead of 260px
   narrower than every other screen (--shell is 1300; the catalogue grid is
   min(1300, 100% - 96)). The prose blocks below keep their own reading cap. */
main.pricing-page { max-width: 1200px; }

.pricing-hero { text-align: center; margin: 8px 0 30px; }
.pricing-hero h1 { font-size: clamp(26px, 5vw, 38px); line-height: 1.25; margin: 0 0 10px; }
.pricing-lead {
  max-width: 60ch; margin: 0 auto;
  font-size: 19px; color: var(--ink-soft); line-height: 1.75;
  /* even lines instead of a long one over a short one */
  text-wrap: balance;
}
/* The owner circled this: the lead was breaking "ฟรี 15 วันแรก" after the
   numeral, so the one promise on the page arrived in two pieces. Thai has no
   spaces inside a phrase, so the browser is free to break anywhere it finds a
   word boundary — this keeps the offer on one line. */
.pricing-lead strong { color: var(--brand-ink); white-space: nowrap; }

.pricing-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 34px; }
.pricing-step { margin: 0; }
.step-head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 16px;
}
.step-n {
  display: grid; place-items: center; width: 30px; height: 30px; flex: none;
  border-radius: 999px; background: linear-gradient(135deg, #F5695A, #C13584 35%, #7B2FA8); color: #FFF;
  font-size: 18px; font-weight: 700;
}
/* 0 1 auto, not 1 1 14ch: the h2 used to grow and shove .step-tag against the
   right edge of a 1200px row, where it read as a stray chip rather than a
   label on this step. Now the two sit together and the tag still drops to its
   own line when the heading needs the width. */
.step-head h2 { font-size: clamp(20px, 2.6vw, 23px); margin: 0; flex: 0 1 auto; }
.step-tag { white-space: nowrap; }
@media (max-width: 559.98px) { .step-tag { white-space: normal; } }
.step-tag {
  padding: 5px 12px; border-radius: 999px; background: var(--tint);
  border: 1px solid var(--border); color: var(--ink-soft);
  font-size: 17px; font-weight: 600;
}

.plan-grid { display: grid; gap: 18px; }
@media (min-width: 720px) {
  .plan-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .plan-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* No cap. It did nothing up to 452 (the content box is narrower than 420
   there anyway) and from 453 to 719 it pinned the lone Basic card at 420 flush
   left with up to 267px of nothing beside it, while Top and Super ran the full
   width directly below — the same stranding the 720 rule below was written to
   remove, one breakpoint lower. */
.plan-grid-1 { max-width: none; }
/* From the width where .plan-grid-2 becomes two columns, the single card in
   step 1 takes column one of the same two, so Basic lines up with Top below it
   rather than sitting stranded at 420 in a 1200px row. */
@media (min-width: 720px) {
  .plan-grid-1 { max-width: none; grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --- one package -------------------------------------------------------- */

.plan-card {
  --plan: var(--brand);
  --plan-soft: rgba(193, 53, 132, .10);
  --plan-ink: #FFFFFF;          /* text ON a --plan fill */
  --plan-text: var(--brand-ink); /* --plan used AS text */
  position: relative;
  display: flex; flex-direction: column; gap: 16px;
  padding: 22px 20px;
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  background: var(--card);
  box-shadow: var(--sh-card);
}
/* white on the amber fill is 3.45 and the amber as text is 3.04-3.45, so Top
   takes dark ink on its fill and a deeper amber for text. In dark every tier
   fill is light (2.06-2.58 with white), so they all take dark ink there. */
.plan-top   { --plan: #C9761A; --plan-soft: rgba(201, 118, 26, .12); --plan-ink: #1F0E2B; --plan-text: #A85800; }
.plan-super { --plan: #7B3FC4; --plan-soft: rgba(123, 63, 196, .12); --plan-text: #7B3FC4; }
:root[data-theme="dark"] .plan-top   { --plan: #F0A44E; --plan-soft: rgba(240, 164, 78, .14); --plan-text: #F0A44E; }
:root[data-theme="dark"] .plan-super { --plan: #BC8CF0; --plan-soft: rgba(188, 140, 240, .16); --plan-text: #BC8CF0; }
:root[data-theme="dark"] .plan-card,
:root[data-theme="dark"] .plan-edit { --plan-ink: #1F0E2B; }

/* The one the owner wants looked at first gets a ring, not a bigger font. */
.plan-super { border-color: color-mix(in srgb, var(--plan) 45%, transparent); }

.plan-ribbon {
  position: absolute; top: -11px; right: 18px;
  padding: 4px 12px; border-radius: 999px;
  background: var(--plan); color: var(--plan-ink);
  font-size: 16.5px; font-weight: 700; letter-spacing: .01em;
  box-shadow: 0 4px 12px rgba(40, 12, 50, .22);
}

.plan-head { display: flex; align-items: center; gap: 13px; }
.plan-glyph {
  display: grid; place-items: center; width: 46px; height: 46px; flex: none;
  border-radius: 14px; background: var(--plan-soft); color: var(--plan);
}
.plan-glyph .icon { width: 24px; height: 24px; }
.plan-head h3 { margin: 0; font-size: 22px; color: var(--plan-text); }
.plan-head p  { margin: 2px 0 0; font-size: 17px; color: var(--ink-soft); line-height: 1.6; text-wrap: pretty; }

.plan-price { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; }
.plan-price strong { font-size: 36.5px; line-height: 1; color: var(--ink); }
.plan-price s { font-size: 18px; color: var(--muted); }
.plan-days { font-size: 17px; color: var(--ink-soft); }
.plan-off {
  padding: 3px 9px; border-radius: 999px;
  background: var(--plan-soft); color: var(--plan-text);
  font-size: 16.5px; font-weight: 700;
}

.plan-does { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.plan-does li { display: flex; gap: 9px; align-items: flex-start; font-size: 18px; line-height: 1.65; text-wrap: pretty; }
.plan-does .icon { width: 18px; height: 18px; flex: none; margin-top: 4px; color: var(--plan); }
.plan-note { margin: 0; font-size: 17px; color: var(--ink-soft); }
.plan-action { margin-top: auto; }
.plan-action .button, .plan-action form { width: 100%; }
.plan-action .button { justify-content: center; }
.plan-top .plan-action .button-primary,
.plan-super .plan-action .button-primary {
  background: var(--plan); background-image: none; border-color: transparent;
  color: var(--plan-ink);
}

/* --- how it works ------------------------------------------------------- */

.pricing-how, .pricing-faq { margin-top: 38px; }
.pricing-how h2, .pricing-faq h2 { font-size: 23px; margin: 0 0 16px; }
.faq-item > summary { text-wrap: pretty; }
.how-steps { list-style: none; margin: 0 0 20px; padding: 0; display: grid; gap: 14px; }
/* 900, not 760. At 768 the three columns left each text block 150px wide —
   measured live — which is the narrowest measure on the page, in the one block
   that explains how the money actually moves. 900 is where .pricing-how takes
   its 880 reading cap anyway, so the 3-up now starts only once a column can
   hold about 200px of text. 760-899 keeps one full-width column. */
@media (min-width: 900px) { .how-steps { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.how-steps li {
  display: flex; gap: 12px; padding: 16px;
  border: 1px solid var(--border); border-radius: var(--r-panel); background: var(--card);
}
.how-n {
  display: grid; place-items: center; width: 30px; height: 30px; flex: none;
  border-radius: 999px; background: var(--tint); color: var(--brand-ink);
  font-size: 17px; font-weight: 700;
}
.how-steps strong { display: block; font-size: 19px; margin-bottom: 5px; }
.how-steps p { margin: 0; font-size: 17.5px; color: var(--ink-soft); line-height: 1.7; text-wrap: pretty; }

.faq-item {
  border: 1px solid var(--border); border-radius: var(--r-field);
  background: var(--card); padding: 0 16px; margin-bottom: 10px;
}
/* It was a disclosure with no disclosure: list-style:none, the webkit marker
   hidden, and nothing put back — four flat rows that never said they opened.
   The owner's screenshot shows them looking like dead cards. Everywhere else
   on this site a thing that opens carries a chevron (.side-caret on the search
   filters, .chat-name-btn, .sort-pill::after), so this gets one too, drawn the
   way .sort-pill draws its own. */
.faq-item > summary {
  position: relative;
  display: flex; align-items: center;
  min-height: 44px;
  cursor: pointer; list-style: none;
  padding: 13px 34px 13px 0;
  font-size: 19px; font-weight: 600; color: var(--ink);
}
.faq-item > summary::-webkit-details-marker { display: none; }
.faq-item > summary::after {
  content: "";
  position: absolute; right: 6px; top: 50%;
  width: 9px; height: 9px; margin-top: -7px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
  transition: transform .18s ease;
}
.faq-item[open] > summary::after { margin-top: -2px; transform: rotate(-135deg); }
@media (prefers-reduced-motion: reduce) { .faq-item > summary::after { transition: none; } }
.faq-item > p { margin: 0 0 14px; font-size: 18px; line-height: 1.8; color: var(--ink-soft); text-wrap: pretty; }

.pricing-cta { margin-top: 34px; text-align: center; display: grid; gap: 10px; justify-items: center; }
/* The cards want the whole 1200; a paragraph does not. Without this the how-to
   and the FAQ ran a Thai line the full width of the page. */
@media (min-width: 900px) {
  .pricing-how,
  .pricing-faq,
  .pricing-cta { max-width: 880px; margin-inline: auto; }
}
/* the page's one real action, on the 19-20 / 50-56 control ramp rather than
   .button's body scale */
.pricing-cta .button { min-height: 56px; padding-inline: 30px; font-size: 20px; }
/* (There used to be a .pricing-page .form-help { 17px } here, with a comment
   saying the site-wide default was 15.5 and under the floor. The default is 17
   now, so the rule set the value it would inherit and the comment was false.
   Both gone. The page's two help lines still read 17, from the base.) */

/* --- where the money goes ----------------------------------------------- */

.pay-box { padding: 18px; border: 1px solid var(--border); border-radius: var(--r-panel); background: var(--card); }
.pay-box h3 { margin: 0 0 12px; font-size: 19px; }
.pay-methods { display: grid; gap: 18px; }
@media (min-width: 620px) { .pay-methods { grid-template-columns: auto minmax(0, 1fr); align-items: start; } }
.pay-qr { margin: 0; text-align: center; }
/* White behind it always: a QR inverted by a dark theme does not scan.

   Sized on BOTH axes, because the picture is whatever the owner's banking app
   exported — very often a whole-screen capture rather than a cropped square.
   The old rule capped width alone at 240, so a tall screenshot was squeezed
   into a narrow column and the code inside it came out small: 777x1200 drew at
   220x340, with the QR itself only part of that. Giving each axis its own cap
   lets a square fill 320 and a tall one be held by its height instead.

   width/height stay auto on purpose. Pairing width:100% with max-height would
   constrain the two axes independently, and THAT is what actually deforms a
   QR — the squares stop being square and scanners start failing. auto keeps
   the real ratio and simply fits inside both caps. It also never enlarges a
   small upload past its own pixels, which would only add blur, never detail. */
.pay-qr img {
  width: auto; height: auto;
  max-width: min(100%, 320px); max-height: 440px;
  display: block; margin: 0 auto;
  background: #FFFFFF; padding: 10px; border-radius: 14px; border: 1px solid var(--border);
}
.pay-qr figcaption { margin-top: 8px; font-size: 15.5px; color: var(--muted); }
.pay-lines { margin: 0; display: grid; gap: 12px; }
.pay-lines > div { display: grid; gap: 3px; }
.pay-lines dt { font-size: 15.5px; font-weight: 700; color: var(--muted); }
.pay-lines dd { margin: 0; font-size: 18.5px; color: var(--ink); }
.pay-account { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pay-number { font-variant-numeric: tabular-nums; letter-spacing: .04em; font-weight: 600; }
.pay-note { margin: 0; font-size: 16.5px; color: var(--ink-soft); line-height: 1.75; }

/* --- a หมอนวด's own page ------------------------------------------------- */

.billing-head { margin: 6px 0 22px; }
.billing-who { margin: 0; font-size: 17px; color: var(--muted); }
.billing-head h1 { margin: 2px 0 10px; font-size: clamp(22px, 4vw, 30px); }
.billing-state {
  display: inline-flex; align-items: center; gap: 8px; margin: 0;
  padding: 8px 14px; border-radius: 999px;
  background: var(--tint); border: 1px solid var(--border);
  font-size: 17px; color: var(--ink-soft); line-height: 1.6;
}
.billing-state .icon { width: 17px; height: 17px; flex: none; }
.billing-state-paid  { color: #1B7A4B; background: rgba(52,199,123,.14); border-color: rgba(52,199,123,.3); }
.billing-state-lapsed{ color: #A32222; background: rgba(214,64,64,.12); border-color: rgba(214,64,64,.28); }
:root[data-theme="dark"] .billing-state-paid   { color: #6FE0A5; }
:root[data-theme="dark"] .billing-state-lapsed { color: #F09A9A; }

.pay-now, .billing-plans, .billing-history { margin-top: 26px; }
.pay-now h2, .billing-plans h2, .billing-history h2 { font-size: 22px; margin: 0 0 14px; }
.pay-summary { display: grid; gap: 14px; margin-bottom: 12px; }
@media (min-width: 620px) { .pay-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.pay-figure {
  padding: 14px 16px; border: 1px solid var(--border);
  border-radius: var(--r-field); background: var(--card);
}
.pay-figure span { display: block; font-size: 15.5px; color: var(--muted); margin-bottom: 4px; }
.pay-figure strong { font-size: 22px; color: var(--ink); }
.pay-ref { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-family: inherit; letter-spacing: .04em; }

.slip-form { margin-top: 18px; display: grid; gap: 14px; }
.slip-cancel { margin-top: 12px; }

.billing-table { width: 100%; border-collapse: collapse; font-size: 16.5px; }
.billing-table th, .billing-table td {
  text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: top;
}
.billing-table th { font-size: 15.5px; color: var(--muted); font-weight: 700; }
.billing-table td small { display: block; color: var(--muted); font-size: 15px; margin-top: 3px; }
.billing-table .mono { font-variant-numeric: tabular-nums; letter-spacing: .03em; }
.billing-foot { margin-top: 28px; }

.order-tag {
  display: inline-block; padding: 3px 10px; border-radius: 999px;
  font-size: 15px; font-weight: 700;
  background: var(--tint); color: var(--ink-soft);
}
.order-paid     { background: rgba(52,199,123,.16); color: #0B7143; }
.order-review   { background: rgba(201,118,26,.16); color: #8A5310; }
.order-rejected { background: rgba(214,64,64,.14); color: #A32222; }
:root[data-theme="dark"] .order-paid     { color: #6FE0A5; }
:root[data-theme="dark"] .order-review   { color: #F0BE7E; }
:root[data-theme="dark"] .order-rejected { color: #F09A9A; }

/* --- the admin's queue --------------------------------------------------- */

.order-row {
  display: grid; gap: 14px; padding: 16px 0;
  border-bottom: 1px solid var(--border);
}
@media (min-width: 760px) { .order-row { grid-template-columns: minmax(0,1fr) 120px auto; align-items: start; } }
.order-main p { margin: 6px 0 0; font-size: 16.5px; color: var(--ink-soft); }
.order-main .mono { font-variant-numeric: tabular-nums; letter-spacing: .03em; }
.order-slip img {
  width: 100%; max-width: 120px; border-radius: 10px; border: 1px solid var(--border); display: block;
}
.order-actions { display: grid; gap: 8px; align-content: start; }
.order-reject { display: flex; gap: 8px; }
.order-reject input { min-height: 34px; font-size: 16px; }

.billing-link { font-size: 15.5px; min-width: 0; flex: 1 1 200px; }
.qr-preview {
  display: block; width: 180px; height: auto; background: #FFFFFF;
  padding: 8px; border-radius: 12px; border: 1px solid var(--border);
}

/* --- the tier badge on a card -------------------------------------------- */

/* .card-promoted already positions and paints this; the tier only recolours it,
   so a Super Star and a Top Star do not look like the same purchase. */
.card-plan-top   { background: rgba(140, 70, 8, .90); }   /* .82 was 3.59 over a pale portrait */
.card-plan-super { background: rgba(78, 32, 132, .82); }


/* ---------------------------------------------------------------------------
   46. หลังบ้าน — ขนาดตัวอักษร และการจัดกลุ่มของหน้าค่าบริการ

   เจ้าของอ่านหน้าค่าบริการแล้วบอกว่าตัวหนังสือเล็กเกินไป ป้ายกำกับช่องกรอกเดิม
   อยู่ที่ 13px คำอธิบายใต้ช่อง 12.5px ซึ่งเป็นขนาดที่อ่านไทยยากจริง สระบนกับ
   วรรณยุกต์ชิดกันจนแยกไม่ออกที่ขนาดนั้น ค่าที่ขยับใช้ทั้งหลังบ้าน ไม่ใช่เฉพาะ
   หน้านี้ เพราะป้ายกำกับทุกหน้าเป็นคลาสเดียวกันหมด

   หน้าบ้านไม่ถูกแตะ: ตรงนั้นคนอ่านเป็นย่อหน้าและวัดกับแบบไว้แล้ว ส่วนหลังบ้าน
   อ่านเป็นป้ายสั้นๆ ทีละคำ และคนอ่านคือเจ้าของร้านคนเดียวที่ต้องนั่งทำงานกับมัน
   ------------------------------------------------------------------------- */

/* All four rules that stood here were dead: the same four selectors are
   restated further down this file, so the later copies were already the ones
   rendering — .field 17.5 (identical), .field small/.form-help 16 (not the
   16.5 written here), .rail-check strong 18.5 (not 18) and .rail-check small
   16 (not 16.5). Two rules for one selector is how the .form-help default
   drifted under the floor in the first place, so the dead half goes rather
   than sitting here looking authoritative.
   (I nearly left .rail-check small behind on a first pass, having checked the
   other three for a later twin and not that one. The grep is the check, not
   the reading.)

   24 ก.ย. 2569 รอบสอง: หกบรรทัดที่เคยอยู่ตรงนี้ก็เป็นเงาเหมือนกัน —
   .admin-main .panel-head h2/p · .admin-title h1/p · .admin-main .notice ·
   .admin-main .button-small ถูกเขียนซ้ำในหมวด 48 (8017-8023) ซึ่งอยู่ทีหลัง
   และชนะทุกตัว พิสูจน์จากของจริง: บรรทัดที่ลบไปบอกว่า h1 เป็น 26px แต่หน้าเว็บ
   วัดได้ 31px ซึ่งเป็นค่าของหมวด 48 */

/* ช่องกรอกเอง — เจาะจงกว่า input[type="number"] ด้วย :not() สองชั้น
   แต่ไม่แตะ checkbox/radio ซึ่ง font-size ไม่มีผลกับมันอยู่แล้ว */
.admin-main input:not([type="checkbox"]):not([type="radio"]),
.admin-main select,
.admin-main textarea { font-size: 18.5px; }

.billing-table { font-size: 17.5px; }
.billing-table th { font-size: 16px; }
.billing-table td small { font-size: 16px; }

/* --- หน้าค่าบริการ: สี่กล่อง ฟอร์มเดียว ---------------------------------- */

.billing-form { margin-top: 16px; }
.billing-form + .admin-panel { margin-top: 16px; }
.billing-form .rail-check + .admin-grid,
.billing-form .plan-edit + .admin-grid { margin-top: 16px; }

/* กล่องราคาของแต่ละแพ็กเกจ รับสี --plan จาก .plan-top / .plan-super ที่ประกาศ
   ไว้แล้วในหมวด 45 — ชื่อแพ็กเกจกับไอคอนจึงเป็นสีเดียวกับการ์ดที่ลูกค้าเห็น */
.plan-edit {
  --plan: var(--brand);
  --plan-soft: rgba(193, 53, 132, .10);
  --plan-ink: #FFFFFF;
  --plan-text: var(--brand-ink);
  min-width: 0;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-field);
  background: var(--bg);
}
.plan-edit + .plan-edit { margin-top: 12px; }
.plan-edit-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 11px;
  margin-bottom: 14px;
}
.plan-edit-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 11px;
  background: var(--plan-soft);
  color: var(--plan);
}
.plan-edit-mark .icon { width: 18px; height: 18px; }
.plan-edit-name {
  /* ชื่อแพ็กเกจเป็นหัวข้อของกล่อง ต้องนำคำโปรยที่ขึ้นไปเป็น 17.5 */
  margin: 0;
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--plan-text);
}
.plan-edit-lead { flex: 1 1 210px; font-size: 17.5px; line-height: 1.6; color: var(--muted); }

.qr-edit { display: grid; gap: 16px; align-items: start; }
.qr-current {
  display: grid;
  gap: 8px;
  justify-items: center;
  margin: 0;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-field);
  background: var(--bg);
}
.qr-current figcaption { font-size: 17px; color: var(--muted); }
.qr-fields { display: grid; gap: 14px; align-content: start; min-width: 0; }

@media (min-width: 620px) {
  .qr-edit.has-qr { grid-template-columns: 204px minmax(0, 1fr); }
}

/* แถบบันทึก ลอยติดขอบล่างจอระหว่างที่ยังอยู่ในฟอร์ม */
.billing-save {
  position: sticky;
  bottom: 12px;
  z-index: 4;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 18px;
  margin-top: 16px;
  padding: 14px 18px;
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  background: var(--card);
  box-shadow: var(--sh-lift);
}
.billing-save .form-help { margin: 0; flex: 1 1 240px; }
.billing-save .button { flex: none; }

/* ราคา · จำนวนวัน · ราคาเดิม อ่านเป็นบรรทัดเดียวของแพ็กเกจนั้น ไม่ใช่สองคอลัมน์
   แล้วเหลือช่องที่สามห้อยอยู่คนเดียวข้างล่าง */
@media (min-width: 760px) {
  .plan-edit-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* จอเตี้ย (โน้ตบุ๊กเล็ก หรือหน้าต่างที่ย่อไว้) แถบบันทึกลอยกินพื้นที่อ่านไปมาก
   เกินคุ้ม ปล่อยให้มันอยู่ท้ายฟอร์มตามปกติ */
@media (max-height: 620px) {
  .billing-save { position: static; }
}


/* ---------------------------------------------------------------------------
   47. หลังบ้าน — กวาดทั้งหมด

   สามอย่างที่ซ้ำกันทุกหน้า และทำให้หลังบ้านดูโล่งกว่าที่ควร:

   1. .admin-empty เป็นการ์ดกว้าง 520px ลอยกลางแผงที่กว้าง 1200px — การ์ดซ้อน
      การ์ด และข้อความบรรทัดเดียวที่บอกแค่ว่า "ว่าง" ไม่ได้บอกว่าทำอะไรต่อ
   2. .stat วางไอคอนชิดซ้ายแล้วปล่อยที่ว่างครึ่งใบ เพราะตัวการ์ดยืดเต็ม 1fr แต่
      เนื้อในกว้างแค่ 150px  สลับเป็น ป้ายบน–ตัวเลขใหญ่ล่าง–ไอคอนชิดขวา
   3. ตัวเลขที่ต้องเทียบกันระหว่างแถว (สถิติ) ถูกวางเป็นการ์ดใบเล็ก อ่านไม่ออก
      ว่าใครมากกว่าใคร  ของแบบนั้นคือตาราง
   ------------------------------------------------------------------------- */

/* --- สถานะว่าง ------------------------------------------------------------ */

.admin-empty {
  display: grid;
  justify-items: center;
  gap: 10px;
  max-width: none;
  margin: 0;
  padding: 40px 24px 42px;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  text-align: center;
}
.empty-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 54px;
  margin-bottom: 2px;
  border-radius: var(--r-pill);
  background: var(--tint);
  color: var(--brand-ink);
}
.empty-mark .icon { width: 25px; height: 25px; }
.admin-empty h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 700;
  line-height: 1.4;
}
.admin-empty p {
  margin: 0;
  max-width: 48ch;
  color: var(--muted);
  font-size: 17.5px;
  line-height: 1.7;
}
.admin-empty .button { margin-top: 6px; }

/* --- การ์ดตัวเลข ---------------------------------------------------------- */

/* ป้ายอยู่บน ตัวเลขอยู่ล่าง: column-reverse สลับลำดับที่เห็นโดยไม่แตะ HTML
   ซึ่งยังเป็น <strong> ก่อน <small> ตามลำดับความสำคัญของเนื้อหา */
.stat { grid-template-columns: minmax(0, 1fr) auto; gap: 14px; padding: 15px 18px; }
.stat-body { display: flex; flex-direction: column-reverse; }
.stat-mark { order: 1; }
.stat-body strong { font-size: 32.5px; line-height: 1.2; letter-spacing: -.01em; }
.stat-body small { font-size: 16.5px; margin-bottom: 4px; }

/* การ์ดที่กดได้ — หน้าใบสมัครใช้มันเป็นตัวกรอง */
.stat-link {
  color: inherit;
  text-decoration: none;
  transition: border-color .18s ease, box-shadow .18s ease, transform .18s var(--ease-rise);
}
.stat-link:hover { border-color: var(--tint); box-shadow: var(--sh-lift); transform: translateY(-1px); }
.stat-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.stat-link.is-on { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand) inset; }
.stat-link.is-on .stat-mark { background: var(--grad-mark); color: #FFFFFF; }

/* --- ตาราง --------------------------------------------------------------- */

.table-scroll { overflow-x: auto; margin-inline: -4px; padding-inline: 4px; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 17.5px; }
.admin-table th,
.admin-table td {
  text-align: left;
  padding: 11px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.admin-table thead th {
  font-size: 16px;
  font-weight: 700;
  color: var(--muted);
  white-space: nowrap;
}
.admin-table tbody tr:last-child td { border-bottom: 0; }
.admin-table tbody tr:hover { background: var(--tint); }
.admin-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.admin-table .num.is-blank { color: var(--muted); }
.admin-table td a { color: var(--ink); font-weight: 600; text-decoration: none; }
.admin-table td a:hover { text-decoration: underline; }
.admin-table td small { display: block; color: var(--muted); font-size: 16px; margin-top: 3px; }
.admin-table .mono { font-variant-numeric: tabular-nums; letter-spacing: .03em; }

.stats-table th:first-child,
.stats-table td:first-child { min-width: 220px; }
.row-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 5px; }
.row-district { color: var(--muted); font-size: 16px; }

/* หัวเรื่องหน้ากับชิปช่วงเวลาอยู่คนละฝั่งของบรรทัดเดียวกัน */
.pill-row.admin-title-actions { gap: 7px; }

/* เมนูหลังบ้านถูกบีบจนอ่านไม่ออกมาตั้งแต่วันแรก ทุกขนาดจอที่ต่ำกว่า 1100px
   แถบเมนูเป็น overflow-x:auto ก็จริง แต่ลิงก์แต่ละอันมี overflow:hidden (ไว้ตัด
   เส้นใต้ ::after) บวก flex-shrink:1 ปริยาย ตัวมันเลยหดลงต่ำกว่าความกว้างเนื้อหา
   ได้เรื่อยๆ แทนที่จะดันให้แถวเลื่อน วัดที่ 375px: ต้องการ 82px ได้จริง 39px
   ทุกป้ายจึงถูกตัดกลางคำ และ scrollWidth เท่ากับ clientWidth คือเลื่อนไม่ได้เลย */
.admin-nav a { flex: none; }

/* ข้อความในสถานะว่างเป็นประโยคสั้น ไม่ใช่ย่อหน้า ปล่อยให้บรรทัดสุดท้ายเหลือคำเดียว
   จะดูเหมือนพิมพ์ค้าง */
.admin-empty h3,
.admin-empty p { text-wrap: balance; }

/* บนมือถือการ์ดตัวเลขสี่ใบเรียงเดี่ยวกินความสูง 760px ก่อนถึงเนื้อหาจริง */
@media (max-width: 559px) {
  .admin-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .stat { padding: 12px 13px; gap: 8px; }
  .stat-body strong { font-size: 25.5px; }
  .stat-body small { font-size: 15.5px; margin-bottom: 3px; }
  .stat-mark { width: 32px; height: 32px; }
  .stat-mark .icon { width: 16px; height: 16px; }
  .admin-empty { padding: 30px 12px 32px; }
}

/* หมวด 48 ใส่กลับมาทั้งหมวด: หายไปพร้อมการสร้างไฟล์ใหม่ตอนย้อนสี (อีกเซสชัน
   23 ก.ย.) ซึ่งตั้งใจย้อนแค่สีกับม่าน หมวดนี้ไม่มีสีสักค่า มีแต่ขนาดตัวอักษรและ
   โครงของหลังบ้าน ที่เจ้าของสั่งให้เก็บไว้ */
/* ---------------------------------------------------------------------------
   48. หลังบ้าน — สเกลตัวอักษร โครงหน้าแก้ไขโปรไฟล์ และแถวปุ่มในแถบขวา

   ขยับเฉพาะหลังบ้าน ไม่แตะหน้าบ้าน หน้าบ้านวัดกับแบบไว้แล้วและขยับไปรอบหนึ่ง
   ส่วนหลังบ้านเป็นเครื่องมือที่คนคนเดียวนั่งทำงานด้วยทั้งวัน ตัวใหญ่กว่าหน้าบ้าน
   จึงเป็นเรื่องถูกต้อง ไม่ใช่ความไม่สม่ำเสมอ
   ------------------------------------------------------------------------- */

.admin-main .field { font-size: 17.5px; }
.admin-main .field > span { line-height: 1.55; }
.admin-main .field small,
.admin-main .form-help { font-size: 17px; line-height: 1.75; }
.admin-main .rail-check strong { font-size: 18.5px; line-height: 1.45; }
.admin-main .rail-check small { font-size: 17px; line-height: 1.7; }
.admin-main .panel-head h2 { font-size: 23px; }
.admin-main .panel-head p { font-size: 17.5px; line-height: 1.65; }
.admin-title h1 { font-size: 31px; }
.admin-title p { font-size: 19px; line-height: 1.6; }
.admin-main .notice { font-size: 17px; line-height: 1.75; }
.admin-main .button { font-size: 18px; }
.admin-main .button-small { font-size: 17px; }
.admin-main .badge { font-size: 17px; }
.admin-main .pill { font-size: 17px; }
.admin-main .admin-empty h3 { font-size: 23px; }
.admin-main .admin-empty p { font-size: 17.5px; }
.admin-main .stat-body strong { font-size: 33px; }
.admin-main .stat-body small { font-size: 17.5px; }
/* ลิงก์ชำระเงินยาว 69 ตัว ไม่มีทางแสดงครบในแถบขวาอยู่แล้ว และมีปุ่มคัดลอกอยู่
   ข้าง ๆ มันคือช่องที่กดคัดลอก ไม่ใช่ข้อความที่ต้องอ่านทีละตัว */
.admin-main .billing-link { font-size: 17px; }
.admin-main .mod-body { font-size: 17px; line-height: 1.7; }
/* ชื่อผู้เขียนเป็นหัวของแถว แต่เดิมเล็กกว่าเนื้อความที่อยู่ใต้มันเอง (16 ใต้ 17) */
.admin-main .mod-meta strong { font-size: 18.5px; }
.admin-main .mod-meta { font-size: 17px; }
.admin-main input:not([type="checkbox"]):not([type="radio"]),
.admin-main select,
.admin-main textarea { font-size: 19px; }
.admin-table { font-size: 18px; }
.admin-table thead th { font-size: 17.5px; }
.admin-table td small,
.row-district { font-size: 17px; }

/* ช่องเลือกไฟล์เคยเป็นปุ่มเปล่าของเบราว์เซอร์ ยืนอยู่ข้างช่องกรอกที่มีรูปทรง
   ของตัวเอง — อาการเดียวกับ input ที่ไม่มี type ซึ่งแก้ไปแล้วในหมวด 8 */
.admin-main input[type="file"] { padding: 9px 12px; line-height: 1.5; cursor: pointer; }
/* a percentage width makes the control compressible, so its 384px intrinsic
   width stops stretching .admin-column's auto track (document was 443px at 375) */
.admin-main input[type="file"] { width: 100%; min-width: 0; }
.admin-main input[type="file"]::file-selector-button {
  margin: 0 12px 0 0;
  padding: 8px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-field);
  background: var(--card);
  color: var(--brand-ink);
  font-family: var(--font-ui);
  font-size: 17px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease;
}
.admin-main input[type="file"]::file-selector-button:hover {
  background: var(--tint);
  border-color: var(--tint);
}

/* --- หน้าแก้ไขโปรไฟล์ ---------------------------------------------------- */

/* แถบขวาไม่เกาะจออีกแล้ว ตอนนี้มันเป็นสามกล่องซ้อนกันซึ่งสูงเกินหนึ่งจอ
   การเกาะจึงได้ผลตรงข้าม คือดันก้นแถบออกนอกจอถาวร ปุ่มบันทึกย้ายไปแถบลอย */
@media (min-width: 1100px) {
  .admin-rail { position: static; }
  .admin-layout { grid-template-columns: minmax(0, 1fr) 380px; }
}

.admin-save-bar {
  grid-column: 1 / -1;
  position: sticky;
  bottom: 12px;
  z-index: 4;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 18px;
  padding: 15px 18px;
  border-radius: var(--r-panel);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-lift);
}
.admin-save-bar .form-help { margin: 0; flex: 1 1 280px; }
.admin-save-bar .button { flex: none; }
@media (max-height: 620px) { .admin-save-bar { position: static; } }

/* ชิประยะเวลาและช่วงเวลามีอย่างละสี่ตัวพอดี ปล่อยให้ flex หักบรรทัดเองจะได้
   3+1 บนจอแคบ ซึ่งอ่านเหมือนสองกลุ่มที่ไม่เกี่ยวกัน  ตารางตายตัวทำให้เป็นชุด
   เดียวเสมอ เจาะจงเฉพาะ .pill-row ที่เป็นลูกของ .field เพื่อไม่ไปยืดชิปช่วงเวลา
   ของหน้าสถิติ ซึ่งมีสามตัวและอยู่บนหัวเรื่อง */
.admin-main .field > .pill-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.admin-main .field > .pill-row .pill { min-width: 0; justify-content: center; }
@media (min-width: 760px) {
  .admin-main .field > .pill-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* --- แถวปุ่มในแถบขวา ----------------------------------------------------- */

/* กล่องค่าบริการมีสองแถวที่เอาช่องกรอกกับปุ่มมาต่อกันในบรรทัดเดียว พอมาอยู่ใน
   แถบแคบ flex ก็บีบปุ่มจนป้ายแตกกลางคำ — "คัด/ลอก" "ออก/ลิงก์/ใหม่" "ให้/เลย"
   ป้ายปุ่มห้ามแตกกลางคำ: ช่องกรอกกินเต็มบรรทัดของตัวเอง แล้วปุ่มค่อยแบ่งบรรทัด
   ถัดไปกันเอง */
.admin-rail .admin-item-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.admin-rail .admin-item-actions > .billing-link,
.admin-rail .admin-item-actions > select { flex: 1 1 100%; min-width: 0; }
.admin-rail .admin-item-actions > input[type="number"] { flex: 0 0 96px; }
.admin-rail .admin-item-actions > .button { flex: 1 1 auto; white-space: nowrap; }

/* สถานะค่าบริการเป็นข้อความล้วน ไม่ใช่สวิตช์ จึงไม่ควรมีกรอบแบบ .rail-check
   ที่สื่อว่ากดได้ */
.admin-rail .rail-check:not(:has(input)) {
  grid-template-columns: minmax(0, 1fr);
  background: var(--tint);
  border-color: var(--tint);
  cursor: default;
}

/* ---------------------------------------------------------------------------
   49. สี่หมวดบริการ · ความปลอดภัย · รายงาน

   ไม่มีม่านสี และไม่แตะโทเค็นเดิมสักตัว — เจ้าของสั่งให้คืนสีทุกตัวเป็นของเดิม
   แล้ว สีที่ใช้มาจากโทเค็นที่มีอยู่ ยกเว้น --alert ในโหมดมืดค่าเดียว (ดูท้ายหมวด)
   ------------------------------------------------------------------------- */

/* fieldset ติด min-inline-size:min-content มาจากเบราว์เซอร์ ถ้าไม่ล้างจะดัน
   ทั้งหน้ากว้างเกินจอบนมือถือ — เจอมาแล้วที่ .district-chips */
.side-cats,
.apply-form fieldset.filter-block,
.report-reasons {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.side-cats > legend,
.apply-form fieldset.filter-block > legend,
.report-reasons > legend { padding: 0; margin-bottom: 10px; }

/* --- หน้าแรก: สี่ไทล์ ------------------------------------------------------ */
.cat-tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin: 18px 0 6px;
}
.cat-tile {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
  padding: 14px;
  border-radius: var(--r-card);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
  color: var(--ink);
  text-decoration: none;
  transition: transform .2s var(--ease-rise), box-shadow .2s ease, border-color .2s ease;
}
.cat-tile:hover { transform: translateY(-2px); box-shadow: var(--sh-lift); border-color: rgba(193, 53, 132, .32); }
.cat-tile:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
.cat-tile.is-on { border-color: rgba(193, 53, 132, .45); }
.cat-tile-mark {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 14px;
  background: var(--grad-mark);
  color: #FFFFFF;
}
.cat-tile-mark .icon { width: 23px; height: 23px; }
.cat-tile-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cat-tile-text strong { font-size: 19.5px; font-weight: 700; line-height: 1.3; }
.cat-tile-text small { font-size: 17px; line-height: 1.55; color: var(--ink-soft); }
.cat-tile-count { margin-top: auto; font-size: 17px; font-weight: 600; color: var(--brand-ink); }
@media (min-width: 900px) {
  .cat-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-top: 22px; }
  .cat-tile {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "mark text" "mark count";
    column-gap: 14px;
    row-gap: 4px;
    align-items: center;
    padding: 16px 18px;
  }
  .cat-tile-mark { grid-area: mark; width: 50px; height: 50px; border-radius: 15px; }
  .cat-tile-mark .icon { width: 26px; height: 26px; }
  .cat-tile-text { grid-area: text; }
  .cat-tile-count { grid-area: count; margin-top: 0; }
}
@media (prefers-reduced-motion: reduce) { .cat-tile, .cat-tile:hover { transition: none; transform: none; } }

/* --- ตัวเลือกหมวด: แถบค้นหาและฟอร์มสมัคร ---------------------------------- */
.cat-pick { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.side-cats .cat-opt:first-child { grid-column: 1 / -1; }
.cat-opt {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-height: 44px;
  padding: 8px 12px;
  border-radius: var(--r-field);
  border: 1px solid var(--border);
  font-size: 16.5px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink-soft);
  cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}
.cat-opt input { position: absolute; opacity: 0; width: 0; height: 0; }
.cat-opt .icon { width: 18px; height: 18px; flex: none; color: var(--brand-ink); }
.cat-opt:has(input:checked),
.cat-opt.on {
  border-color: rgba(193, 53, 132, .42);
  background: rgba(193, 53, 132, .1);
  font-weight: 600;
  color: var(--brand-ink);
}
.cat-opt:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }

.cat-pick-apply { grid-template-columns: minmax(0, 1fr); gap: 10px; }
@media (min-width: 640px) { .cat-pick-apply { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.cat-pick-apply .cat-opt { align-items: flex-start; padding: 13px 15px; }
.cat-pick-apply .cat-opt .icon { width: 22px; height: 22px; margin-top: 2px; }
.cat-pick-apply .cat-opt > span { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.cat-pick-apply .cat-opt strong { font-size: 18.5px; font-weight: 700; color: var(--ink); }
.cat-pick-apply .cat-opt small { font-size: 15.5px; font-weight: 400; line-height: 1.55; color: var(--ink-soft); }

/* --- ป้ายหมวดบนการ์ดและหน้าโปรไฟล์ --------------------------------------- */
.card-cat {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  align-self: flex-start;
  max-width: 100%;
  font-size: 17px;
  font-weight: 600;
  line-height: 1.4;
  color: var(--brand-ink);
}
.card-cat .icon { width: 16px; height: 16px; flex: none; }
.profile-id .card-cat { font-size: 18px; margin-bottom: 2px; }
.profile-id .card-cat .icon { width: 18px; height: 18px; }

/* --- กล่อง "ก่อนนัดพบ" บนโปรไฟล์ ------------------------------------------ */
.safety-box {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 6px;
  padding: 16px 18px;
  border-radius: var(--r-card);
  background: var(--tint);
  border: 1px solid var(--border);
}
.safety-box .profile-h { display: flex; align-items: center; gap: 8px; padding-top: 0; font-size: 22px; }
.safety-box .profile-h .icon { width: 22px; height: 22px; color: var(--brand-ink); }
.safety-box ul,
.safety-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
  padding-left: 1.2em;
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink-soft);
}
.safety-box li::marker,
.safety-list li::marker { color: var(--brand-ink); }
.safety-links { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; margin: 0; font-size: 18px; }
/* both links keep a 44px target on a phone — .text-link's own floor is 40 */
.safety-links .text-link { min-height: 44px; }
.report-link { display: inline-flex; align-items: center; gap: 5px; }
.report-link .icon { width: 18px; height: 18px; }
/* The four points are what the box is for, not help text. The rule above is
   shared with .safety-list on guide.php and rules.php, which stays at 17. */
.safety-box ul { font-size: 18px; line-height: 1.6; }

/* --- วิธีใช้ และ กติกา ------------------------------------------------------ */
.guide-section h2 { display: flex; align-items: center; gap: 9px; }
.guide-section h2 .icon { width: 22px; height: 22px; color: var(--brand-ink); }
.safety-section {
  padding: 20px 22px;
  border-radius: var(--r-card);
  background: var(--tint);
  border: 1px solid var(--border);
}
.guide-help {
  margin: 12px 0 0;
  font-size: 17px;
  line-height: 1.7;
  color: var(--ink-soft);
}
.guide-help strong { color: var(--ink); font-variant-numeric: tabular-nums; }

.rules-list { display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; list-style: none; }
.rules-list li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px;
  align-items: start;
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink-soft);
}
.rules-list li strong { color: var(--ink); }
.rules-list .icon { width: 20px; height: 20px; margin-top: 4px; }
.rules-yes .icon { color: var(--brand-ink); }

/* --- ฟอร์มสมัครและฟอร์มแจ้ง ------------------------------------------------ */
.consent-card { align-items: flex-start; line-height: 1.6; }
.consent-card .check-box { margin-top: 3px; flex: none; }
.report-reasons { display: flex; flex-direction: column; gap: 8px; }

/* --- หลังบ้าน: รายงาน และสถานะที่ต้องรีบดู --------------------------------

   --danger มีค่าเดียวทั้งสองโหมด (#C42A55) ซึ่งบนการ์ดโหมดมืดได้แค่ราว 2.6:1
   อ่านไม่ออก --alert จึงยกเป็นชมพูอ่อนในโหมดมืด ที่เหลือเป็นค่าเดิม */
:root { --alert: var(--danger); --alert-bg: rgba(196, 42, 85, .12); }
:root[data-theme="dark"] { --alert: #F58A9C; --alert-bg: rgba(245, 138, 156, .16); }
.rules-no .icon { color: var(--alert); }
.badge-danger { background: var(--alert-bg); color: var(--alert); }
.stat-danger .stat-mark { background: var(--alert-bg); color: var(--alert); }
.mod-row.is-urgent { border-color: var(--alert); }
.text-danger { color: var(--alert); font-weight: 700; }
.button-danger {
  background: transparent;
  border: 1px solid var(--alert);
  color: var(--alert);
}
.button-danger:hover { background: var(--alert-bg); }

/* ไทล์หมวดอยู่ในคอลัมน์เดียวกับแถบผลลัพธ์และกริดการ์ด — ฟอร์มหน้าแรกกว้างเต็มจอ
   ลูกทุกตัวของมันต้องกำหนดความกว้างเอง ไม่งั้นแถวไทล์วิ่งชนขอบจอ (วัดได้ x=0 กว้าง 1425) */
@media (min-width: 1024px) {
  .main-container .cat-tiles { width: min(1300px, 100% - 96px); margin-inline: auto; }
}

/* ---------------------------------------------------------------------------
   50. รูปตรงตัวจริง — the badge, the provider's check page, the owner's review

   The check page (/verify.php) is phone-first and self-contained: it loads no
   Google Fonts (its CSP does not allow them) and none of the site chrome, so it
   sets its own ground. The camera screen is dark on purpose — a lit face reads
   best against it, and the provider's eye goes to the instruction, not the room.
   Colours are the existing tokens; the one literal green is the camera's own
   "pose reached" signal, which only ever sits on the dark camera ground.
   ------------------------------------------------------------------------- */

/* --- the badge ----------------------------------------------------------- */
.card-name .face-pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-left: 6px;
  padding: 1px 8px 1px 6px;
  border-radius: 999px;
  background: var(--tint);
  color: var(--brand-ink);
  font-family: var(--font-ui);
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1.55;
  vertical-align: 2px;
  white-space: nowrap;
}
.card-name .face-pill .icon { width: 12px; height: 12px; }
.face-note {
  margin: 0;
  font-size: 16px;
  line-height: 1.7;
  color: var(--ink-soft);
}
.face-note b { color: var(--ink); }

/* --- the provider's page ------------------------------------------------- */
body.verify-page {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
}
.verify-app {
  width: 100%;
  max-width: 480px;
  margin-inline: auto;
  padding: max(16px, env(safe-area-inset-top)) 16px max(20px, env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.v-card {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 22px 20px;
  border-radius: var(--r-card);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--sh-card);
  font-size: 17.5px;
  line-height: 1.7;
}
.v-card[hidden], .v-cam[hidden] { display: none; }
.v-card h1 { margin: 0; font-size: 26px; line-height: 1.35; font-family: var(--font-display); text-wrap: balance; }
.v-card h1:focus { outline: none; }
.v-card p { margin: 0; }
.v-kicker { font-size: 17px; font-weight: 700; color: var(--brand-ink); letter-spacing: normal; }
.v-small { font-size: 17px; line-height: 1.6; color: var(--ink-soft); }
.v-small a, .v-foot a { color: var(--brand-ink); }
.v-warn {
  padding: 10px 12px;
  border-radius: 12px;
  background: var(--tint);
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink-soft);
}
.v-profile { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 14px; background: var(--tint); }
.v-profile img { width: 56px; height: 56px; border-radius: 14px; object-fit: cover; flex: none; }
.v-profile b { display: block; font-size: 18px; }
.v-profile small { font-size: 17px; color: var(--ink-soft); }
.v-steps { margin: 0; padding-left: 1.3em; display: grid; gap: 4px; font-weight: 600; }
.v-rows { margin: 0; display: grid; gap: 0; font-size: 17px; line-height: 1.65; }
/* 96px held "ภาพที่เก็บ" at 15.5 — at 17 the same label needs 106 or it wraps */
.v-rows div { display: grid; grid-template-columns: 106px minmax(0, 1fr); gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--border); }
.v-rows dt { color: var(--muted); font-weight: 600; }
.v-rows dd { margin: 0; }
.v-check { font-size: 17px; line-height: 1.6; align-items: flex-start; }
.v-check .check-box { margin-top: 3px; flex: none; }
.v-wide { width: 100%; min-height: 50px; justify-content: center; font-size: 18px; }
.v-textbtn {
  align-self: center;
  min-height: 44px;
  padding: 8px 10px;
  border: 0;
  background: none;
  color: var(--brand-ink);
  font: 600 17px/1.5 var(--font-ui);
  text-align: center;
  cursor: pointer;
}
.v-textbtn[hidden] { display: none; }
.v-quote { padding: 12px 14px; border-radius: 12px; background: var(--tint); font-weight: 600; }
.v-bigcheck {
  width: 64px; height: 64px; border-radius: 50%; display: grid; place-items: center; align-self: center;
  background: rgba(52, 199, 123, .15); color: #1B7A4B;
}
.v-bigcheck .icon { width: 32px; height: 32px; stroke-width: 2.4; }
.v-row { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.v-confirm { display: grid; gap: 10px; }
/* 14.5 was the smallest type anywhere on the site */
.v-foot {
  margin: 6px 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0 6px;
  font-size: 17px;
  color: var(--muted);
}

/* the camera screen */
.v-cam {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-inline: -16px;
  padding: 12px 16px 16px;
  min-height: calc(100vh - 32px);
  background: #150B19;
  color: #FFFFFF;
}
.v-cam-top { display: grid; gap: 4px; }
.v-prog { display: flex; gap: 5px; }
.v-prog b { flex: 1; height: 5px; border-radius: 3px; background: rgba(255, 255, 255, .22); }
.v-prog b.on { background: #F58A9C; }
.v-stepno { margin: 0; font-size: 14.5px; color: rgba(255, 255, 255, .78); }
.v-say { margin: 0; font-family: var(--font-display); font-size: 25px; font-weight: 700; line-height: 1.35; min-height: 2.7em; }
.v-view {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 4;
  max-height: 62vh;
  border-radius: 22px;
  overflow: hidden;
  background: #000;
}
.v-view video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scaleX(-1);          /* a mirror, which is what people expect of a front camera */
}
.v-view video[hidden] { display: none; }
.v-oval {
  position: absolute;
  left: 50%;
  top: 46%;
  width: 62%;
  aspect-ratio: 3 / 4;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  border: 3px dashed rgba(255, 255, 255, .75);
  box-shadow: 0 0 0 999px rgba(0, 0, 0, .28);
  pointer-events: none;
}
.v-oval[hidden] { display: none; }
.v-oval.ok { border-style: solid; border-color: #6FE0A5; }
.v-arrow {
  position: absolute;
  top: 46%;
  width: 34px;
  height: 34px;
  border-left: 7px solid #F58A9C;
  border-bottom: 7px solid #F58A9C;
  border-radius: 3px;
  pointer-events: none;
}
.v-arrow[hidden] { display: none; }
/* On the mirrored preview the provider's own left is the screen's left. */
.v-arrow-L { left: 7%; transform: translateY(-50%) rotate(45deg); }
.v-arrow-R { right: 7%; transform: translateY(-50%) rotate(-135deg); }
.v-arrow-U { left: 50%; top: 9%; transform: translateX(-50%) rotate(135deg); }
.v-arrow-D { left: 50%; top: auto; bottom: 16%; transform: translateX(-50%) rotate(-45deg); }
.v-count {
  position: absolute;
  right: 14px;
  top: 14px;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(0, 0, 0, .5);
  font: 700 34px/1 var(--font-ui);
}
.v-count[hidden] { display: none; }
.v-hint {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 12px;
  margin: 0;
  padding: 8px 12px;
  border-radius: 12px;
  background: rgba(0, 0, 0, .55);
  font-size: 17px;
  line-height: 1.5;
  text-align: center;
}
.v-hint:empty { display: none; }
.v-cam-actions { display: grid; gap: 8px; }
.v-cam .v-textbtn { color: #F9D2E3; }
.v-file { position: relative; overflow: hidden; }
.v-file input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.v-file[hidden], #v-shoot[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .v-oval, .v-arrow { transition: none; } }

/* --- the owner's side ---------------------------------------------------- */
.face-panel details.face-call { border-top: 1px solid var(--border); padding-top: 12px; display: grid; gap: 10px; }
.face-panel details.face-call > summary { cursor: pointer; font-weight: 700; font-size: 20px; min-height: 36px; display: flex; align-items: center; }
.face-panel details.face-call[open] > summary { margin-bottom: 4px; }
.face-quote { margin: 0; padding: 10px 12px; border-radius: 12px; background: var(--tint); font-size: 17px; line-height: 1.65; }
.face-script { margin: 0; padding-left: 1.3em; display: grid; gap: 4px; font-size: 17px; font-weight: 600; }
/* ไม่ตั้ง font-size เอง ปล่อยให้ .admin-main textarea (19px) ทำงาน ช่องนี้เคย
   เล็กกว่าช่องกรอกอื่นทุกช่องในหลังบ้านอยู่ 4px */
.face-once textarea { width: 100%; min-height: 150px; line-height: 1.6; }
.face-review { display: grid; gap: 12px; padding: 16px 0; border-top: 1px solid var(--border); }
.face-review:first-of-type { border-top: 0; padding-top: 4px; }
.face-review-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 14px; }
.face-review-head h3 { margin: 0; font-size: 20px; }
.face-review-head small { color: var(--muted); font-size: 17px; }
.face-review .flags { display: flex; flex-wrap: wrap; gap: 6px; }
.face-compare { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 5fr); gap: 16px; }
.face-lab { margin: 0 0 8px; font-size: 18px; font-weight: 700; color: var(--muted); }
.face-tiles { display: grid; gap: 8px; }
.face-photos { grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); }
.face-frames { grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); }
.face-tiles img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; border-radius: 12px; background: var(--tint); display: block; }
.face-tiles figure { margin: 0; display: grid; gap: 4px; }
.face-tiles figcaption { font-size: 17px; line-height: 1.45; color: var(--ink-soft); }
.face-tiles figcaption b { display: block; color: var(--ink); font-size: 17.5px; }
.face-checklist { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 6px 16px; font-size: 17px; }
.face-checklist label { display: flex; gap: 8px; align-items: flex-start; }
.face-checklist input { margin-top: 6px; accent-color: #C13584; }
.face-decide { display: grid; gap: 10px; }
.face-decide .decide-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.face-decide .decide-row .field { margin: 0; }
.face-stepup { display: grid; gap: 12px; max-width: 460px; }
.face-exp { margin: 0; padding-left: 1.2em; display: grid; gap: 6px; }
@media (max-width: 640px) { .face-compare { grid-template-columns: minmax(0, 1fr); } }

/* --- the provider's standing page and the notice ------------------------- */
.billing-face { display: grid; gap: 10px; padding: 18px 20px; border-radius: var(--r-card); background: var(--card); border: 1px solid var(--border); }
.billing-face h2 { margin: 0; font-size: 21px; }
.billing-face p { margin: 0; color: var(--ink-soft); }
.billing-face-form { display: grid; gap: 10px; }
.billing-face-form .check-card { align-items: flex-start; line-height: 1.6; }
.privacy-rows { margin: 0; display: grid; font-size: 17px; line-height: 1.7; }
.privacy-rows div { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
.privacy-rows dt { color: var(--muted); font-weight: 600; }
.privacy-rows dd { margin: 0; color: var(--ink-soft); }
@media (max-width: 480px) { .privacy-rows div { grid-template-columns: minmax(0, 1fr); gap: 2px; } }

/* --- สแกนหน้าตอนสมัคร ------------------------------------------------------
   Added with the application-time route: the applicant uploads photographs and
   scans in one sitting, and the owner sees both side by side in the queue. */
.apply-done { gap: 16px; }
.apply-face {
  display: grid;
  gap: 10px;
  width: 100%;
  max-width: 520px;
  margin-inline: auto;
  padding: 20px 22px;
  border-radius: var(--r-card);
  background: var(--tint);
  border: 1px solid var(--border);
  text-align: left;
}
.apply-face h2 {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 21px;
  line-height: 1.35;
}
.apply-face h2 .icon { width: 21px; height: 21px; color: var(--ok); }
.apply-face p { margin: 0; font-size: 17px; line-height: 1.7; color: var(--ink-soft); }
.apply-face .button { justify-content: center; min-height: 50px; font-size: 18px; }
.apply-face .small-note { font-size: 15.5px; }
.apply-face-done, .apply-face-off { background: var(--card); }

/* the owner's queue */
.apply-compare { margin-top: 12px; }
.apply-compare .face-tiles img { aspect-ratio: 3 / 4; }
.apply-face-check {
  display: grid;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--r-card);
  background: var(--tint);
  border: 1px solid var(--border);
}
.apply-face-check .face-lab { margin: 0; }

/* the four steps at the top of the application form */
.apply-steps {
  margin: 4px 0 0;
  padding-left: 1.4em;
  display: grid;
  gap: 6px;
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink-soft);
  text-align: left;
}
.apply-steps b { color: var(--ink); }
.apply-steps::marker { color: var(--brand-ink); }

/* ---------------------------------------------------------------------------
   54. ใบสมัคร — ตัวหนังสืออ่านออก และการ์ดที่ข้อความไม่ล้น (24 ก.ย. 2569)

   เจ้าของวงกรอบแดงรอบบรรทัด "เลือกได้หนึ่งประเภทต่อหนึ่งโปรไฟล์…" แล้วบอกว่า
   มันล้น และบอกว่าทั้งหน้าตัวหนังสือเล็กมากจนมองไม่เห็น ทั้งสองข้อถูก

   ที่มาของการล้น: หมวด 49 ล้าง padding ของ fieldset.filter-block เพื่อแก้
   min-inline-size ของ fieldset ที่ดันหน้าเกินจอ แต่มันล้าง padding ของ "การ์ด"
   ไปด้วย ทั้งที่ตัวที่แก้บั๊กจริงคือ min-width:0 อย่างเดียว การ์ดจึงไม่มีขอบใน
   เหลือเลย ป้ายหัวข้อกับบรรทัดช่วยเหลือจึงแนบขอบการ์ด และบรรทัดล่างสุดไป
   ชนขอบโค้ง 24px พอดี (วัดได้: ขอบล่างของข้อความ = ขอบล่างการ์ด = 508px)

   ทุกกฎในหมวดนี้ขึ้นต้นด้วย .apply-page:not(.report-page) ไม่ใช่แค่ .apply-form
   เพราะ report.php ใช้ markup ชุดเดียวกันทั้งชุด — body class "apply-page
   report-page", main.apply-screen, form.apply-form.filter-card,
   fieldset.filter-block > legend.filter-title, .location-head, .form-controls
   รอบแรกผมสโคปแค่ .apply-form/.apply-screen แล้วคิดว่าปลอดภัย ซึ่งผิด:
   หน้าแจ้งรายงานโดนทั้งความกว้าง 980 ทั้ง ramp และ fieldset ก็ได้การ์ดซ้อน
   การ์ดมาโดยไม่ได้ตั้งใจ (วัดได้ main = 980 ทั้งที่ควรเป็น 1300)
   .apply-steps กับ .cat-pick-apply เป็นของ apply.php อยู่แล้ว แต่ใส่คำนำหน้า
   ให้เหมือนกันทั้งหมดเพื่อให้อ่านแล้วรู้ทันทีว่าหมวดนี้ไม่แตะ report.php
   ------------------------------------------------------------------------- */

/* คืน padding และเส้นขอบให้การ์ด โดยไม่แตะ min-width:0 ที่เป็นตัวแก้บั๊กจริง */
.apply-page:not(.report-page) .apply-form fieldset.filter-block {
  padding: 20px;
  border: 1px solid var(--border);
}
/* พอการ์ดมีเส้นขอบกลับมา <legend> ก็ทำตัวตามปกติของมัน คือไปนั่งคร่อมเส้นขอบบน
   (วัดได้: ขอบบนการ์ดกับขอบบนป้ายอยู่ที่ 293px เท่ากัน) เจ้าของเห็นเป็น
   "ตัวหนังสือทับเส้น" — ถูกแล้ว เป็นผลจากการคืนเส้นขอบของผมเอง
   float ทำให้ legend หลุดจากช่องพิเศษของมัน กลายเป็นกล่องธรรมดาที่อยู่ในขอบใน
   20px ตามปกติ และเส้นขอบบนกลับมาต่อเนื่องไม่ขาด (ทางแก้มาตรฐาน ไม่ใช่ hack) */
.apply-page:not(.report-page) .apply-form fieldset.filter-block > legend {
  float: left;
  width: 100%;
  padding: 0;
  margin: 0 0 12px;
}
.apply-page:not(.report-page) .apply-form .filter-block { padding: 20px; }

/* ฟอร์มกว้าง 1,174px ต่อหนึ่งช่องอ่านยากและดูไม่ได้ตั้งใจออกแบบ
   980 ทำให้ช่องเดี่ยวราว 900 และช่องคู่ราว 440 */
.apply-page:not(.report-page) main.apply-screen { max-width: 980px; }

/* --- หัวเรื่อง --- */
.apply-page:not(.report-page) .location-head h1 { font-size: 33px; line-height: 1.3; }
.apply-page:not(.report-page) .location-head p { font-size: 19.5px; line-height: 1.7; }
/* ต้องมีคำนำหน้าเหมือนกฎอื่นในหมวดนี้ ไม่ใช่เพราะวันนี้รั่ว (report.php ไม่มี
   <ol class="apply-steps">) แต่เพราะหัวหมวดนี้เขียนไว้ว่า "ทุกกฎในหมวดนี้ขึ้นต้น
   ด้วย .apply-page:not(.report-page)" ซึ่งเมื่อก่อนไม่จริงสำหรับบรรทัดนี้บรรทัดเดียว
   คำอธิบายที่ไม่ตรงกับโค้ดคือกับดัก เพราะคนอ่านจะเชื่อแล้วไม่ตรวจซ้ำ */
.apply-page:not(.report-page) .apply-steps { font-size: 19px; gap: 10px; line-height: 1.7; }

/* --- หัวข้อในการ์ด --- */
.apply-page:not(.report-page) .apply-form .filter-title {
  display: block;
  margin-bottom: 4px;
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--ink);
}

/* --- ป้ายช่อง คำอธิบาย และตัวควบคุม --- */
.apply-page:not(.report-page) .apply-form .field { font-size: 18.5px; gap: 8px; }
.apply-page:not(.report-page) .apply-form .field small,
.apply-page:not(.report-page) .apply-form .form-help { font-size: 17.5px; line-height: 1.6; }
.apply-page:not(.report-page) .apply-form .filter-note { font-size: 17.5px; line-height: 1.6; }
/* :not(radio/checkbox/hidden) — the bare `input` also matched the radios and
   checkboxes hidden inside .cat-opt, .check-card and .pill, which set
   height: 0. min-height beats height, so they were computing to 54px tall.
   Nothing broke only because all three are position:absolute and width:0; it
   was one `position: static` away from a 54px hole in a chip row. Measured
   before: .cat-opt input / .check-card input / .pill input all 54px.
   The no-type-attribute case still has to be caught — the LINE contact field
   has no `type` at all — so this excludes by type rather than selecting by it. */
.apply-page:not(.report-page) .apply-form input:not([type="radio"]):not([type="checkbox"]):not([type="hidden"]),
.apply-page:not(.report-page) .apply-form select,
.apply-page:not(.report-page) .apply-form textarea { font-size: 19.5px; min-height: 54px; }
.apply-page:not(.report-page) .apply-form textarea { min-height: 96px; line-height: 1.7; }
/* คาเร็ตของช่องเลือกนับจากขอบบนของ .field ตามสูตรหมวด 8 — ป้ายโตขึ้นเป็น
   18.5 และช่องสูงขึ้นเป็น 54 จึงต้องขยับตาม ไม่งั้นไปเกาะขอบล่างเหมือนที่เคย
   เกิดกับแถบค้นหา */
.apply-page:not(.report-page) .apply-form .field:has(select)::after { top: calc(1.6em + 8px + 23px); }

/* --- ตัวเลือก --- */
.apply-page:not(.report-page) .apply-form .cat-opt { font-size: 18px; }
.apply-page:not(.report-page) .apply-form .cat-pick-apply .cat-opt strong { font-size: 20px; }
.apply-page:not(.report-page) .apply-form .cat-pick-apply .cat-opt small { font-size: 17px; line-height: 1.6; }
.apply-page:not(.report-page) .apply-form .pill { min-height: 42px; padding: 0 15px; border-radius: 21px; font-size: 18px; }
.apply-page:not(.report-page) .apply-form .check-card { font-size: 18.5px; padding: 13px 14px; }

/* --- ปุ่มส่ง --- */
.apply-page:not(.report-page) .apply-form .form-controls .button { min-height: 56px; padding-inline: 30px; font-size: 20px; }
.apply-page:not(.report-page) .apply-form .form-controls .text-link { font-size: 18.5px; min-height: 44px; }


/* ---------------------------------------------------------------------------
   55. หน้าแจ้งรายงาน — ตัวหนังสือขนาดเดียวกับใบสมัคร (24 ก.ย. 2569)

   เจ้าของสั่ง "ขยายให้เท่ากัน" หลังหมวด 54 ขยายเฉพาะใบสมัคร report.php ใช้
   markup ชุดเดียวกับ apply.php แต่หมวด 54 กัน report ออกไว้ทุกกฎโดยตั้งใจ
   หน้านี้จึงเหลือเป็นหน้าเดียวในเว็บที่ยังเป็นตัวหนังสือชุดเก่า

   วัดของจริงก่อนแก้ (เรนเดอร์ฟอร์มจริงที่ 1440px):
     ป้ายช่องกรอก 16 · คำอธิบายใต้ช่อง 15.5 · บรรทัดเบอร์ฉุกเฉิน 15.5
     หัวเรื่อง 28.5 · ป้ายหัวข้อ 18 · ช่องพิมพ์ 18 · ปุ่มส่ง 18
   สองค่าที่เป็น 15.5 ต่ำกว่าพื้น 16 ที่เจ้าของตั้งไว้เอง และหนึ่งในสองนั้นคือ
   บรรทัด "โทร 191 · โทร 1300" ซึ่งเป็นบรรทัดสำคัญที่สุดในหน้า

   ทำไมลดความกว้างเหลือ 980 ด้วย: วัดได้ว่าช่องเล่ารายละเอียดกว้าง 1,175px
   เป็นตัวเลขเดียวกับที่ทำให้ใบสมัครดูไม่เสร็จ ฟอร์มคอลัมน์เดียวที่กว้างขนาดนั้น
   อ่านยากด้วยเหตุผลเดียวกัน ไม่ใช่การกลับคำแก้ 1300 ของหมวด 54 — อันนั้นแก้
   ผลข้างเคียงที่ไม่ได้ตั้งใจ ส่วนอันนี้เป็นการตัดสินใจที่ตั้งใจและวัดมาแล้ว

   *** การ์ดของหน้านี้ก็ไม่มีขอบใน เหมือนที่ใบสมัครเคยเป็น ***
   ฉบับแรกของหมวดนี้เขียนว่า "หมวด 49 ล้าง padding/border ของ `.report-reasons`
   ไว้โดยตั้งใจ เพราะมันคือรายการตัวเลือก ไม่ใช่การ์ด" — ผิดทั้งสองท่อน
   หมวด 49 (8119-8126) ล้าง padding/border เป็นการรีเซ็ตรวมของ fieldset ทุกตัว
   เพื่อแก้ min-inline-size ของเบราว์เซอร์ ไม่ใช่การตัดสินใจเรื่องหน้าตา และ
   หมวด 54 (8656-8660) ก็ระบุไว้เองว่าการล้าง padding/border เป็นผลข้างเคียง
   ที่ไม่ได้ตั้งใจ ตัวที่แก้บั๊กจริงคือ min-width:0 อย่างเดียว

   และมันไม่ได้เป็น "รายการตัวเลือก" ด้วย เพราะหมวด 49 ล้างแค่ padding กับ
   border ไม่ได้ล้าง background / box-shadow / border-radius ที่ `.filter-block`
   (3671-3677) ใส่ไว้ วัดของจริงบน report.php ได้ว่ากล่องนี้ยังเป็นการ์ดขาว
   เงาใต้กล่อง มุมโค้ง 24px แต่ขอบในเป็น 0 ทั้งสี่ด้าน — ป้ายหัวข้อชิดขอบบน
   ตัวเลือกอันล่างสุดชิดขอบล่าง และมุมโค้ง 24px ตัดผ่านพอดี
   คือบั๊คตัวเดียวกับที่เจ้าของวงกรอบแดงในใบสมัคร แค่ยังไม่มีใครเห็นเพราะ
   หน้านี้เปิดดูไม่ได้จนกว่าจะมีผู้ให้บริการจริง
   ในฟอร์มเดียวกัน กล่องถัดไป (`div.filter-block`) มีขอบใน 22px อยู่แล้ว
   กล่องแรกจึงเป็นกล่องเดียวในทั้งสองหน้าที่ไม่มีขอบใน

   จึงคืน padding + เส้นขอบให้เหมือนหมวด 54 และต้อง float legend ตามไปด้วย
   เพราะพอมีเส้นขอบ <legend> จะไปนั่งคร่อมเส้นตามพฤติกรรมปกติของมัน

   หน้านี้ไม่มี <select> จึงไม่ต้องมีกฎขยับคาเร็ต และไม่มี .pill / .cat-opt /
   .filter-note / .text-link ในแถบปุ่ม จึงไม่ได้ลอกกฎพวกนั้นมา
   ส่วน .empty-state (จอ "ได้รับรายงานแล้ว" กับ "ไม่พบโปรไฟล์") ใช้ร่วมกับอีก
   หลายหน้าทั้งเว็บ จึงไม่แตะ ไม่งั้นจะรั่วออกไปนอกหน้านี้แบบเดียวกับที่หมวด 54
   เคยรั่วมาโดนหน้านี้

   หมายเหตุเลขหมวด: ไฟล์นี้มีเลข 50 และ 54 ซ้ำกันอย่างละสองที่อยู่ก่อนแล้ว
   อย่าใช้เลขหมวดเป็นเครื่องบอกลำดับความสำคัญ ให้ค้นด้วยชื่อคลาสแทน
   ------------------------------------------------------------------------- */

.report-page main.apply-screen { max-width: 980px; }

/* --- คืนขอบในให้การ์ด (เหมือนหมวด 54 ทำกับใบสมัคร) --- */
.report-page .apply-form fieldset.filter-block {
  padding: 20px;
  border: 1px solid var(--border);
}
/* พอเส้นขอบกลับมา <legend> จะไปคร่อมเส้นบนตามปกติของมัน float ดึงมันออกจาก
   ช่องพิเศษนั้น กลายเป็นกล่องธรรมดาในขอบใน 20px และเส้นขอบต่อเนื่องไม่ขาด
   กฎนี้แทนที่ margin-bottom 10px ของหมวด 49 สำหรับ legend ตัวนี้โดยตั้งใจ */
.report-page .apply-form fieldset.filter-block > legend {
  float: left;
  width: 100%;
  padding: 0;
  margin: 0 0 12px;
}
/* กล่องที่สองรับ 22px มาจาก 3744 ส่วนใบสมัครใช้ 20px — ปรับให้ตรงกันตามที่
   เจ้าของสั่งว่า "ขยายให้เท่ากัน" */
.report-page .apply-form .filter-block { padding: 20px; }

/* --- หัวเรื่อง --- */
.report-page .location-head h1 { font-size: 33px; line-height: 1.3; }
.report-page .location-head p { font-size: 19.5px; line-height: 1.7; }

/* --- ป้ายหัวข้อในฟอร์ม --- */
.report-page .apply-form .filter-title {
  display: block;
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--ink);
}

/* --- ป้ายช่อง คำอธิบาย และตัวควบคุม --- */
.report-page .apply-form .field { font-size: 18.5px; gap: 8px; }
.report-page .apply-form .field small,
.report-page .apply-form .form-help { font-size: 17.5px; line-height: 1.6; }

/* เจาะจงเฉพาะช่องกรอกจริง — radio ที่ซ่อนใน .check-card ถูกตั้ง height:0 ไว้
   ถ้าเหมารวมด้วย min-height จะไปทับค่านั้น (วันนี้ไม่พังเพราะมัน absolute อยู่
   นอกโฟลว์ แต่ไม่ควรเขียนโค้ดให้ต้องพึ่งเรื่องนั้น) */
.report-page .apply-form input:not([type="radio"]):not([type="checkbox"]):not([type="hidden"]),
.report-page .apply-form textarea { font-size: 19.5px; min-height: 54px; }
.report-page .apply-form textarea { min-height: 96px; line-height: 1.7; }

/* --- ตัวเลือกเหตุผล --- */
.report-page .apply-form .check-card { font-size: 18.5px; padding: 13px 14px; }

/* --- ปุ่มส่ง --- */
.report-page .apply-form .form-controls .button { min-height: 56px; padding-inline: 30px; font-size: 20px; }


/* ---------------------------------------------------------------------------
   56. หกหน้าที่ยังไม่เคยไล่ดู — ขนาดที่กดได้บนมือถือ (24 ก.ย. 2569)

   วัดที่ 375px: ลิงก์ท้ายเว็บสูง 28 · ป้ายเขต/อำเภอสูง 34 · ชื่อจังหวัดที่เป็น
   ลิงก์สูง 25 · ลิงก์ท้ายหน้าตรวจใบหน้าสูง 17 ทั้งหมดต่ำกว่า 44

   เฉพาะจอมือถือ เพราะบนเดสก์ท็อปมีเมาส์ชี้ได้แม่น และการยืดทุกอย่างเป็น 44
   จะดันท้ายเว็บสูงขึ้นโดยไม่ได้อะไรกลับมา
   ------------------------------------------------------------------------- */

/* ชื่อเขตต้องใหญ่กว่าจำนวนคนที่ต่อท้าย พอ .pill-count ขึ้นเป็น 17 ลำดับก็กลับหัว
   ป้ายจึงต้องขึ้นตาม — .search-side .pill ใช้ 18/38 มาก่อนแล้ว ใช้สเกลเดียวกัน
   height: auto กันตัวอักษรที่โตขึ้นไม่ให้ชนกรอบ 34px ตายตัวของ .pill
   ต้องอยู่เหนือ @media ข้างล่าง เพราะความเฉพาะเจาะจงเท่ากัน ตัวที่อยู่ทีหลังชนะ */
.location-page .pill-row a.pill {
  height: auto;
  min-height: 38px;
  font-size: 18px;
}

@media (max-width: 759.98px) {
  /* ท้ายเว็บ: หกลิงก์เรียงพับบรรทัด ระยะห่าง 14 ยังอยู่ แค่แต่ละอันสูงขึ้น */
  .site-footer .footer-links a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* ป้ายเขตและอำเภอ — .pill ใช้ร่วมกับแถบค้นหาและใบสมัครซึ่งตั้งความสูงของ
     ตัวเองไว้แล้ว ตัวที่เป็น <a> มีอยู่หน้าเดียวคือหน้าพื้นที่ให้บริการ */
  .location-page .pill-row a.pill {
    height: auto;
    min-height: 44px;
    padding: 0 16px;
    border-radius: 22px;
  }
}

/* ชื่อจังหวัดในหัวข้อเป็นลิงก์ กล่องข้อความสูง 25 padding 10 บนล่างทำให้เป็น 45
   แล้ว margin ติดลบเท่ากันหักความสูงที่เพิ่มออกจากกล่องบรรทัด หัวข้อจึงอยู่ที่
   เดิมเป๊ะ ได้พื้นที่กดเพิ่มโดยไม่ขยับอะไรเลย */
.location-block-head h2 a {
  display: inline-block;
  padding-block: 10px;
  margin-block: -10px;
}

/* ในจอ "ก่อนเปิดกล้อง" ลิงก์อ่านประกาศฉบับเต็มเป็นเนื้อหาทั้งย่อหน้า ไม่ใช่
   ลิงก์แทรกกลางประโยค (ข้อยกเว้นของ WCAG 2.5.5 จึงไม่ครอบ) สูง 21 */
.v-small > a:only-child {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* ท้ายหน้าตรวจใบหน้า — .v-foot เป็น flex แล้ว ตัวคั่น · จึงจัดกลางตามไปด้วย */
.v-foot a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 6px;
}

/* ---------------------------------------------------------------------------
   57. หลังบ้าน — หัวข้อและคำอธิบายที่อ่านออก (24 ก.ย. 2569)

   เจ้าของขอให้ไล่ตรวจส่วนที่เหลือแล้วขยายทั้งหัวข้อและคำอธิบาย วัดจริงครบ 11
   หน้าของ /admin/ ที่ 1440 และ 375: หัวข้อไม่มีปัญหา (h1 31 · หัวกล่อง 23)
   แต่ "คำอธิบาย" ทุกชนิดกองอยู่ที่ 14-16 เหมือนกันหมดทุกหน้า

   สามกฎข้างล่างต้องอยู่ท้ายไฟล์ เพราะตัวที่มันไปทับ — .main-nav a (985/3062)
   และ .button-small (593) — มีความเฉพาะเจาะจงเท่ากัน ตัวที่อยู่ทีหลังจึงชนะ
   ------------------------------------------------------------------------- */

/* แถบเมนูหลังบ้านสูง 38 มาตลอด ต่ำกว่าเพดาน 44 ที่ใช้ทั้งเว็บ
   min-height ไม่ใช่ height เพราะต่ำกว่า 1024 กล่องต้องโตเองได้เมื่อป้าย
   ขึ้นบรรทัดที่สอง — height จะหนีบไว้แล้วตัดทิ้ง */
.admin-nav a { min-height: 44px; font-size: 19px; }

/* ปุ่ม "ดูเว็บไซต์" อยู่บนหัวเว็บ ซึ่งอยู่นอก .admin-main กฎ
   .admin-main .button-small ในหมวด 48 จึงไปไม่ถึง มันค้างอยู่ที่ 16.5/40
   ของค่ากลางมาตลอด */
.admin-header .button-small { font-size: 17px; min-height: 44px; }

/* บนมือถือหัวเว็บซ่อนปุ่มนั้นไว้ (บรรทัด 2771) ลิงก์ท้ายหน้าจึงเป็นทางเดียว
   ที่จะกลับไปดูหน้าเว็บ เป็นปุ่มที่มีคำ ไม่ใช่ของประดับ */
.admin-footer .text-link { font-size: 18px; }

/* ชิปช่วงวันบนหน้าสถิติเป็นตัวกรองหลักของหน้านั้น แต่ .pill ตั้งกล่องตายตัว
   ไว้ 34px height: auto ปลดล็อกให้ 18px มีที่อยู่ */
.admin-main .pill-row.admin-title-actions .pill {
  height: auto;
  min-height: 38px;
  font-size: 18px;
}

/* ---------------------------------------------------------------------------
   58. หลังบ้านบนมือถือ — ของที่กดได้ (24 ก.ย. 2569)

   วัดที่ 375: ปุ่มไอคอนบนหัวเว็บ 36 (ตัวหนึ่งคือปุ่มออกจากระบบ) · โลโก้ 40 ·
   ปุ่มในแถว "แก้ไข/ซ่อน/คัดลอก" 42 · ชิประยะเวลาในหน้าแก้ไข 34 ·
   ชิปช่วงวันในหน้าสถิติ 38 · หัวข้อพับของกล่องวิดีโอคอล 36 ·
   ชื่อคนในตารางสถิติ 21 — ทั้งหมดต่ำกว่า 44

   เฉพาะจอมือถือ เดสก์ท็อปมีเมาส์ชี้ได้แม่นและหัวเว็บตั้งใจให้เตี้ย
   ------------------------------------------------------------------------- */

@media (max-width: 759.98px) {
  /* บรรทัด 2777 ตั้งไว้ 36 ปุ่มสลับโหมดสีกับปุ่มออกจากระบบอยู่ในนี้ทั้งคู่ */
  .admin-header .icon-btn { width: 44px; height: 44px; min-width: 44px; }
  .admin-header .icon-btn .icon { width: 20px; height: 20px; }
  .admin-brand { min-height: 44px; }

  /* ปุ่มประจำแถว — .button-small เป็น 40 ของค่ากลาง วัดจริงได้ 42 */
  .admin-main .button-small { min-height: 44px; }

  /* ชิประยะเวลาในหน้าแก้ไข: .pill ตั้งกล่องตายตัว 34 ต้องปลด height ก่อน */
  .admin-main .field > .pill-row .pill { height: auto; min-height: 44px; }
  .admin-main .pill-row.admin-title-actions .pill { min-height: 44px; }

  .face-panel details.face-call > summary { min-height: 44px; }

  /* ชื่อคนในตารางสถิติเป็นลิงก์สูง 21 ในช่องที่สูง 93 — padding คู่กับ margin
     ติดลบทำให้พื้นที่กดเป็น 56 โดยตารางไม่สูงขึ้นเลย (วัดแล้ว: แถวคง 93
     ตารางคง 1165 ส่วน display:block + padding ดันตารางขึ้นเป็น 1429) */
  .admin-table td a {
    display: inline-block;
    padding-block: 12px;
    margin-block: -12px;
  }
}

/* ---------------------------------------------------------------------------
   59. หลังบ้านบนเดสก์ท็อป — แท็บสิบอันไม่เคยพอดีแถวเดียว (24 ก.ย. 2569)

   หมวดที่ 1100px ย้ายเมนูขึ้นไปอยู่แถวเดียวกับโลโก้ (grid-row 1 คอลัมน์กลาง)
   แล้วตั้ง justify-content: center กับ overflow: visible ซึ่งใช้ได้ตอนเมนูมี
   ไม่กี่อัน แต่ตอนนี้มีสิบอัน วัดที่ 1440: แท็บกินพื้นที่ 1020 ในช่องกลาง 939
   และเพราะ justify เป็น center ส่วนเกินก็ล้นออกทั้งสองข้างเท่า ๆ กัน โดย
   overflow: visible ไม่ตัดอะไรเลย ผลคือแท็บแรกทับป้าย "หลังบ้าน" ข้างโลโก้
   (ซ้าย 172 ขณะที่ช่องเริ่มที่ 241) และแท็บสุดท้ายทับปุ่ม "ดูเว็บไซต์"
   เลื่อนก็ไม่ได้เพราะ overflow ไม่ใช่ auto — ของที่ล้นจึงเข้าไม่ถึงถาวร

   คืนเมนูลงแถวของตัวเองเหมือนจอแคบ ได้ความกว้างเต็ม 1376 แท็บใช้ 1020
   เหลือที่ว่าง 356 เห็นครบสิบอันพร้อมกัน ไม่มีอะไรทับกัน หัวเว็บสูงขึ้น 39px
   overflow-x กับ mask เก็บไว้เป็นตาข่ายรับ เผื่อวันหลังมีเมนูเพิ่มอีก
   ------------------------------------------------------------------------- */

@media (min-width: 1100px) {
  .admin-nav {
    grid-column: 1 / -1;
    grid-row: 2;
    justify-content: flex-start;
    overflow-x: auto;
    mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 28px), transparent);
  }
}

/* ---------------------------------------------------------------------------
   60. การ์ดตัวเลขหลังบ้านบนมือถือ — ให้ลงตัว (24 ก.ย. 2569)

   หมวดที่ 559px บังคับสองคอลัมน์ไว้ เหตุผลเดิมคือกันการ์ดสี่ใบเรียงเดี่ยว
   ซึ่งตอนนั้นกินความสูง 760px — วัดใหม่วันนี้การ์ดเตี้ยลงแล้ว สี่ใบเรียงเดี่ยว
   เหลือ 413px เหตุผลเดิมจึงไม่ตรงขนาดนั้นอีก แต่ยังไม่คุ้มจะเรียงเดี่ยวทั้งหมด

   ปัญหาจริงที่วัดได้ที่ 375 มีสองอย่าง และเกิดคนละที่กัน:

   1. บล็อกที่มีสามใบ (รายการ · ตรวจใบหน้า · รายงาน) เหลือใบเดียวค้างแถวล่าง
      ข้าง ๆ ที่ว่าง และช่องข้อความในการ์ดกว้างแค่ 99px ขณะที่ป้ายอย่าง
      "โปรไฟล์จริงที่เผยแพร่" ต้องการ 109 ป้ายจึงตกบรรทัด การ์ดสองใบบนสูง 123
      อีกใบสูง 96  บีบไอคอนหรือ padding ก็ไม่พอ เพราะหน้าตรวจใบหน้ามีป้าย
      ยาวกว่านั้นอีกสองอัน — สามใบจึงเรียงเดี่ยวไปเลย ได้ความกว้างเต็ม 343
      ป้ายทุกอันอยู่บรรทัดเดียว การ์ดเท่ากันหมด สูงขึ้น 51-78px

   2. บล็อกที่มีสี่ใบ (สถิติ) แถวบนสูง 96 แถวล่าง 123 เพราะป้ายใบหนึ่งตกบรรทัด
      grid-auto-rows: 1fr ทำให้ทุกแถวสูงเท่ากัน สองคอลัมน์จึงยังอยู่ครบ
      (ห้องรวมกับใบสมัครไม่มีป้ายตกบรรทัดอยู่แล้ว ความสูงจึงไม่เปลี่ยน)
   ------------------------------------------------------------------------- */

@media (max-width: 559px) {
  /* quantity query: ลูกตัวแรกที่นับจากท้ายเป็นลำดับสาม = มีลูกสามตัวพอดี */
  .admin-stats > :first-child:nth-last-child(3),
  .admin-stats > :first-child:nth-last-child(3) ~ * { grid-column: 1 / -1; }

  .admin-stats { grid-auto-rows: 1fr; }
}

/* ---------------------------------------------------------------------------
   61. ลิงก์ในเนื้อความ — ทำครั้งเดียวใช้ร่วมกัน (28 ก.ย. 2569)

   ช่องทางติดต่อเรื่องข้อมูลส่วนบุคคลกดได้แล้ว แต่มันอยู่กลางประโยค ไม่ใช่ปุ่มเดี่ยว
   .text-link เป็น inline-flex สูงอย่างน้อย 44px (487) เอามาวางกลางข้อความตรง ๆ
   จะกลายเป็นกล่องสูง 44px แทรกกลางบรรทัด

   ไฟล์นี้แก้เรื่องนี้มาแล้วสามที่ด้วยกฎที่ผูกกับหน้าใคร หน้ามัน — .login-note (3554)
   .chat-system (5830) .notif-foot (7209) โดยที่ 7208 เขียนไว้เองว่า "the same fix
   as .chat-system .text-link" แปลว่ารู้กันอยู่แล้วว่าเป็นแพตเทิร์นเดียวกัน
   แค่ไม่เคยยกขึ้นมาเป็นของกลาง อันนี้คือของกลางอันนั้น ใช้เป็นตัวขยายของ .text-link
   ครั้งต่อไปที่ต้องใช้ ให้ใส่คลาสนี้ ไม่ต้องเขียนกฎใหม่ผูกกับหน้า
   (สามอันเดิมยังอยู่ ยังทำงานถูก ถ้าจะย้ายมาใช้อันนี้ต้องไปเติมคลาสใน HTML
   ของสามหน้านั้น จึงไม่รวบในรอบนี้)

   text-underline-offset 3px ตามสองอันหลัง เพราะภาษาไทยมีสระล่างกับวรรณยุกต์
   ใต้บรรทัด ถ้าเส้นใต้ชิดเกินไปจะทับ ุ ู ฺ จนอ่านไม่ออก

   ทำไมต้องมีเส้นใต้ ไม่ใช่แค่สี: ลิงก์ในเนื้อความของเว็บนี้ได้แค่ a { color } (307)
   คือแยกจากข้อความรอบข้างด้วยสีอย่างเดียว ลิงก์เส้นนี้พาไปใช้สิทธิ์ตามกฎหมาย
   (ขอดู ขอลบ ถอนความยินยอม) จึงต้องเห็นว่ากดได้โดยไม่ต้องแยกแยะสี
   ลิงก์ที่อยู่ในประโยคได้รับการยกเว้นเกณฑ์เป้าแตะ 44px (WCAG 2.5.5 ข้อยกเว้น inline)
   ------------------------------------------------------------------------- */

.text-link.text-link-inline {
  display: inline;
  min-height: 0;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.text-link.text-link-inline:hover,
.text-link.text-link-inline:focus-visible { text-decoration-thickness: 2px; }

/* ---------------------------------------------------------------------------
   62. หน้าบ้านบนมือถือ — ของที่กดได้ (28 ก.ย. 2569)

   วัดที่ 375: ช่องค้นหา 36 · "ใกล้ฉัน" 21 · ปุ่มเรียงลำดับ 30 · หัวใจบนการ์ด 30 ·
   ปุ่มไอคอนบนหัวเว็บ 34 (30 ที่ 320) · แถบเลื่อนอายุ 16 · โลโก้หน้าเข้าสู่ระบบ 40

   ปุ่มพวกนี้ "วาด" ตัวเองทั้งนั้น — มีพื้น มีขอบ มีมุมโค้ง การขยายกล่องจึงเปลี่ยน
   หน้าตา และ box-sizing: border-box (252) ทำให้เติม padding แล้วกล่องไม่โตอยู่ดี
   ทางที่เหลือคือขยาย "พื้นที่กด" ด้วยกล่องใสที่ลอยออกนอกผัง ตัวที่มองเห็นไม่ขยับ
   สักพิกเซล  pseudo-element ไม่เคยเป็นเป้าของอีเวนต์ ตัวจัดการคลิกที่ใช้
   closest() จึงยังได้ปุ่มตัวเดิมเหมือนเดิม

   หน่วยวัดที่พลาดกันบ่อย: กล่องที่ absolute ยึดกับ **padding box** ของตัวตั้ง
   ไม่ใช่ border box — ขอบ 1px ของปุ่มจึงต้องบวกคืนในทุก inset ไม่งั้นได้ 42 ไม่ใช่ 44
   ------------------------------------------------------------------------- */

@media (max-width: 759.98px) {

  /* ช่องค้นหา: กรอบสูง 48 อยู่แล้ว (4069) ช่องข้างในกลับเป็น 36 */
  .search-field input { min-height: 44px; }

  /* "ใกล้ฉัน" เป็นลิงก์ในกล่องควบคุม ไม่ใช่ลิงก์ในประโยค ข้อยกเว้นของ WCAG
     จึงไม่ครอบ  44 พอดีกับกล่องเนื้อหา 46px ของช่องค้นหา เหลือข้างละ 1px */
  a.search-area { position: relative; }
  a.search-area::before {
    content: "";
    position: absolute;
    left: -1px;
    right: -1px;
    top: 50%;
    height: 44px;
    margin-top: -22px;
  }

  /* เม็ดเรียงลำดับสูง 30 ตามแบบ และ select ข้างในถูกตรึงไว้ที่ 28 — ทั้งคู่ไม่ขยับ
     ::before ไม่ใช่ ::after เพราะ ::after คือลูกศรของเม็ดนี้ (737/771/4203/4781) */
  .sort-pill.sort-open::before {
    content: "";
    position: absolute;
    inset-inline: -1px;
    top: 50%;
    height: 44px;
    transform: translateY(-50%);
  }

  /* หัวใจบนการ์ด: จานขาว 30px คงเดิมที่ 8px จากขอบรูปตามแบบ (3980)
     ผูกกับ .provider-card เพราะหน้าโปรไฟล์ก็แขวน .card-fav ไว้เหมือนกัน แต่ที่นั่น
     เป็น .profile-hero-bar .hero-btn ซึ่งเป็น 44 อยู่แล้ว (5075) ถ้าเขียนลอย ๆ
     มันจะกลายเป็น 58 ที่นั่น */
  .provider-card .card-fav::after {
    content: "";
    position: absolute;
    inset: -7px;
  }

  /* ปุ่มไอคอนบนหัวเว็บ — จานโตไม่ได้เลย  --brand-reserve (6742) ไม่ใช่ค่าเผื่อ
     แต่เป็นผลบวกของเฟอร์นิเจอร์ในแถบพอดี และมันเป็นตัวลบในสูตรขนาดชื่อเว็บ (6769)
     ทุก 1px ที่จานโต ชื่อเว็บหด 4px  ดัน 34 เป็น 44 = ชื่อเหลือ 17.75px ทุกหน้า
     จึงขยายเฉพาะพื้นที่กด: padding box 32 ใน border box 34 ทุก inset บวกขอบคืน 1px
       บน -11 -> 1-11 = -10   ล่าง -1 -> 33+1 = 34 (ขอบล่างจานพอดี)  = สูง 44
       ข้าง -4 -> -3 และ 37   = กว้าง 40 คือครึ่งหนึ่งของช่องไฟ 6px ข้างละครึ่ง
                                สี่ปุ่มจึงชนกันพอดีและไม่ซ้อนกัน
     ขยายขึ้นบนล้วน ๆ กินจาก padding 16px ด้านบนของแถบ (12 ที่ 3794 + 4 ที่ 3831)
     ไม่ยื่นลงล่าง เพราะใต้จานคือคำโปรยซึ่งอยู่ในลิงก์ <a class="brand"> จะไปแย่งกด */
  .mobile-topbar .header-actions > .icon-btn::after {
    content: "";
    position: absolute;
    inset: -11px -4px -1px;
  }

  /* แถบเลื่อนช่วงอายุ: 16px คือทั้งกล่อง และทั้งกล่องคือพื้นที่ลาก ไม่ใช่แค่รางที่เห็น
     ทดสอบด้วยการกดจริงที่ 375 แล้ว — กดสูงจากขอบบน 8px ได้ค่าเดียวกับกดบนราง
     เบราว์เซอร์วาดรางกับปุ่มที่ขนาดเดิมโดยจัดกลางในความสูงที่ให้ หน้าตาจึงไม่เปลี่ยน
     กล่องคู่สูงขึ้น 75.5 -> 131.5 ระยะระหว่างสองรางจึงห่างพอให้ไม่กดผิดอัน */
  .range-pair input[type="range"] { min-height: 44px; }

  /* โลโก้บนหน้าเข้าสู่ระบบเป็นลิงก์กลับหน้าแรก  .brand ตั้งพื้นไว้ 40 (917) ซึ่งเป็น
     ของทุกหน้า แตะไม่ได้ จึงแก้เฉพาะหน้านี้  margin ติดลบหักความสูงที่เพิ่มออกจาก
     margin box ซึ่งเป็นสิ่งที่คอลัมน์ flex ใช้คิด — วัดแล้วทุกอย่างใต้ลิงก์อยู่ที่เดิม
     สโคปด้วย body.login-page เพราะหน้าล็อกอินหลังบ้านใช้คลาสเดียวกันแต่สูง ~53.6
     อยู่แล้ว กฎลอย ๆ จะไปหด 4px ที่นั่น */
  body.login-page .login-brand { min-height: 44px; margin-block: -2px; }
}

/* ต่ำกว่า 360 จานเหลือ 30 และช่องไฟเหลือ 5 (6780-6783) ค่าเดิมจะได้แค่ 42
   padding box 28: บน -16 -> 1-16 = -15  ล่าง 0 -> 29  = สูง 44
   ข้าง -3.5 -> -2.5 และ 32.5 = กว้าง 35 คือเต็มช่องไฟพอดี ชนกันไม่ซ้อนกัน
   ต้องอยู่ "หลัง" บล็อก 759.98 เพราะที่ 320 ทั้งสองบล็อกตรงเงื่อนไข ตัวหลังชนะ
   และตั้งเฉพาะ inset โดยรับ content/position มาจากกฎบน */
@media (max-width: 359.98px) {
  .mobile-topbar .header-actions > .icon-btn::after { inset: -16px -3.5px 0; }
}

/* ---------------------------------------------------------------------------
   63. หน้าตัวหนังสือยาวบนเดสก์ท็อป — ความยาวบรรทัด (28 ก.ย. 2569)

   วิธีใช้ · กติกา · ประกาศความเป็นส่วนตัว ใช้ .guide-container ซึ่งกว้าง 1300
   เท่า --shell  1300 เป็นความกว้างที่ถูกสำหรับ "ตารางการ์ด" แต่ไม่ใช่สำหรับ
   "ย่อหน้า"  วัดที่ 1440 ก่อนแก้: บรรทัดยาวสุด 188 / 222 / 218 ตัวอักษร

   ตัวเทียบคือหน้าค่าลงประกาศ ซึ่งหมวด 53 หนีบย่อหน้าไว้ที่ 880 ไปแล้วด้วยเหตุผล
   เดียวกัน ("การ์ดอยากได้ทั้ง 1200 ย่อหน้าไม่ใช่") และเจ้าของรับแล้ว บรรทัดยาวสุด
   ที่นั่นคือ 145 ตัวอักษร ใช้ 880 เท่ากันเพื่อให้ทั้งเว็บใช้ค่าเดียว

   หลังแก้ วัดที่ 1440: 125 / 128 / 150 — อยู่ในย่านเดียวกับหน้าที่รับแล้ว

   .guide-steps ได้รับการยกเว้น มันคือตารางสามช่องซึ่ง "อยากได้" ความกว้างเต็ม
   ลองหนีบดูแล้วแต่ละช่องเหลือ 241px = 40 ตัวอักษรต่อบรรทัด ซึ่งแคบเกินไป
   ปล่อยไว้ที่ 1252 ได้ 67 ตัวอักษรต่อช่อง กำลังดี

   ที่ 900 ซึ่งเป็นขอบของกฎ ตัวคุมกว้าง 885 อยู่แล้ว 880 จึงไม่บีบอะไร ไม่มีการ
   กระโดด และต่ำกว่า 900 กฎไม่ทำงานเลย
   ------------------------------------------------------------------------- */

@media (min-width: 900px) {
  .guide-container > * { max-width: 880px; margin-inline: auto; }
  .guide-container > .guide-steps { max-width: none; }
}

/* ---------------------------------------------------------------------------
   64. กล่องแจ้งในหลังบ้านบนเดสก์ท็อป — ความยาวบรรทัด (28 ก.ย. 2569)

   ไล่ตรวจหลังบ้านที่ 760-1920 แล้วสะอาดเกือบหมด ไม่มีกล่องถูกตัดสักจุด ไม่มีหน้าไหน
   เลื่อนแนวนอน ความกว้างโตตามจอแล้วหยุดที่ 1300 เหลือปัญหาเดียวคือ .notice

   กล่องแจ้งมีเจ็ดอันในหลังบ้าน เป็นข้อความอธิบายที่เจ้าของต้องอ่านเพื่อรู้ว่าต้องทำ
   อะไรต่อ ("เริ่มต้นเพิ่มผู้ให้บริการจริง" · "ฟอร์มสมัครปิดอยู่" · "รายงานไม่ได้ซ่อน
   โปรไฟล์ให้อัตโนมัติ" ฯลฯ) แต่มันกินความกว้างเต็ม 1099-1252 วัดที่ 1440 หน้า
   รายการได้ **227 ตัวอักษรในบรรทัดเดียว** ยาวที่สุดในเว็บทั้งเว็บ ยาวกว่าหน้ากติกา
   ที่เพิ่งแก้ไปในหมวด 63 (222) เสียอีก  หน้าใบสมัคร/ตรวจใบหน้า/รายงานอยู่ที่ 144-146

   หนีบเฉพาะ "เนื้อหา" ไม่ใช่ตัวกล่อง แถบสีจึงยังกว้างเต็มเหมือนเดิม เปลี่ยนแค่
   ข้อความข้างในให้เป็นคอลัมน์ที่อ่านได้  880 คือค่าเดียวกับหมวด 53 และ 63
   ผลที่วัดได้: 227 -> 114 ที่ 1440 และเท่ากันที่ 1024 กับ 1920
   กล่องสูงขึ้น 94 -> 123 เพราะข้อความตกเป็นสองบรรทัด ซึ่งคือสิ่งที่ต้องการ

   สโคปด้วย .admin-main เพราะ .notice ใช้ที่หน้าชำระเงินของหน้าบ้านด้วย
   ------------------------------------------------------------------------- */

@media (min-width: 900px) {
  .admin-main .notice > div { max-width: 880px; }
  /* คำอธิบายใต้หัวกล่อง เป็นร้อยแก้วแบบเดียวกัน หน้าใบสมัครมีแผงกว้างเต็ม
     คำอธิบายจึงยืดไปถึง 1153 ส่วนแผงแคบ ๆ ที่อื่นต่ำกว่า 880 อยู่แล้ว กฎนี้จึง
     ไม่ไปแตะ */
  .admin-main .panel-head p { max-width: 880px; }
}

/* วัดแล้วเหลือของที่กว้างเกิน 880 อีกสองชิ้น ทั้งคู่ "ไม่ใช่" ร้อยแก้ว จึงไม่แตะ:
   .admin-person-meta (937) เป็นแถวข้อมูลของการ์ด — ประเภท · อายุ · ย่าน · สถานะ
   คั่นด้วยจุด ไม่ใช่ประโยคที่อ่านรวด การหนีบจะทำให้มันตกบรรทัดโดยไม่ได้อะไร
   .admin-save-bar .form-help (1056) เป็นบรรทัดเดียวข้างปุ่มบันทึกในแถบลอย
   มันคือ flex: 1 1 280px ที่ตั้งใจให้ดันปุ่มไปชิดขวา หนีบแล้วจะเกิดช่องว่างกลางแถบ */

/* ---------------------------------------------------------------------------
   65. หน้าเข้าสู่ระบบหลังบ้าน (28 ก.ย. 2569)

   ไล่ตรวจหลังบ้านบนมือถือรอบสุดท้าย ทั้ง 11 view ที่ 375 และ 320 สะอาดหมด —
   ไม่มีปุ่มต่ำกว่า 44 ไม่มีข้อความต่ำกว่า 17 ไม่มีจุดถูกตัด ไม่เลื่อนแนวนอน
   แท็บที่เลือกอยู่ยังถูกเลื่อนมาให้เห็นตามหมวด 58/59

   เหลือหน้าเดียวที่ไม่เคยตรวจเลย เพราะมันคือสถานะ "ยังไม่ได้ล็อกอิน" ซึ่งการปลอม
   เซสชันเพื่อเข้าไปวัดทำให้มองไม่เห็น — หน้าเข้าสู่ระบบ และมันมีสองจุด:

   1. คำโปรยใต้ชื่อเว็บบนการ์ด ("จัดการผู้ให้บริการ") อยู่ที่ 14.5px จากกฎฐาน
      .brand-text small (959) ซึ่งเป็นข้อความที่เล็กที่สุดในเว็บทั้งเว็บตอนนี้
      แก้เฉพาะการ์ดนี้ ไม่แตะกฎฐาน เพราะ .mobile-topbar ทับไว้ที่ 16 และถูกล็อก
      ด้วยเพดาน 76px ของ .brand (ดู [[tongpok-bigger-type]] — ห้ามขยาย)
      กฎฐานมี white-space: nowrap + overflow: hidden วัดแล้วที่ 17px กว้าง 83
      ไม่ถูกตัดทั้งที่ 320 / 375 / 1440 การ์ดสูงขึ้น 54 -> 57

   2. ปุ่มสลับโหมดสีในแถบท้ายการ์ดอยู่ที่ 40px จากค่ากลางของ .icon-btn (607)
      หมวด 58 ยก .admin-header .icon-btn เป็น 44 ไปแล้ว แต่ปุ่มนี้อยู่ในแถบท้าย
      ของการ์ด ไม่ใช่หัวเว็บ จึงไม่โดน — 44 ที่นี่ไม่ดันอะไร แถบท้ายสูง 63 เท่าเดิม

   ไม่ผูกกับความกว้าง เพราะทั้งสองจุดเล็กเท่ากันบนเดสก์ท็อป และหลังบ้านไม่ได้อยู่
   ใต้ข้อผูกของแบบที่ 1440 แบบหน้าบ้าน — หมวด 57 ก็ขยายตัวอักษรหลังบ้านทุกความกว้าง
   ------------------------------------------------------------------------- */

body.admin-page .login-brand .brand-text small { font-size: 17px; }
.admin-login-foot .icon-btn { width: 44px; height: 44px; min-width: 44px; }
