/* ---------------------------------------------------------------------------
   Colour tokens.

   Two themes, one set of names. Light is the default and stays the default: the
   theme is whatever data-theme on <html> says, and nothing sets it to dark unless
   a person asked for dark. prefers-color-scheme is deliberately NOT consulted --
   a salesperson showing the catalogue on their own phone must see the same screen
   the customer would, and a phone in dark mode would otherwise show them a page
   nobody has ever reviewed.

   Three kinds of colour live here, and telling them apart is the whole job:

     brand    — graphite chrome and the red accent, taken from the Metro Power
                System logo (a crimson mark, a silver swoosh, charcoal lettering).
                IDENTICAL in both themes. The header and footer stay graphite on a
                dark page; that is what keeps the site recognisable, and what keeps
                the internal module visibly different from the public catalogue
                (see internal.css, line 1).
     on-dark  — text and rules that sit ON the navy chrome. Always light, in both
                themes, because what is under them never changes.
     surface  — page, cards, panels, borders, body text, status tints. These flip.

   A colour written straight into a rule is a colour that cannot flip, so any new
   one belongs here first. bin/check_theme.php fails the build on stragglers.
   --------------------------------------------------------------------------- */
:root {
  /* Native parts the page cannot style -- the open <select> list, scrollbars, the date/time pickers,
     autofill -- follow this, not the tokens. Without it they stayed a white list of black text on the
     dark page (DARKTEXT · 28 Sep 2026). */
  color-scheme: light;

  /* brand — the same in both themes. A neutral graphite chrome, not blue navy, so the logo's
     silver-and-crimson mark reads against it. */
  --navy: #24272e;
  --navy-soft: #2f333b;
  --navy-lift: #363c45;
  --steel: #5f6b78;
  /* A shade darker than the public header: the internal module's chrome. The difference is
     deliberate, so that a screen from the back office is never mistaken for the public catalogue
     in a demo. */
  --chrome: #1b1e24;
  /* The logo's crimson. It has to double as button FILL (white on it) and as link/price TEXT on
     both light and dark surfaces, so it is the logo's red family at a working brightness rather
     than the mark's own deep maroon -- a maroon dark enough to match the emblem exactly would be
     unreadable as text on a dark panel. The deep maroon lives on --accent-hover instead. */
  --accent: #e23b4d;
  --accent-hover: #c31f38;
  --accent-bright: #f26076;
  --accent-bright-bg: rgba(242, 96, 118, .16);
  /* Delete buttons: an orange-leaning red, kept clearly apart from the crimson brand so a "delete"
     is never mistaken for a primary action. --danger is TEXT on a tint and lightens in the dark
     theme; a solid fill needs its own darker value. */
  --danger-solid: #b3281a;
  /* Its twin for "confirm / the customer accepted": --ok is TEXT and turns light green in the dark
     theme, so a button filled with it put white type on #4ade80 (1.74:1) -- unreadable. */
  --ok-solid: #0d7d4e;

  /* on the navy chrome — always light, in both themes */
  --on-dark: #ffffff;
  --on-dark-soft: #cbd6e3;
  --on-dark-muted: #93a9c0;
  --on-dark-faint: #6f89a4;
  --on-dark-hover: rgba(255, 255, 255, .08);
  --on-dark-line: rgba(255, 255, 255, .1);
  --on-dark-border: rgba(255, 255, 255, .22);

  /* surfaces — these flip */
  --bg: #f4f6f9;
  --card: #ffffff;
  --surface: #f1f4f8;
  --surface-2: #fafbfd;
  --hover: #f0f3f7;
  --line: #dfe5ec;
  --line-soft: #eef2f7;

  /* text — flips.
     --muted and --ok are three and two per cent darker than they were: at their old values they
     measured 4.31:1 and 4.42:1 against the surfaces they actually sit on, just under the 4.5:1
     that WCAG AA asks of body text. The shift is invisible side by side and bin/check_theme.php
     will not let them drift back. */
  --ink: #14202e;
  --muted: #617187;
  --faint: #b6c1cf;

  /* status — flips, tint and text together or the contrast breaks */
  --ok: #0d7d4e;
  --ok-bg: #e6f5ee;
  --ok-line: #b7e0cc;
  --danger: #b3281a;
  --danger-bg: #fdecea;
  --danger-line: #f3c2bd;
  --warn: #7a5c07;
  --warn-bg: #fff8e6;
  --warn-line: #f3dfa8;
  --info: #2c5282;
  --info-bg: #eef4fb;
  --info-line: #cfe0f2;
  --accent-soft: #fcecef;
  /* Buying cost. Its own colour, not a status one: it is neither good news nor bad, and it must
     never be mistaken for a selling price on a screen a salesperson reads in a hurry. */
  --cost: #b8860b;

  /* The guided tour dims the page and draws on top of the dimming, so what is behind its panel is
     the dim layer, not the page. Fixed in both themes for that reason. */
  --tour-dim: rgba(16, 17, 21, .66);
  --tour-panel: rgba(26, 28, 34, .9);

  --radius: 10px;
  --shadow: 0 1px 2px rgba(13, 27, 42, .06), 0 8px 24px rgba(13, 27, 42, .06);
  --overlay: rgba(22, 24, 28, .55);
}

/* Dark. Only the flipping groups are restated -- brand and on-dark are inherited
   unchanged, which is the point of separating them.

   Surfaces get lighter as they come forward (bg < card < surface < hover), the
   reverse of the light theme, because on a dark page "raised" reads as lighter.
   Status text is lightened well past its light-theme value: a dark red on a dark
   panel is unreadable, and an unreadable error message is worse than none. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: #10161d;
  --card: #171f29;
  --surface: #1d2833;
  --surface-2: #1a2029;
  --hover: #232f3c;
  --line: #2b3744;
  --line-soft: #222d39;

  --ink: #e4eaf1;
  --muted: #93a3b6;
  /* Lighter than the obvious mirror of the light value. --faint carries placeholder text and
     empty-state labels, which are still text someone has to read; at #6b7c90 it measured 3.50:1
     on a raised panel. */
  --faint: #8996a6;

  --ok: #4ade80;
  --ok-bg: #10281c;
  --ok-line: #1f4d33;
  --danger: #ff8a80;
  --danger-bg: #2c1615;
  --danger-line: #4d2523;
  --warn: #fbbf24;
  --warn-bg: #2a2210;
  --warn-line: #4a3c15;
  --info: #7ab8f5;
  --info-bg: #14202e;
  --info-line: #27405c;
  --accent-soft: #2a1214;
  --cost: #e0b34a;

  /* A soft shadow is invisible on a dark page, so depth comes from the border
     instead and the shadow only deepens the separation. */
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .35);
  --overlay: rgba(0, 0, 0, .68);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'IBM Plex Sans Thai', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size: 15px;
  line-height: 1.6;
}

a { color: inherit; text-decoration: none; }

/* DARKTEXT (28 Sep 2026): a field nobody styled took the browser's own colours -- a white box of black type
   in the middle of a dark page; with color-scheme: dark it turned grey with a 2.43:1 placeholder instead.
   Zero specificity (:where on both halves), so ANY rule that styles a field still wins; this only fills in
   for the browser's defaults, and only in dark -- light keeps exactly what it had. */
:where(:root[data-theme="dark"]) :where(input, textarea, select) { background-color: var(--surface); color: var(--ink); }
:where(:root[data-theme="dark"]) ::placeholder { color: var(--faint); opacity: 1; }
code, .mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }

/* The browser's own rule for [hidden] is `display: none` at the weakest possible specificity,
   so ANY class that sets a display beats it -- and el.hidden = true silently does nothing. That
   is how an empty attachment preview and a stale result count stayed on screen. Stating it once
   here fixes every such element, including ones not written yet. */
[hidden] { display: none !important; }

.wrap { max-width: 1200px; margin: 0 auto; padding: 0 20px; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 8px;
  z-index: 100;
  background: var(--card);
  color: var(--ink);
  padding: 10px 16px;
  border-radius: 8px;
  box-shadow: var(--shadow);
}
.skip-link:focus { left: 20px; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ---------- language switch ----------
   Three links on the navy chrome, so on-dark throughout. The current one is not a link: there is
   nowhere for it to go, and a disabled-looking link that does nothing is worse than plain text. */
.lang-switch {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--on-dark-border);
  border-radius: 8px;
  overflow: hidden;
}
.lang-opt {
  padding: 7px 10px;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1;
  color: var(--on-dark-soft);
  background: var(--on-dark-hover);
  white-space: nowrap;
  transition: color .15s, background .15s;
}
.lang-opt + .lang-opt { border-left: 1px solid var(--on-dark-border); }
a.lang-opt:hover { color: var(--on-dark); background: var(--navy-soft); }
.lang-opt.is-current { color: var(--on-dark); background: var(--accent); }

/* ---------- theme toggle ----------
   Sits on the navy chrome on both sides of the site, so its own colours are on-dark and do not
   flip. Only the icon inside it swaps. */
.theme-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid var(--on-dark-border);
  background: var(--on-dark-hover);
  color: var(--on-dark-soft);
  cursor: pointer;
  transition: color .15s, border-color .15s;
}
.theme-toggle:hover { color: var(--accent); border-color: var(--accent); }
.theme-toggle svg { width: 17px; height: 17px; }
/* One button, two icons: the moon offers dark, the sun offers a way back. Which one shows is
   decided by the theme itself, so the button cannot end up disagreeing with the page. */
.theme-toggle .i-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .i-sun { display: block; }
:root[data-theme="dark"] .theme-toggle .i-moon { display: none; }

/* ---------- header ---------- */
.site-header {
  background: var(--navy);
  color: var(--on-dark);
  position: sticky;
  top: 0;
  z-index: 20;
}
.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 74px;
}
/* The brand is the one thing in this bar that must never be squeezed. Adding the account and
   basket chips pushed the row over the available width, and because .brand could shrink it was
   the part that gave -- the company name broke onto three lines and the tagline onto six.
   Everything else in the header now yields before the logo does. */
.brand { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; }
.brand-mark {
  background: var(--accent);
  color: var(--on-dark);
  font-weight: 700;
  letter-spacing: .5px;
  padding: 7px 10px;
  border-radius: 6px;
  font-size: 14px;
}
.site-logo {
  /* The full logo -- emblem plus the METRO POWER SYSTEM wordmark -- cropped tight from the source
     onto its transparent ground, so it floats on the dark header with no chip. The wordmark is
     small at header size but legible; the brand text beside it repeats the name. */
  height: 56px;
  width: auto;
  aspect-ratio: 682 / 606;
  object-fit: contain;
}
.main-nav .nav-cta { display: inline-flex; align-items: center; gap: 7px; }
.brand-text { min-width: 0; }
.brand-text strong { display: block; font-size: 16px; line-height: 1.25; white-space: nowrap; }
.brand-text small {
  display: block;
  color: var(--on-dark-muted);
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 30ch;
}

.main-nav { display: flex; align-items: center; gap: 3px; flex-wrap: nowrap; }
.main-nav a {
  padding: 8px 10px;
  border-radius: 7px;
  color: var(--on-dark-soft);
  font-weight: 500;
  font-size: 14px;
  white-space: nowrap;   /* a nav label never breaks mid-word; the row stays one line */
}
.main-nav a:hover { background: var(--navy-soft); color: var(--on-dark); }
.main-nav a.active { color: var(--on-dark); background: var(--navy-soft); }
.main-nav .nav-cta {
  background: var(--accent);
  color: var(--on-dark);
  margin-left: 4px;
}
.main-nav .nav-cta:hover { background: var(--accent-hover); color: var(--on-dark); }

/* ---------- how the header gives way as it runs out of room ----------
   Seven nav links, a red call-to-action, two chips, a language switch and a theme toggle do not
   fit beside a logo on a 1366px laptop. Rather than let them collide, the bar sheds detail in the
   order it can most afford to lose it: the tagline (which the page repeats anyway), then the chip
   labels (the icons still read), then nav padding. The logo and the links themselves never move. */
@media (max-width: 1500px) {
  body:not(.internal) .brand-text small { display: none; }
}
@media (max-width: 1400px) {
  /* Measured on the live site: at 1366 the row ran 9px past the viewport and the theme toggle
     hung off the edge. Trimming the gaps and the nav padding buys back about 40px. */
  body:not(.internal) .main-nav a { padding: 8px 7px; font-size: 13.5px; }
  body:not(.internal) .main-nav { gap: 1px; }
  body:not(.internal) .header-inner { gap: 9px; }
  body:not(.internal) .lang-switch { transform: scale(.94); transform-origin: right center; }
}
@media (max-width: 1240px) {
  /* Icon-only chips. The count badge stays -- it is the whole point of the basket. */
  body:not(.internal) .nav-cart span:not(.count) { display: none; }
  body:not(.internal) .nav-cart { padding: 8px 11px; gap: 0; }
  body:not(.internal) .nav-cart .count { margin-left: 5px; }
}
@media (max-width: 1080px) {
  body:not(.internal) .main-nav .nav-cta span,
  body:not(.internal) .main-nav .nav-cta { font-size: 13px; }
  body:not(.internal) .brand-text strong { font-size: 15px; }
  body:not(.internal) .site-logo { height: 46px; }
}


/* ---------- mobile nav ---------- */
.nav-toggle {
  display: none;
  width: 40px;
  height: 40px;
  border: 0;
  background: transparent;
  cursor: pointer;
  padding: 9px;
  flex-direction: column;
  justify-content: space-between;
  order: 3;
}
.nav-toggle span {
  display: block;
  height: 2px;
  background: var(--on-dark);
  border-radius: 2px;
  transition: transform .2s ease, opacity .2s ease;
}
body.nav-open .nav-toggle span:nth-child(1) { transform: translateY(9px) rotate(45deg); }
body.nav-open .nav-toggle span:nth-child(2) { opacity: 0; }
body.nav-open .nav-toggle span:nth-child(3) { transform: translateY(-9px) rotate(-45deg); }

/* PUBLIC header only. Every selector is scoped to `body:not(.internal)` because the back office
   reuses the same `.main-nav` / `.nav-toggle` class names but collapses at 960px with an entirely
   different (in-flow, grouped) layout -- see internal.css. Without this scope the `position:absolute`
   below leaked onto the internal menu under 900px, so the back-office nav jumped between two layouts
   at the 900/960 boundary. */
/* The collapse point used to be 900px, chosen when the bar held a logo, six links and a CTA.
   It now also carries an account chip, a basket and the language switch, and measurement on the
   live site showed the row running 52px past a 1024px viewport -- the toggle and the language
   switch were simply off-screen. The panel now takes over while there is still room to spare. */
@media (max-width: 1100px) {
  body:not(.internal) .nav-toggle { display: flex; }
  /* Six links will not fit a phone header. The panel drops below the bar rather than
     squeezing the links until they wrap under the logo. */
  body:not(.internal) .main-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--navy);
    border-top: 1px solid var(--on-dark-line);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 8px 20px 16px;
    display: none;
  }
  body:not(.internal).nav-open .main-nav { display: flex; }
  body:not(.internal) .main-nav a { padding: 12px 10px; font-size: 15px; border-radius: 8px; }
  body:not(.internal) .main-nav .nav-cta { margin: 8px 0 0; justify-content: center; }
  body:not(.internal) .site-header { position: relative; }
  /* The basket and the account chip stay in the bar on a phone rather than going into the
     collapsed panel: a shopper mid-purchase should never have to open a menu to reach the cart. */
  body:not(.internal) .header-inner { gap: 8px; }
}
@media (max-width: 620px) {
  /* Logo only. The company name is the page title and the footer; the chips are not repeated
     anywhere, so they are what survives. */
  body:not(.internal) .brand-text { display: none; }
  body:not(.internal) .site-logo { height: 40px; }
}

/* ---------- shared ---------- */
main.wrap { padding-top: 28px; padding-bottom: 56px; min-height: 60vh; }
h1 { font-size: 26px; margin: 0 0 6px; }
h2 { font-size: 19px; margin: 0 0 14px; }
.muted { color: var(--muted); }
.page-head { margin-bottom: 22px; }
.page-head p { margin: 0; color: var(--muted); }

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

/* Live search suggestions (assets/js/suggest.js) — a dropdown under any search box, public and
   back office. Lives here in app.css so both sides get it; every token flips with the theme.
   The wrapper is .suggest-box, not .suggest -- `.suggest button` further down (the chat's suggested questions) turned
   every option into its own bordered card (SEARCHOPT · 5 ต.ค. 2569). */
.suggest-box { position: relative; }
.suggest-panel {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 50;
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: var(--shadow); max-height: 340px; overflow-y: auto; text-align: left;
}
.suggest-item {
  display: block; width: 100%; text-align: left; padding: 9px 14px;
  border: 0; border-bottom: 1px solid var(--line); background: none; cursor: pointer; font: inherit;
}
.suggest-item:last-child { border-bottom: 0; }
.suggest-item:hover, .suggest-item.suggest-active { background: var(--accent-soft); }
.suggest-label { display: block; font-size: 13.5px; color: var(--ink); }
.suggest-meta { display: block; font-size: 11.5px; color: var(--muted); margin-top: 1px; }
.suggest-empty { padding: 14px; color: var(--muted); font-size: 13px; text-align: center; }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--accent);
  color: var(--on-dark);
  border: 0;
  border-radius: 8px;
  padding: 10px 18px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.btn:hover { background: var(--accent-hover); }
.btn-ghost { background: var(--card); color: var(--ink); border: 1px solid var(--line); }
.btn-ghost:hover { background: var(--hover); }
/* SEARCHOPT (ผู้ใช้สั่ง 2 ต.ค. 2569 "ปุ่มค้นหาเป็นสีน้ำเงิน ส่วนปุ่มล้างเป็นสีแดง ทำทั้งระบบ"): the public site's ค้นหา. The back office
   paints it from its type, .btn-search (BTNCOLOR, internal.css) -- the same blue, which does not flip with the theme */
.btn.btn-search { background: #2563eb; color: #fff; border: 1px solid #2563eb; }
.btn.btn-search:hover { background: #1d4ed8; border-color: #1d4ed8; }

.tag {
  display: inline-block;
  background: var(--line-soft);
  /* --muted, not --steel: --steel is a brand colour and does not flip, so on a dark tint it was
     dark-on-dark at 2.15:1. */
  color: var(--muted);
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 12px;
  font-weight: 500;
}
.tag-sku {
  background: var(--accent-soft);
  color: var(--accent);
  font-family: 'JetBrains Mono', monospace;
}
.tag-stock { background: var(--ok-bg); color: var(--ok); }

/* ---------- grids ---------- */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 16px;
}
.product-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: box-shadow .15s, transform .15s;
}
.product-card:hover { box-shadow: var(--shadow); transform: translateY(-2px); }
.product-card .thumb {
  height: 120px;
  border-radius: 8px;
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.product-card .thumb img { max-width: 100%; max-height: 100%; object-fit: contain; }
.product-card .thumb .placeholder { color: var(--faint); font-size: 26px; font-weight: 700; }
.product-card .title {
  font-weight: 600;
  font-size: 14px;
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.product-card .meta { display: flex; gap: 6px; flex-wrap: wrap; }
.product-card .price { font-weight: 700; color: var(--accent); margin-top: auto; }
.product-card .price.ask { color: var(--muted); font-weight: 500; font-size: 13px; }
.product-card .price-vat { font-size: 11px; font-weight: 500; color: var(--muted); margin-left: 4px; }

/* ---------- ปุ่มซื้อ / ขอราคา บนการ์ด ----------
   The card body is one big anchor, so the action has to live beside it, not inside it: a <button>
   nested in an <a> never submits. The wrapper is the flex column the card used to be. */
.product-card-wrap { display: flex; flex-direction: column; }
.product-card-wrap .product-card { flex: 1; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.product-card-wrap .card-action {
  background: var(--card);
  border: 1px solid var(--line);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
  padding: 0 14px 14px;
}
.product-card-wrap .card-action .btn,
.product-card-wrap .card-action form { width: 100%; }
.btn-sm { padding: 7px 12px; font-size: 13px; }
.card-action .btn-sm { display: block; text-align: center; }

.buy-form { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }

/* ช่องจำนวน: ป้ายซ้าย · ช่องกรอกกับหน่วยอยู่ในกรอบเดียวกัน · ทั้งก้อนสูงเท่าปุ่มข้าง ๆ
   เดิมเป็น `flex-direction: column` ซ้อนสามชั้น (ป้าย/ช่อง/หน่วย) จึงสูงเป็นสามบรรทัด
   ขณะที่ปุ่มทุกอันสูงบรรทัดเดียว -- แถวเลยดูไม่เสมอกันทั้งที่ไม่มีใครตั้งใจ

   เส้นขอบอยู่ที่ `.qty-box` ไม่ใช่ที่ `<input>` เพราะหน่วยแต่ละคำกว้างไม่เท่ากัน
   (`เส้น` · `กล่อง` · `m`) การเผื่อ padding-right ของ input เป็นตัวเลขตายตัว
   จะพังทันทีที่เจอคำยาว */
.qty-field {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: var(--muted);
}
.qty-box {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 8px 11px;
  background: var(--card);
}
.qty-box input {
  width: 44px;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  font-size: 13.5px;
  text-align: right;
  background: transparent;
  color: var(--ink);
}
/* เส้นขอบย้ายไปอยู่ที่กรอบแล้ว -- วงโฟกัสของ input จึงต้องย้ายตามไปด้วย
   ไม่งั้นคนที่กด Tab มาถึงช่องนี้จะไม่เห็นว่าเคอร์เซอร์อยู่ตรงไหน */
.qty-box input:focus { outline: none; }
.qty-field:focus-within .qty-box { border-color: var(--accent); }
.qty-field .qty-unit { font-size: 12px; }

/* ---------- checkout / คำสั่งซื้อ ---------- */
.checkout-grid { display: grid; grid-template-columns: 1fr 340px; gap: 20px; align-items: start; }
@media (max-width: 820px) { .checkout-grid { grid-template-columns: 1fr; } }

.pay-grid { display: grid; grid-template-columns: auto 1fr; gap: 24px; align-items: start; }
@media (max-width: 620px) { .pay-grid { grid-template-columns: 1fr; } }

/* The QR keeps a white card under it in BOTH themes on purpose: a dark-mode QR rendered on a dark
   surface is unreadable to every phone camera, and this one is how the company gets paid. */
/* theme:fixed กล้องมือถืออ่าน QR บนพื้นเข้มไม่ได้ */
.qr-box {
  background: #ffffff;
  padding: 12px;
  border-radius: 10px;
  border: 1px solid #d8dcd9;
  display: inline-block;
  line-height: 0;
}
.qr-box svg { display: block; width: 190px; height: 190px; }
/* theme:end */

.order-pill {
  display: inline-block;
  padding: 5px 13px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 700;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
}
.order-pill.status-pending  { background: var(--warn-bg, #fbf0dc); color: #8a5a0b; border-color: #e2c48a; }
.order-pill.status-review   { background: var(--warn-bg, #fbf0dc); color: #8a5a0b; border-color: #e2c48a; }
.order-pill.status-paid     { background: #e2efe7; color: #17603c; border-color: #a9d3bd; }
.order-pill.status-shipping { background: #e4edf9; color: #1a4c8b; border-color: #a8c4e8; }
.order-pill.status-done     { background: #e2efe7; color: #17603c; border-color: #a9d3bd; }
.order-pill.status-cancelled{ background: #f8e5e3; color: #94241b; border-color: #e8b3ad; }

.order-timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.order-timeline li { padding-left: 16px; border-left: 2px solid var(--line); }
.order-timeline li:first-child { border-left-color: var(--accent); }

/* ---------- ประวัติการสั่งซื้อในบัญชีลูกค้า ---------- */
.account-orders { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.account-order {
  display: block;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px 18px;
  color: var(--ink);
  transition: box-shadow .15s, transform .15s;
}
.account-order:hover { box-shadow: var(--shadow); transform: translateY(-2px); }

/* ---------- แถบเตือนโหมดราคาทดสอบ ----------
   Deliberately loud and deliberately not dismissible: the whole point is that nobody forgets
   these prices are provisional. It removes itself when the seeded prices are removed. */
/* theme:fixed แถบเตือน ต้องเด่นเท่ากันทั้งสองโหมด */
.test-price-bar {
  background: #7a3b00;
  color: #ffe9cf;
  text-align: center;
  padding: 9px 16px;
  font-size: 13.5px;
  line-height: 1.5;
  border-bottom: 1px solid #5c2c00;
}
.test-price-bar strong { color: #ffffff; margin-right: 6px; }
/* theme:end */

/* ---------- ตารางข้อมูล (ใช้ร่วมกันทั้งสองฝั่ง) ----------
   Moved out of internal.css for the same reason as the form styles: the basket, the order-tracking
   page and the customer's order history all use them, and none of those load internal.css. */
table.data { width: 100%; border-collapse: collapse; font-size: 13.5px; background: var(--card); }
table.data th, table.data td {
  text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top;
}
table.data th {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); background: var(--surface-2);
}
table.data td.num { text-align: right; font-variant-numeric: tabular-nums; font-family: 'JetBrains Mono', monospace; }
/* A numeric header must sit above its right-aligned number, not drift to the left edge of a wide
   column -- otherwise the label and its value read as belonging to different columns. */
table.data th.num { text-align: right; }
table.data tr:last-child td { border-bottom: 0; }
table.data tfoot td { border-bottom: 0; padding-top: 6px; padding-bottom: 6px; }
.table-wrap { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; overflow-x: auto; }

/* ---------- แถบแจ้งผล ----------
   These were used on every shop page from the day they were written and were never defined
   ANYWHERE -- not here, not in internal.css. Every "บันทึกแล้ว" and every error message on the
   basket, the checkout and the account pages rendered as an unstyled line of text. */
.alert {
  padding: 12px 15px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  font-size: 14px;
  line-height: 1.6;
}
.alert.ok  { background: #e6f5ee; color: #0d6b45; border-color: #b7e0cc; }
.alert.bad { background: #fdecea; color: #a02020; border-color: #f3c2bd; }

/* ---------- ฟอร์ม (ใช้ร่วมกันทั้งหน้าบ้านและหลังบ้าน) ----------
   Lives in app.css, not internal.css, because the checkout, the customer account pages and the
   quotation-request form are public -- and the public side never loads internal.css. */
.fieldset { padding: 20px; margin-bottom: 18px; }
.fieldset h2 { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.field { margin-bottom: 14px; }
.field label { display: block; font-weight: 600; font-size: 13px; margin-bottom: 5px; }
.field .hint { font-size: 12px; color: var(--muted); font-weight: 400; margin-top: 3px; }
.field input, .field select, .field textarea {
  width: 100%; border: 1px solid var(--line); border-radius: 8px;
  padding: 9px 12px; font: inherit; font-size: 14px; background: var(--card); color: var(--ink);
}
.field textarea { resize: vertical; min-height: 84px; }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: 2px solid var(--accent); outline-offset: -1px;
}
/* A checkbox or radio inside .field must not stretch to the full width the rule above gives
   every input -- that is what turns "ต้องการใบกำกับภาษี" into a 700px-wide tick box. */
.field input[type="checkbox"], .field input[type="radio"] { width: auto; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 560px) { .field-row { grid-template-columns: 1fr; } }

/* ---------- หน้านโยบาย ---------- */
.policy-body { font-size: 15px; line-height: 1.8; color: var(--ink); }
.policy-body ul { margin: 0; padding-left: 22px; display: flex; flex-direction: column; gap: 6px; }
.policy-body p { margin: 0 0 10px; }
.policy-body p:last-child { margin-bottom: 0; }

.foot-legal {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  padding: 14px 0 0;
  font-size: 13px;
}
.foot-legal a { color: var(--on-dark-soft); }
.foot-legal a:hover { color: var(--on-dark); }

/* ---------- แถบแจ้งเรื่องคุกกี้ ---------- */
.cookie-note {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 16px;
  z-index: 60;
  max-width: 720px;
  margin: 0 auto;
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 10px 34px -18px rgba(0, 0, 0, .55);
  padding: 14px 18px;
  display: flex;
  gap: 16px;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  font-size: 13.5px;
  line-height: 1.6;
}
.cookie-note[hidden] { display: none; }
.cookie-note button {
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: #fff;
  font: inherit;
  font-weight: 600;
  padding: 8px 18px;
  cursor: pointer;
  white-space: nowrap;
}
.cookie-note button:hover { background: var(--accent-hover); }

/* ตะกร้าบนแถบเมนู */
.nav-cart {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  color: var(--ink);
  font-size: 13.5px;
  font-weight: 600;
  white-space: nowrap;
}
.nav-cart:hover { background: var(--hover); }
/* Once there is something in it, the basket stops being one more grey chip in a crowded header. */
.nav-cart.has-items { border-color: var(--accent); color: var(--accent); }

/* ---------- แถบยืนยัน "ใส่ตะกร้าแล้ว" ----------
   Sits under the header on the page the visitor was returned to, carrying the two links that
   otherwise have to be hunted for. Without this, adding to the basket looked like nothing
   happened at all. */
.cart-toast {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 11px 18px;
  background: var(--ok-bg, #e6f5ee);
  color: #0d6b45;
  border-bottom: 1px solid #b7e0cc;
  font-size: 14px;
  font-weight: 600;
}
.cart-toast.bad { background: #fdecea; color: #a02020; border-bottom-color: #f3c2bd; }
.cart-toast .btn {
  padding: 6px 15px;
  font-size: 13px;
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 8px;
  font-weight: 600;
}
.cart-toast .btn.btn-solid { background: var(--accent); color: #fff; border-color: var(--accent); }
.cart-toast .btn:hover { filter: brightness(.96); }
.nav-cart .count {
  background: var(--accent);
  color: #fff;
  border-radius: 999px;
  min-width: 19px;
  height: 19px;
  display: inline-grid;
  place-items: center;
  font-size: 11.5px;
  padding: 0 5px;
}

/* ---------- layout with sidebar ---------- */
.with-side { display: grid; grid-template-columns: 250px 1fr; gap: 24px; align-items: start; }
.sidebar { position: sticky; top: 90px; }
.sidebar section { padding: 16px; margin-bottom: 14px; }
.sidebar h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); }
.sidebar ul { list-style: none; margin: 0; padding: 0; max-height: 340px; overflow-y: auto; }
.sidebar li a {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding: 5px 8px;
  border-radius: 6px;
  font-size: 13px;
}
.sidebar li a:hover { background: var(--hover); }
.sidebar li a.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.sidebar li a span { color: var(--muted); font-size: 12px; }

.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}
.search-box { display: flex; gap: 8px; flex: 1; max-width: 460px; }
.search-box input {
  flex: 1;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 10px 14px;
  font: inherit;
  background: var(--card);
}
.search-box input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

.pagination { display: flex; gap: 6px; justify-content: center; margin-top: 28px; flex-wrap: wrap; }
.pagination a, .pagination span {
  padding: 7px 12px;
  border-radius: 7px;
  border: 1px solid var(--line);
  background: var(--card);
  font-size: 14px;
}
.pagination .current { background: var(--navy); color: var(--on-dark); border-color: var(--navy); }
.pagination .gap { border: 0; background: none; }
/* PAGENUM: the one pager (app/Pager.php) -- page count, numbers and a jump box, under every paged table */
.pagination { align-items: center; }
/* the page you are on: --navy sat on the dark theme's card nearly unseen, and a light box would be a white box with dark
   type on the dark theme (bin/test_dark_text rule 3). The brand's deep crimson does not flip and carries white at ~5.9:1
   on both themes -- a marker of where you are, not a button (the back office's buttons take their colour from what they do · BTNCOLOR). */
.pagination .current { background: var(--accent-hover); color: var(--on-dark); border-color: var(--accent-hover); font-weight: 700; }
.pagination .pg-info { border: 0; background: none; padding: 7px 4px; color: var(--muted); font-size: 13px; }
.pagination .pg-jump { display: inline-flex; align-items: center; gap: 6px; margin: 0 0 0 6px; }
.pagination .pg-jump label { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }
.pagination .pg-jump input {
  width: 74px; padding: 6px 8px; border: 1px solid var(--line); border-radius: 7px;
  background: var(--card); color: var(--ink); font: inherit; font-size: 14px;
}

/* ---------- product detail ---------- */
.crumbs { font-size: 13px; color: var(--muted); margin-bottom: 16px; }
.crumbs a:hover { color: var(--accent); }
.detail { display: grid; grid-template-columns: 380px 1fr; gap: 30px; }
.detail .gallery {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  height: 340px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.detail .gallery img { max-width: 100%; max-height: 100%; object-fit: contain; }
.detail .gallery .placeholder { color: var(--faint); font-size: 15px; text-align: center; }
.thumb-strip { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.thumb-btn {
  width: 62px; height: 62px; padding: 4px; cursor: pointer;
  background: var(--card); border: 1px solid var(--line); border-radius: 8px;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.thumb-btn img { max-width: 100%; max-height: 100%; object-fit: contain; }
.thumb-btn.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.detail h1 { font-size: 22px; line-height: 1.45; }
.spec-table { width: 100%; border-collapse: collapse; margin-top: 18px; }
.spec-table th, .spec-table td {
  text-align: left;
  padding: 11px 14px;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
}
.spec-table th { width: 170px; color: var(--muted); font-weight: 500; background: var(--surface-2); }
.spec-table td.mono { font-family: 'JetBrains Mono', monospace; }
.cta-row { display: flex; gap: 10px; margin-top: 22px; flex-wrap: wrap; align-items: center; }
/* `align-items` ตั้งต้นของ flex คือ `stretch` -- ปุ่มผีจึงเคยยืดเต็มความสูงของ `.buy-form`
   กลายเป็นกล่องสูงสองใบขนาบปุ่มเล็ก ๆ หนึ่งอัน ทั้งที่ทุกอันเป็นทางเลือกระดับเดียวกัน
   ตอนนี้ `.qty-field` ยุบเหลือบรรทัดเดียวแล้ว ทุกอันจึงสูงพอ ๆ กัน
   `center` ทำให้ส่วนต่าง 1-2px ที่เหลือ (เส้นขอบ · line-height) มองไม่ออก */

/* ปุ่มทึบประกาศ `border: 0` ส่วนปุ่มผีมีเส้นขอบ 1px -- ไม่ชดเชยแล้วสูงต่างกัน 2px
   ซึ่งเห็นได้ชัดเมื่อวางติดกัน · `:not()` ไว้กันไม่ให้ไปทับสีเส้นขอบของปุ่มผีเอง */
.cta-row .btn:not(.btn-ghost) { border: 1px solid transparent; }

/* แถวบนหน้าสินค้าเล็กกว่าแถว CTA ท้ายบทความโดยตั้งใจ -- ที่นี่ปุ่มอยู่ใต้ตารางสเปค
   ไม่ใช่ปิดท้ายหน้า จึงไม่ควรดังกว่าตัวราคาที่อยู่เหนือมัน */
.cta-buy .btn { padding: 8px 15px; font-size: 13.5px; }
.cta-buy .qty-field input { padding: 7px 10px; }
.tag-row { display: flex; gap: 7px; flex-wrap: wrap; }
.tag-link { transition: background .12s, color .12s; }
.tag-link:hover { background: var(--accent); color: var(--on-dark); }

.faq { margin-top: 24px; }
.faq h2 { font-size: 15px; margin: 0 0 10px; }
.faq details {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  margin-bottom: 7px;
}
.faq summary {
  padding: 11px 14px;
  cursor: pointer;
  font-weight: 600;
  font-size: 14px;
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::before { content: '+ '; color: var(--accent); font-weight: 700; }
.faq details[open] summary::before { content: '– '; }
.faq details p { margin: 0; padding: 0 14px 13px; color: var(--muted); font-size: 14px; }

.contact-card {
  margin-top: 24px;
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  padding: 16px 20px;
}
.contact-card h2 { font-size: 15px; margin: 0 0 12px; }
.contact-card ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 9px 20px;
}
.contact-card li { display: flex; align-items: center; gap: 10px; font-size: 14px; }
.contact-card .ico {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 6px;
  background: var(--accent-soft);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
}
.contact-card a { color: var(--ink); border-bottom: 1px solid var(--line); }
.contact-card a:hover { color: var(--accent); border-color: var(--accent); }
.price-big { font-size: 26px; font-weight: 700; color: var(--accent); margin-top: 14px; }
.price-big.ask { font-size: 17px; color: var(--muted); }

/* ---------- brands ---------- */
.brand-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.brand-tile {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  transition: border-color .15s, box-shadow .15s, transform .15s;
}
.brand-tile:hover { border-color: var(--accent); box-shadow: var(--shadow); transform: translateY(-2px); }

/* Logos arrive at wildly different aspect ratios and mostly assume a white ground, so each
   one gets a fixed box and is contained rather than cropped. */
.brand-logo {
  height: 58px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
}
.brand-logo img { max-width: 100%; max-height: 100%; object-fit: contain; }
.brand-wordmark {
  font-weight: 700;
  font-size: 15px;
  letter-spacing: .02em;
  color: var(--faint);
  text-align: center;
  line-height: 1.25;
}
.brand-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  border-top: 1px solid var(--line);
  padding-top: 9px;
}
.brand-foot strong { font-size: 13px; }

/* ---------- chat ---------- */
/* The assistant is a working surface, not a page to read -- it gets the full window, and the
   reference rail folds away when the conversation itself needs the room. */
.wrap-wide { max-width: 1560px; }

.chat-shell { display: grid; grid-template-columns: 1fr 360px; gap: 20px; align-items: start; }
.chat-shell.side-hidden { grid-template-columns: 1fr; }
.chat-shell.side-hidden .chat-side { display: none; }
.chat-panel {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  height: calc(100vh - 150px);
  min-height: 520px;
}
.side-toggle { padding: 6px 12px; font-size: 13px; white-space: nowrap; }
.chat-head {
  padding: 14px 18px;
  border-bottom: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.chat-head strong { font-size: 15px; }
.chat-head .status { font-size: 12px; color: var(--muted); }
.chat-head .dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); margin-right: 5px; }
.chat-head .dot.off { background: var(--faint); }

.chat-log { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.msg { display: flex; gap: 10px; max-width: 88%; }
.msg.user { align-self: flex-end; flex-direction: row-reverse; }
.msg .avatar {
  width: 30px; height: 30px; border-radius: 8px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: var(--on-dark); background: var(--steel);
}
.msg.assistant .avatar { background: var(--accent); }
.msg .bubble {
  background: var(--surface);
  border-radius: 12px;
  padding: 11px 15px;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: 14.5px;
}
.msg.user .bubble { background: var(--navy); color: var(--on-dark); }
.msg .bubble strong { font-weight: 700; }
.msg .bubble.error { background: var(--danger-bg); color: var(--danger); }

/* Scoped to .dots deliberately: as ".typing span" this also matched the status label beside
   the dots and squeezed it into a 6px box, which printed "กำลังอ่านรูป…" one letter per line. */
.typing .dots span {
  display: inline-block; width: 6px; height: 6px; margin-right: 3px;
  background: var(--steel); border-radius: 50%; animation: blink 1.2s infinite;
}
.typing .dots span:nth-child(2) { animation-delay: .2s; }
.typing .dots span:nth-child(3) { animation-delay: .4s; }
@keyframes blink { 0%, 60%, 100% { opacity: .25; } 30% { opacity: 1; } }

.sources { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; }
.source-card { display: flex; align-items: stretch; gap: 6px; }
.source-card .source-item { flex: 1 1 auto; min-width: 0; }

/* The found products, in the rail. Each card carries a tick; the single quotation button lives
   at the top of the card, so one trip can seed a quotation with every part that was ticked. */
.found-card { display: flex; flex-direction: column; }
.found-count {
  display: inline-block;
  margin-left: 4px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-dark);
  font-size: 11px;
  font-weight: 700;
}
.found-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: calc(100vh - 470px);
  min-height: 120px;
  overflow-y: auto;
}
.found-list .source-card { flex-direction: row; align-items: center; gap: 8px; }
.found-list .source-item { flex: 1 1 auto; min-width: 0; }
.found-empty { font-size: 12.5px; margin: 0; line-height: 1.6; }

/* The tick sits in its own padded label so the hit area is a finger, not a 13px box. */
.s-pick {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  padding: 4px;
  cursor: pointer;
}
.s-pick-box {
  width: 17px;
  height: 17px;
  accent-color: var(--accent);
  cursor: pointer;
  margin: 0;
}

/* Full width under the heading: it acts on the whole list, so it should not look like it belongs
   to any one row. Disabled until something is ticked, rather than hidden -- a button that is not
   there cannot teach anyone that ticking is what turns it on. */
.found-quote {
  width: 100%;
  justify-content: center;
  margin: 0 0 10px;
  font-size: 12.5px;
  white-space: nowrap;   /* Thai breaks between any two characters when squeezed */
}
.found-quote:disabled {
  opacity: .5;
  cursor: not-allowed;
}
.source-item {
  display: flex; gap: 10px; align-items: center;
  border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; background: var(--card);
}
.source-item:hover { border-color: var(--accent); }
.source-item .s-img {
  width: 34px; height: 34px; border-radius: 6px; background: var(--surface); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  font-size: 10px; color: var(--faint); font-weight: 700;
}
.source-item .s-img img { max-width: 100%; max-height: 100%; object-fit: contain; }
.source-item .s-body { min-width: 0; }
.source-item .s-title {
  font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.source-item .s-sku { font-size: 11px; color: var(--muted); font-family: 'JetBrains Mono', monospace; }

/* A candidate the salesperson did NOT type -- flagged on the card because it carries no price, no
   stock and no quotation tick, and nothing else on screen would say why. Warn tokens, so the badge
   flips with the theme; nothing hard-coded. */
.found-list .source-card.near-code {
  border-left: 3px solid var(--warn-line); background: var(--warn-bg); border-radius: 8px;
  padding-left: 6px;
}
.source-item .s-near {
  font-size: 10.5px; color: var(--warn); margin-top: 2px; line-height: 1.35;
  white-space: normal;
}

.chat-input { border-top: 1px solid var(--line); padding: 12px; display: flex; gap: 10px; }
.chat-input textarea {
  flex: 1; border: 1px solid var(--line); border-radius: 9px; padding: 11px 14px;
  font: inherit; resize: none; max-height: 130px; min-height: 44px;
}
.chat-input textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.chat-input button { align-self: flex-end; height: 44px; }
.chat-input button:disabled { opacity: .5; cursor: not-allowed; }

.chat-side section { padding: 16px; margin-bottom: 14px; }
.chat-side h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); }
.suggest { display: flex; flex-direction: column; gap: 7px; }
.suggest button {
  text-align: left; background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 8px; padding: 9px 12px; font: inherit; font-size: 13px; cursor: pointer;
}
.suggest button:hover { border-color: var(--accent); background: var(--accent-soft); }

.notice {
  background: var(--warn-bg); border: 1px solid var(--warn-line); color: var(--warn);
  border-radius: 8px; padding: 12px 14px; font-size: 13.5px; margin-bottom: 16px;
}
/* a link in a notice -- but never a button there: this rule outranked .btn and painted the white text of the red download
   buttons dark yellow and underlined, unreadable (user screenshot 1 ต.ค. 2569 · BTNSIZE) */
.notice a:not(.btn) { color: var(--warn); text-decoration: underline; font-weight: 600; }

/* ---------- footer ---------- */
.site-footer {
  background: var(--navy);
  color: var(--on-dark-muted);
  font-size: 13.5px;
  margin-top: 64px;
  padding: 46px 0 0;
}

/* Three blocks side by side: who we are, what we sell, how to reach us. */
.foot-grid {
  display: grid;
  grid-template-columns: 1.15fr 1.35fr 1fr;
  gap: 44px;
  padding-bottom: 34px;
}
.foot-block h3 {
  color: var(--on-dark);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: .06em;
  margin: 0 0 16px;
}

.foot-brand { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.foot-brand img {
  width: 92px;
  height: auto;
  aspect-ratio: 396 / 192;
  object-fit: contain;
  background: var(--on-dark);
  border-radius: 7px;
  padding: 5px 7px;
  flex: none;
}
.foot-brand strong { color: var(--on-dark); font-size: 15px; line-height: 1.35; }
.foot-tagline { margin: 0 0 10px; }
.foot-tax { margin: 12px 0 0; color: var(--on-dark-faint); font-size: 12.5px; }

/* All three sites, in the first footer block, where a single address used to sit.
   Every colour here is an on-dark token: this block always sits on the navy footer, in both
   themes, so a surface token like --card would be white text in light mode and dark-on-dark
   in the other. See CLAUDE.md, the three kinds of colour. */
.foot-branches { list-style: none; margin: 0 0 10px; padding: 0; max-width: 38ch; }
.foot-branches li { margin-bottom: 11px; line-height: 1.55; }
.fb-name { display: block; color: var(--on-dark); font-weight: 600; font-size: 13.5px; }
.fb-name em {
  font-style: normal;
  font-size: 11px;
  font-weight: 600;
  color: var(--accent);
  border: 1px solid var(--on-dark-line);
  border-radius: 4px;
  padding: 1px 6px;
  margin-left: 6px;
  white-space: nowrap;
}
.fb-addr { display: block; font-size: 12.5px; color: var(--on-dark-soft); }
a.fb-tel { font-size: 12.5px; font-variant-numeric: tabular-nums; }
a.fb-tel:hover { color: var(--accent); }
.foot-more { display: inline-block; font-size: 12.5px; font-weight: 600; color: var(--accent); }
.foot-more:hover { color: var(--on-dark); }

/* Departmental mailboxes in the contact block. Indented to the same 36px the .fc-ico rows start
   at, so the column reads as one list and not two. */
.fc-dept { margin: 0 0 12px 36px; line-height: 1.5; }
.fc-dept-name { display: block; color: var(--on-dark); font-weight: 600; font-size: 13px; }
.fc-dept-line { display: block; font-size: 12.5px; }
.fc-dept-line a { color: var(--on-dark-soft); }
.fc-dept-line a:hover { color: var(--accent); }
.fc-dept-tel { margin-left: 8px; font-variant-numeric: tabular-nums; color: var(--on-dark-faint); }

/* Two link columns inside the middle block: product types, then brands. */
.foot-links { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 20px; }
.foot-links ul { list-style: none; margin: 0; padding: 0; }
.foot-links li { margin-bottom: 8px; }
.foot-links a:hover { color: var(--accent); }
.foot-links strong { color: var(--accent); font-weight: 600; }

.fc-row { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 13px; line-height: 1.5; }
.fc-row small { color: var(--on-dark-faint); font-size: 12px; }
.fc-ico {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 6px;
  background: var(--on-dark-hover);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
}
a.fc-row:hover { color: var(--on-dark); }
a.fc-row:hover .fc-ico { background: var(--accent); color: var(--on-dark); }

.foot-bottom {
  border-top: 1px solid var(--on-dark-line);
  padding: 18px 0 24px;
  display: flex;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  font-size: 12.5px;
  color: var(--on-dark-faint);
}

/* ---------- responsive ---------- */
@media (max-width: 980px) {
  .with-side, .chat-shell, .detail { grid-template-columns: 1fr; }
  .sidebar { position: static; }
  .chat-side { order: -1; }
  .chat-panel { height: 70vh; }
}
@media (max-width: 980px) {
  .foot-grid { grid-template-columns: 1fr 1fr; gap: 30px; }
  .foot-block:first-child { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  /* At a 180-230px minimum plus the gap, two tiles do not fit a 350px viewport and these
     grids collapse to a single column -- a phone-length wall of half-empty cards. */
  .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .brand-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .product-card .thumb { height: 110px; }
  .product-card .title { font-size: 13px; }
  .foot-grid { grid-template-columns: 1fr; gap: 26px; }
  .foot-links { grid-template-columns: 1fr 1fr; }
  .header-inner { gap: 8px; min-height: 56px; }
  /* 390px cannot hold the logo, the company name, three nav links and a phone number.
     The name goes: the logo already identifies the company, and the phone number is the
     thing a customer on a phone is most likely to want. */
  .brand-text { display: none; }
  .site-logo { height: 44px; width: auto; }
  /* A wrapped phone number reads as a broken layout, not a phone number. */
  .main-nav .nav-cta { white-space: nowrap; }
  .footer-contact { text-align: left; }
  .msg { max-width: 96%; }
}

/* ---------- articles (public) ---------- */
.article-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 18px;
}
.article-card {
  display: flex; flex-direction: column; gap: 10px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 22px; box-shadow: var(--shadow);
}
.article-card:hover { border-color: var(--accent); }
.article-card h2 { font-size: 17px; margin: 0; line-height: 1.45; text-wrap: balance; }
.article-card .article-answer { font-size: 13.5px; color: var(--muted); margin: 0; flex: 1; }
.article-card .article-meta {
  display: flex; justify-content: space-between; align-items: center;
  font-size: 12.5px; color: var(--muted);
}
.article-card .article-meta a { color: var(--accent); font-weight: 600; }

.article-page { max-width: 780px; margin: 0 auto; }
.article-head h1 { font-size: 32px; line-height: 1.35; text-wrap: balance; margin: 0 0 8px; }
.article-head .article-meta { color: var(--muted); font-size: 13.5px; margin: 0; }

/* The direct answer, set apart. It is the passage an assistant lifts, so it must read as a
   complete statement on its own -- not as the first paragraph of something longer. */
.article-answer-box {
  margin: 26px 0;
  padding: 20px 22px;
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 0 10px 10px 0;
  font-size: 16px;
  line-height: 1.75;
}
.article-body { font-size: 16px; line-height: 1.85; }
.article-body h2 { font-size: 21px; margin: 34px 0 12px; text-wrap: balance; }
.article-body p { margin: 0 0 16px; }
.article-body ul { margin: 0 0 18px; padding-left: 22px; }
.article-body li { margin-bottom: 7px; }

.article-products { margin: 42px 0; }
.article-products h2 { font-size: 21px; margin: 0 0 16px; }
.article-faq { margin: 42px 0; }
.article-faq h2 { font-size: 21px; margin: 0 0 14px; }
.article-faq details {
  border: 1px solid var(--line); border-radius: 10px; padding: 14px 18px;
  margin-bottom: 10px; background: var(--card);
}
.article-faq summary { font-weight: 600; cursor: pointer; }
.article-faq p { margin: 12px 0 0; color: var(--muted); line-height: 1.75; }

.article-cta {
  margin: 42px 0 0; padding: 28px; border-radius: var(--radius);
  background: var(--navy); color: var(--on-dark); text-align: center;
}
.article-cta h2 { margin: 0 0 8px; font-size: 22px; }
.article-cta p { margin: 0 0 18px; color: var(--on-dark-soft); }
.article-cta .cta-row { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.article-cta .btn-ghost { color: var(--on-dark); border-color: var(--on-dark-border); }

/* ---------- RFQ (public) ---------- */
.rfq-form { max-width: 760px; padding: 28px; position: relative; }
.rfq-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
.rfq-form .field { margin-bottom: 16px; }
.rfq-form label { display: block; font-weight: 600; font-size: 13.5px; margin-bottom: 5px; }
.rfq-form input[type=text], .rfq-form input[type=tel], .rfq-form input[type=email],
.rfq-form textarea {
  width: 100%; border: 1px solid var(--line); border-radius: 8px;
  padding: 11px 13px; font: inherit; background: var(--card);
}
.rfq-form textarea { resize: vertical; }
.rfq-product {
  background: var(--accent-soft); border-radius: 8px; padding: 12px 15px;
  margin-bottom: 20px; font-size: 14px;
}
.rfq-done { max-width: 640px; margin: 0 auto; padding: 44px 32px; text-align: center; }
.rfq-tick {
  width: 60px; height: 60px; border-radius: 50%; margin: 0 auto 18px;
  background: var(--ok-bg); color: var(--ok); font-size: 30px;
  display: flex; align-items: center; justify-content: center;
}
.rfq-ref { font-size: 15px; margin: 0 0 18px; }
.rfq-read {
  background: var(--info-bg); border: 1px solid var(--info-line); color: var(--info);
  border-radius: 10px; padding: 14px 16px; margin: 0 0 18px;
  font-size: 14px; line-height: 1.7; text-align: left;
}

/* ---------- product documents ---------- */
.doc-list { margin: 26px 0; }
.doc-list h2 { font-size: 18px; margin: 0 0 12px; }
.doc-item {
  display: flex; align-items: center; gap: 14px;
  border: 1px solid var(--line); border-radius: 10px;
  padding: 13px 16px; margin-bottom: 8px; background: var(--card);
}
.doc-item:hover { border-color: var(--accent); }
.doc-icon {
  flex: 0 0 auto; width: 44px; height: 44px; border-radius: 8px;
  background: var(--danger-bg); color: var(--danger);
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700;
}
.doc-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.doc-body strong { font-size: 14.5px; }
.doc-body .muted { font-size: 12.5px; }
.doc-dl { flex: 0 0 auto; color: var(--accent); font-weight: 600; font-size: 13px; white-space: nowrap; }

/* ---------- RFQ: two-column layout with service panel ---------- */
/* The right column is narrowed and given a right margin so the sticky panel clears the fixed
   contact rail (which floats at right:16px, vertically centred). Without the gap the rail sat on
   top of the first card on ~1280px screens where the page gutter is thin. */
.rfq-layout { display: grid; grid-template-columns: minmax(0,1fr) 300px; gap: 24px; align-items: start; }
.rfq-layout .rfq-form { max-width: none; }
@media (max-width: 980px) { .rfq-layout { grid-template-columns: 1fr; } }

.rfq-services {
  position: sticky; top: 90px;
  background: var(--navy); color: var(--on-dark);
  border-radius: var(--radius); padding: 22px 20px;
  display: flex; flex-direction: column; gap: 8px;
}
@media (min-width: 981px) and (max-width: 1360px) {
  /* pull the whole panel left of the contact rail on tight screens */
  .rfq-services { margin-right: 52px; }
}
.rfq-svc-head { margin-bottom: 8px; }
.rfq-svc-eyebrow {
  display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--accent-bright); margin-bottom: 7px;
}
.rfq-svc-head h2 { font-size: 19px; margin: 0 0 5px; line-height: 1.3; color: var(--on-dark); }
.rfq-svc-head p { font-size: 12.5px; color: var(--on-dark-soft); margin: 0; line-height: 1.6; }

.rfq-svc {
  display: flex; align-items: center; gap: 13px;
  padding: 12px; border-radius: 10px;
  transition: background .15s;
}
.rfq-svc:hover { background: var(--on-dark-hover); }
.rfq-svc-ico {
  flex: 0 0 auto; width: 42px; height: 42px; border-radius: 10px;
  background: var(--accent-bright-bg); color: var(--accent-bright);
  display: flex; align-items: center; justify-content: center;
}
.rfq-svc-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.rfq-svc-text strong { font-size: 14px; color: var(--on-dark); }
.rfq-svc-text span { font-size: 12px; color: var(--on-dark-soft); line-height: 1.5; }

.rfq-svc-all {
  display: block; text-align: center; margin-top: 8px; padding: 11px;
  border-radius: 9px; border: 1px solid var(--on-dark-border);
  color: var(--on-dark); font-weight: 600; font-size: 13px;
}
.rfq-svc-all:hover { background: var(--on-dark-hover); }

/* attached-photo thumbnails */
.rfq-thumbs { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.rfq-thumb { position: relative; width: 84px; height: 84px; border-radius: 8px; overflow: hidden; border: 1px solid var(--line); }
.rfq-thumb img { width: 100%; height: 100%; object-fit: cover; }
.rfq-thumb-del {
  position: absolute; top: 3px; right: 3px; width: 22px; height: 22px; border: 0;
  border-radius: 50%; background: rgba(0,0,0,.6); color: var(--on-dark); cursor: pointer;
  font-size: 12px; line-height: 1; display: flex; align-items: center; justify-content: center;
}
.rfq-thumb-del:hover { background: var(--danger-solid); }
.drop.is-full { opacity: .55; pointer-events: none; }

/* ---------------------------------------------------------------- waiting cover
   One cover for every screen that posts real work. It sits above everything and swallows
   clicks, which is the point: the second click on an import button is what starts a second
   import on top of the first. Driven by assets/js/loading.js. */
.mps-loading {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: var(--overlay);
  backdrop-filter: blur(2px);
}
.mps-loading.is-open { display: flex; }
.mps-loading-box {
  background: var(--card);
  border-radius: 14px;
  padding: 26px 30px;
  max-width: 520px;
  width: 100%;
  text-align: center;
  box-shadow: 0 18px 50px rgba(0, 0, 0, .3);
}
.mps-spinner {
  width: 38px; height: 38px;
  margin: 0 auto 14px;
  border: 3px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: mps-spin .8s linear infinite;
}
@keyframes mps-spin { to { transform: rotate(360deg); } }
.mps-loading-title { font-size: 15px; font-weight: 700; color: var(--ink); }
.mps-loading-stage { font-size: 13px; color: var(--muted); margin-top: 5px; min-height: 18px; }
.mps-progress {
  height: 18px;
  margin-top: 12px;
  border-radius: 999px;
  background: var(--bg);
  overflow: hidden;
}
.mps-progress-bar {
  height: 100%;
  width: 0;
  background: var(--accent);
  color: var(--on-dark);
  font-size: 11px;
  line-height: 18px;
  font-weight: 700;
  transition: width .25s ease;
}
/* A log that grows without bound would push the note off screen, so it scrolls in place. */
.mps-loading-log {
  margin: 12px 0 0;
  padding: 9px 11px;
  max-height: 170px;
  overflow: auto;
  text-align: left;
  background: var(--bg);
  border-radius: 8px;
  font-size: 11.5px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
}
.mps-loading-note { font-size: 11.5px; color: var(--muted); margin-top: 12px; }

/* LOADALL (back office): a page is on its way -- a thin bar across the top at once; the cover above follows only if the
   next page takes more than half a second. Brand colour, the same in both themes. Driven by assets/js/loading.js. */
.mps-navbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 3px;
  z-index: 10000;
  pointer-events: none;
  opacity: 0;
  transition: opacity .15s;
  background: linear-gradient(90deg, transparent 0, var(--accent) 30%, var(--accent) 70%, transparent 100%) no-repeat;
  background-size: 50% 100%;
}
.mps-navbar.is-on { opacity: 1; animation: mps-navbar 1.1s ease-in-out infinite; }
@keyframes mps-navbar { from { background-position: -50% 0; } to { background-position: 150% 0; } }
@media (prefers-reduced-motion: reduce) { .mps-navbar.is-on { animation: none; background: var(--accent); } }

/* ---------- print ----------
   Paper has one theme. A browser drops background colours when printing but keeps text colours, so
   a quotation printed from a dark screen comes out as pale grey type on white -- readable on the
   monitor it was composed on and not on the page it is handed to a customer on. Restating the light
   tokens under @media print costs one rule and removes the whole class of problem.

   !important is deliberate and is the narrowest tool here: the dark values live on :root[data-theme],
   which outranks a plain :root by one attribute selector. */
@media print {
  :root, :root[data-theme="dark"] {
    color-scheme: light !important;
    --bg: #ffffff !important;
    --card: #ffffff !important;
    --surface: #ffffff !important;
    --surface-2: #ffffff !important;
    --hover: #ffffff !important;
    --line: #cccccc !important;
    --line-soft: #e5e5e5 !important;
    --ink: #000000 !important;
    --muted: #444444 !important;
    --faint: #777777 !important;
    --ok: #0d6b45 !important;
    --ok-bg: #ffffff !important;
    --danger: #8a1a1a !important;
    --danger-bg: #ffffff !important;
    --warn: #6b5000 !important;
    --warn-bg: #ffffff !important;
    --info: #23456d !important;
    --info-bg: #ffffff !important;
    --cost: #7a5a08 !important;
    --shadow: none !important;
  }
  body { background: var(--bg) !important; }
  .theme-toggle, .nav-toggle, .skip-link { display: none !important; }
}

/* บรรทัดใบเสนอราคาที่ลูกค้าไม่รับ — เสนอ 5 ลูกค้าเอา 3 เป็นเรื่องปกติ
   ใช้ opacity ไม่ใช่สีเฉพาะ เพราะมันถูกทั้งโหมดสว่างและโหมดมืดโดยไม่ต้องมีสองชุด
   (กับดักที่ CLAUDE.md เตือน: ใช้ --card เป็นสีตัวอักษรแล้วโหมดมืดตัวหนังสือหายไปเลย) */
#items-table .acc-col { text-align: center; }
#items-table tr.line-rejected { opacity: .5; }
#items-table tr.line-rejected .i-sku,
#items-table tr.line-rejected .i-desc { text-decoration: line-through; }

/* QTIMG: รูปสินค้าในหน้ากรอกใบเสนอราคา */
#items-table .i-img { text-align: center; vertical-align: middle; }
/* theme:fixed รูปสินค้าถ่ายบนพื้นขาว พื้นหลังรูปจึงต้องขาวทั้งสองโหมด ไม่งั้นขอบรูปเป็นกรอบขาวลอยบนพื้นมืด */
#items-table .i-img img { display: block; margin: 0 auto; width: 44px; height: 44px;
                          object-fit: contain; background: #ffffff; border-radius: 4px; }
/* theme:end */
