/* Kanban board — mobile first. Field crews hold this in one hand, in daylight,
   with gloves on: big targets, high contrast, no hover-only affordances.

   The palette, the type and the shapes are the HR portal's (Sneat), so the two
   halves of the system look like one product: same purple, same Public Sans /
   Sarabun pairing, same card radius and shadow. What is *not* borrowed is
   Sneat's layout — a Bootstrap admin shell with a fixed side menu is the wrong
   answer to a board dragged with a thumb — so this file keeps the mobile
   structure and wears the HR skin over it.

   Light and dark follow the same switch as the portal: `data-bs-theme` on
   <html>, remembered in localStorage under `adminTheme`. On the production
   domain both apps are one origin, so the choice carries across. */

:root {
  color-scheme: light;

  --bg: #f5f5f9;
  --surface: #ffffff;
  --surface-2: #f2f3f3;      /* inset areas: board columns, quiet buttons */
  --line: #e4e6e8;
  --grid: #e2e6ea;           /* the Gantt's day columns, on the page ground */

  --ink: #384551;            /* headings and anything that must be read first */
  --ink-2: #646e78;          /* body copy */
  --ink-soft: #7a838b;       /* captions, units, disabled */

  /* Each accent comes as three: the solid colour, a tinted background and the
     text colour that is legible on that tint. Sneat's own bg-label-* trio —
     the flat bright colours are for bars and dots, never for small text. */
  --brand: #696cff;
  --brand-ink: #ffffff;
  --brand-soft: #e7e7ff;
  --brand-text: #2a2b66;
  --danger: #ff3e1d;
  --danger-soft: #ffe0db;
  --danger-text: #66190c;
  --warn: #ffab00;
  --warn-soft: #fff2d6;
  --warn-text: #664400;
  --ok: #71dd37;
  --ok-soft: #e8fadf;
  --ok-text: #2d5816;
  --info: #03c3ec;
  --muted-soft: #ebeef0;
  --muted-text: #353a41;

  --radius: 6px;             /* Sneat's 0.375rem */
  --radius-lg: 10px;
  --shadow: 0 .1875rem .5rem 0 rgba(34, 48, 62, .1);
  --shadow-lg: 0 .25rem .75rem 0 rgba(34, 48, 62, .14);
  --scrim: rgba(34, 48, 62, .5);

  /* A shade under the page, so a column reads as a tray and the white cards
     in it read as cards. Sneat has no board, so this one is ours. */
  --board-col: #eaebef;

  --col-w: 84vw;
  --tap: 44px;
}

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

  --bg: #2f3a45;
  --surface: #384551;
  --surface-2: #434f5b;
  --line: #4e5965;
  --grid: #46515d;

  --ink: #e9eaec;
  --ink-2: #bdc1c5;
  --ink-soft: #a7acb2;

  --brand-soft: #3f4199;
  --brand-text: #d2d3ff;
  --danger-soft: #661f10;
  --danger-text: #ffc5bb;
  --warn-soft: #4d3300;
  --warn-text: #ffe6b3;
  --ok-soft: #2c5615;
  --ok-text: #d4f5c3;
  --muted-soft: #4e5965;
  --muted-text: #e4e6e8;

  --board-col: #2b3540;

  --shadow: 0 .1875rem .5rem 0 rgba(0, 0, 0, .3);
  --shadow-lg: 0 .25rem .75rem 0 rgba(0, 0, 0, .4);
  --scrim: rgba(0, 0, 0, .6);
}

@media (min-width: 700px) {
  :root { --col-w: 300px; }
}

/* On a wide screen several columns are already on screen, so stopping at every
   one turns a flick of the trackpad into a fight. It still snaps — it just
   lets you travel. */
@media (min-width: 700px) {
  .col { scroll-snap-stop: normal; }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink-2);
  /* The HR portal's pairing: Public Sans for Latin, Sarabun for Thai. Thai
     glyphs sit taller, so the line-height is loosened the same way it is
     there. */
  font-family: "Public Sans", "Sarabun", "Noto Sans Thai", -apple-system,
    BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  font-size: 15px;
  line-height: 1.6;
  -webkit-text-size-adjust: 100%;
  overscroll-behavior: none;
}

button, input, select, textarea { font: inherit; color: inherit; }

/* ------------------------------------------------------------------ icons */
/* Boxicons, from the HR portal's set — see icons.css, which holds the glyphs.
   These rules are only about how one sits next to text.

   A .bx is a mask over `currentColor` sized in `em`, so it takes the colour
   and the size of whatever it is inside for free: a stage-tinted heading tints
   its own icon, a disabled button greys its own icon, and none of it needs a
   second rule. That is the whole reason these are not emoji.

   Thai ascenders push the optical centre of a line up, so a glyph baseline-
   aligned with Thai text reads low. Everything that pairs an icon with a word
   is a flex row with `align-items: center` instead. */

.bx { flex: 0 0 auto; }

/* A lone icon in a round/square tap target — the toolbar, the mini buttons,
   the column's ＋ — sits a size up from body text, because there is no word
   beside it doing the reading. */
.icon-btn > .bx,
.mini > .bx,
.col-add > .bx,
.crew-x > .bx,
.pick-go > .bx { font-size: 1.25em; }

.icon-btn, .mini, .btn, .tabs button, .day-item, .move-item, .chip, .flag {
  display: inline-flex;
  align-items: center;
}

/* Buttons that carry an icon *and* a word: one gap, set here, so every button
   on the page spaces them the same. */
.btn, .tabs button { gap: 8px; }
.chip, .flag { gap: 5px; }

.tabs button { justify-content: center; }

/* A label above a field can carry an icon too (`ผู้ติดต่อ`, `แม่แบบขั้นตอน`).
   `.field > span` is a block by default; this makes those a row. */
.field > span:has(> .bx) { display: flex; align-items: center; gap: 6px; }

/* Section headings: <h2>/<h3> with their own glyph. A page of stacked cards
   reads as one grey wall otherwise — the icon is what the eye lands on when
   scrolling back to a section it has already read. */
.sec-head {
  display: flex;
  align-items: center;
  gap: 8px;
}

.sec-head > .bx { font-size: 1.15em; opacity: .75; }

button {
  cursor: pointer;
  border: 0;
  background: none;
}

.hidden { display: none !important; }

/* ------------------------------------------------------------------ shell */
/* Two bars, not one. The old header put the company, the project, four icon
   buttons and nothing else on a single row, which on a phone left every
   control unlabelled and the person using it anonymous.
     row 1 — who and where: the app, the company, the signed-in user and level
     row 2 — what to do here: the project and the actions on it
   Splitting them means each control can carry a word next to its glyph. */

#app { display: flex; flex-direction: column; height: 100%; }

.appbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 21;
  flex: 0 0 auto;
}

.appbar .logo {
  width: 34px;
  height: 34px;
  border-radius: var(--radius);
  background: var(--brand);
  color: var(--brand-ink);
  display: grid;
  place-items: center;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .3px;
  flex: 0 0 auto;
}

.appbar .titles {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.25;
}

/* The app's name is a button now, and has to keep looking like a title: no
   padding, left-aligned, and free to shrink so the ellipsis below still
   belongs to the company name rather than to this. */
.app-name {
  display: block;
  padding: 0;
  text-align: left;
  min-width: 0;
  max-width: 100%;
}

.home-btn:active { opacity: .7; }

.appbar .titles strong {
  font-size: 15px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The company reads as a caption under the app name, and is the way back to
   the step that chose it — so it is a button, with a caret to say so. */
.org-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 0;
  font-size: 12px;
  color: var(--ink-soft);
  max-width: 100%;
  min-width: 0;
  text-align: left;
}

/* By id, not by position: both these bars gained a leading icon, and a
   `:first-child` rule written when the name *was* the first child silently
   stopped matching — the name lost its ellipsis, and in the project bar it
   also lost the flex-grow that keeps it left against `space-between`. */
/* Shrink-to-fit, so the ⌄ stays against the company name instead of being
   pushed to the far end of the header. The project bar below wants the
   opposite — see #project-name. */
#org-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* How many jobs are in the company, on the screen that is picking one. It is
   its own element rather than part of `#org-name` because that name is the
   thing allowed to ellipsize: on a 390px phone "KeyMax Industry Co.,Ltd." eats
   the caption whole, and a count folded into it would be the half that
   disappeared — which is the half this line was added for. */
.org-count { flex: 0 0 auto; white-space: nowrap; }

/* Below 480px the caption cannot hold both. A company name cut to "Key…" to
   make room for a number is the wrong half to keep — the list under it is the
   jobs, so their count is the thing already on screen, while which company
   this is has no other answer on the page. It comes back the moment there is
   width for it, which is every screen the count was asked for. */
@media (max-width: 480px) {
  .org-count { display: none; }
}

.org-btn .caret { flex: 0 0 auto; font-size: 9px; opacity: .8; }
.org-btn:active { color: var(--brand); }

/* Who is signed in, and at what level — the same pair the HR portal shows in
   its navbar, for the same reason: what is on the screen depends on the level,
   so a missing button is only explicable if the level is visible. */
.who {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px 4px 4px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface);
  flex: 0 0 auto;
  max-width: 46vw;
}

.who:active { background: var(--surface-2); }

/* The ☰ at the end of the project toolbar. Pinned to the right, the way the
   chip it replaces was — a control that moves corner to corner between two
   screens is a control people hunt for. */
.projline .menu-btn { flex: 0 0 auto; margin-left: auto; }

.who .avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--brand-soft);
  color: var(--brand-text);
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: 13px;
  flex: 0 0 auto;
  overflow: hidden;
}

.who .avatar img { width: 100%; height: 100%; object-fit: cover; }

/* The name and the level are the point of the chip, so they stay on screen
   down to a 320 px phone; it is the app title beside them that gives way. */
.who .who-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.2;
  min-width: 0;
}

.who .who-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  max-width: 26vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.role-badge {
  font-size: 10px;
  font-weight: 600;
  border-radius: 999px;
  padding: 1px 7px;
  background: var(--muted-soft);
  color: var(--muted-text);
  white-space: nowrap;
}

.role-badge.owner { background: var(--brand-soft); color: var(--brand-text); }
.role-badge.manager { background: var(--brand-soft); color: var(--brand-text); }
.role-badge.field_worker { background: var(--ok-soft); color: var(--ok-text); }
.role-badge.viewer { background: var(--muted-soft); color: var(--muted-text); }

/* The same fact as a .role-badge, for the one person who can change it. A
   picker rather than a dialog: an owner setting up the company goes down the
   list changing several in a row, and a confirm step on each is four taps for
   a decision that is one. */
.role-pick {
  flex: 0 0 auto;
  min-height: 38px;
  max-width: 45vw;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 6px 8px;
  font-size: 13px;
}

/* Wraps instead of scrolling: on a phone the project picker takes the first
   line and the actions the second, which is what lets every one of them keep
   the word beside its glyph. On anything wider it is one row again. */
.workbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}

/* Its own line on a phone — the actions then wrap as one group under it,
   instead of one action riding up beside the name.

   Inside a project this is the *only* bar: the header above it is hidden, so
   this one row carries the way back (‹), where you are (the job), and the way
   into everything else (☰) — the same left / middle / right shape the ผู้ดูแล
   bar uses, so the two screens read as one app. */
.projline {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 100%;
  min-width: 0;
}

@media (min-width: 700px) {
  .projline { flex: 1 1 auto; }
}

/* The board shows the job's name and nothing else about it. Everything the
   name used to sit beside — customer, contact, phone, map — is behind a tap,
   which is where information read once a day belongs. */
.project-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 38px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font-weight: 500;
  text-align: left;
}

#project-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-btn > .bx:first-child { color: var(--ink-soft); }

.project-btn .caret { flex: 0 0 auto; color: var(--brand); font-size: 14px; }
.project-btn:active { background: var(--surface-2); }

.icon-btn {
  min-width: var(--tap);
  height: var(--tap);
  padding: 0 10px;
  border-radius: var(--radius);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 16px;
  color: var(--ink-2);
  background: var(--surface-2);
  border: 1px solid var(--line);
  flex: 0 0 auto;
  white-space: nowrap;
}

/* The word beside the glyph is what makes the row readable. The toolbar wraps
   to keep it, so the word is only dropped on a phone too narrow for even a
   second row of it. */
.icon-btn .label { font-size: 13px; }

@media (max-width: 339px) {
  .icon-btn .label { display: none; }
}

.icon-btn:active { background: var(--line); }
.icon-btn.on { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }

.chip.person { background: var(--brand-soft); color: var(--brand-text); }
.chip.person.me { background: var(--brand); color: var(--brand-ink); font-weight: 600; }

/* A chip holding faces needs the padding on its left taken back, or the ring
   round the first avatar reads as a gap. */
.chip.person { padding-left: 3px; gap: 6px; }
.chip.person .avatar.xs { width: 19px; height: 19px; font-size: 9px; }

.btn {
  min-height: var(--tap);
  padding: 0 16px;
  border-radius: var(--radius);
  background: var(--brand);
  color: var(--brand-ink);
  font-weight: 500;
  box-shadow: 0 .125rem .25rem 0 rgba(105, 108, 255, .4);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}

.btn.ghost {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  box-shadow: none;
}

.btn.danger { background: var(--danger); box-shadow: 0 .125rem .25rem 0 rgba(255, 62, 29, .4); }
.btn.sm { min-height: 38px; padding: 0 12px; font-size: 14px; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* ------------------------------------------------------------------- tabs */

/* Sneat's pill nav rather than an underline: the same shape the HR portal
   uses for its own view switchers, and a filled pill survives daylight on a
   phone better than a 3px rule. */
/* Four tabs, or five once ผู้ดูแลกลุ่มบริษัท adds ภาพรวม. Five needed 373px
   of a 360px screen, and because `.tabs` had no overflow of its own the extra
   13px went to the *page* — every screen in the app scrolled sideways, which
   is what made a phone feel like it needed zooming out. Two guards now, so it
   cannot come back whatever gets added here: the strip scrolls within itself,
   and below 400px it gives up some padding before it gives up fitting. */
.tabs {
  display: flex;
  gap: 4px;
  padding: 8px 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  scrollbar-width: none;
  /* And it never gives up its *height*. `#app` is a fixed-height flex column,
     so a tall view — รายงาน, ภาพรวม — hands the column more content than it
     has room for, and the shrink is shared out in proportion to every item's
     size. `overflow-x: auto` makes this strip's automatic minimum height 0,
     so it happily took its share: 54px of pills squeezed into 32px, clipped
     mid-glyph under the bar above it. The bars around it were already pinned
     (`.workbar`, `.mine-note`); this one was not. The view is the only thing
     in the column that scrolls, so the view is the only thing that shrinks. */
  flex: 0 0 auto;
}

.tabs::-webkit-scrollbar { display: none; }

.tabs button {
  flex: 1 1 0;
  min-width: 0;
  min-height: 38px;
  border-radius: var(--radius);
  font-size: 14px;
  white-space: nowrap;
  color: var(--ink-2);
}

@media (max-width: 400px) {
  .tabs { padding: 8px 8px; gap: 2px; }
  .tabs button { font-size: 13px; padding-left: 4px; padding-right: 4px; }
  .tabs button > .bx { font-size: .95em; }
}

/* The way out of a strip that is not the job's own views. Icon only and
   shrink-to-fit: it is not one of the sections, so it does not take a
   section's width — the same ‹ the project toolbar uses, meaning the same
   thing, one level up. */
.tabs .tab-back {
  flex: 0 0 auto;
  min-width: 36px;
  padding: 0 6px;
  color: var(--ink-soft);
  font-size: 18px;
  margin-right: 4px;
  border-right: 1px solid var(--line);
  border-radius: var(--radius) 0 0 var(--radius);
}

.tabs .tab-back:active { background: var(--surface-2); }

.tabs button.on {
  color: var(--brand-ink);
  font-weight: 500;
  background: var(--brand);
  box-shadow: 0 .125rem .25rem 0 rgba(105, 108, 255, .4);
}

/* ------------------------------------------------------ the "my work" strip
   Only on screen while the filter is. A board quietly showing a third of its
   cards is indistinguishable from a board that has lost them, so the filter
   states itself here and carries its own way out. */

.mine-note,
.ov-scope {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  font-size: 13px;
  color: var(--brand-text);
  background: var(--brand-soft);
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}

.mine-note .grow,
.ov-scope .grow { flex: 1 1 auto; min-width: 0; }

/* The group screen's own version of the same admission: what is on screen
   is less than everything, and here is the way back to all of it. */
.mine-note .mini,
.ov-scope .mini {
  width: auto;
  height: 32px;
  padding: 0 10px;
  background: var(--surface);
  color: var(--brand-text);
}

.view {
  flex: 1 1 auto;
  overflow: auto;
  padding: 12px;
  -webkit-overflow-scrolling: touch;
}

/* ------------------------------------------------------------------ admin */
/* Same shape as the HR portal — a menu down the side, one section at a time —
   without pulling in its theme: on a phone the menu becomes a scrolling row of
   chips rather than a drawer nobody would find. */

/* ผู้ดูแล's top bar. Same three parts as the HR portal's, in the same order:
   the hamburger, the name of the page, the way out. */
.adminbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 21;
  flex: 0 0 auto;
}

.adminbar-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.admin {
  display: flex;
  flex-direction: column;
  gap: 12px;
  height: 100%;
}

@media (min-width: 900px) {
  .admin { flex-direction: row; }
}

/* The menu was a horizontally scrolling row of chips, on the theory that a
   drawer is a thing nobody finds. Measured on a real phone that theory was
   wrong in the worst way: six sections wanted 832px of a 336px strip, so four
   of them sat off-screen with nothing — no arrow, no fade — saying they were
   there. A drawer people have to open beats four pages people cannot see.

   One element, two layouts, the way Sneat does it: fixed and off-canvas on a
   phone, docked in the flow on a wide screen where there is room for it. */
.admin-menu {
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: min(280px, 82vw);
  padding: 12px;
  background: var(--surface);
  border-right: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
  overflow-y: auto;
  z-index: 41;
  transform: translateX(-100%);
  transition: transform .2s ease;
  overscroll-behavior: contain;
}

.admin-menu.open { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .admin-menu { transition: none; }
}

/* What the drawer closes onto. Below the drawer, above everything else. */
.admin-scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  z-index: 40;
}

/* Docked, so the hamburger has nothing left to open. */
@media (min-width: 900px) {
  #admin-menu-btn { display: none; }

  .admin-menu {
    position: static;
    transform: none;
    width: 220px;
    flex: 0 0 auto;
    box-shadow: none;
    padding: 0 12px 0 0;
    overflow: visible;
  }
  .admin-scrim { display: none; }
}

.admin-menu button {
  white-space: nowrap;
  text-align: left;
  padding: 10px 14px;
  border-radius: var(--radius);
  min-height: 44px;
  color: var(--ink-2);
  background: none;
  border: 1px solid transparent;
  display: flex;
  align-items: center;
  gap: 10px;
}

.admin-menu button > .bx { font-size: 1.1em; opacity: .8; flex: none; }

/* The last item is the way back to the work, not another settings page — so
   it is set apart rather than sitting in the list as a seventh section. */
.admin-menu .back {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
}

.admin-menu button.on {
  background: rgba(105, 108, 255, .1);
  color: var(--brand);
  font-weight: 700;
  border-color: rgba(105, 108, 255, .35);
}

/* The group headers are shown at both sizes now. They were hidden on a phone
   because a row of chips has no room for them; a drawer is a column and the
   headers are most of what makes six items scannable. */
.admin-menu .group {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .3px;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 12px 6px 2px;
}

.admin-menu .group:first-child { padding-top: 2px; }

.admin-body { flex: 1 1 auto; min-width: 0; overflow-y: auto; }

.admin-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
  flex-wrap: wrap;
}

.admin-head h2 { font-size: 17px; margin: 0; flex: 1 1 100%; }

@media (min-width: 560px) {
  .admin-head h2 { flex: 1 1 auto; }
}

.admin-head input[type="search"] {
  min-height: 40px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 6px 12px;
  min-width: 160px;
  flex: 1 1 180px;
  background: var(--surface);
}

.rec {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin-bottom: 8px;
}

.rec .grow { flex: 1 1 auto; min-width: 0; }
.rec b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rec small { color: var(--ink-soft); font-size: 12px; }

.tag {
  font-size: 11px;
  border-radius: 999px;
  padding: 2px 8px;
  background: var(--surface-2);
  color: var(--ink-soft);
  flex: 0 0 auto;
}

.tag.juristic { background: var(--brand-soft); color: var(--brand-text); }
.tag.individual { background: var(--ok-soft); color: var(--ok-text); }
.tag.off { background: var(--danger-soft); color: var(--danger-text); }

/* เข้าใช้ PM ได้ / ยังไม่อนุญาต on a member's row, and the filter over the list. */
.mem-access { margin: 0 0 12px; margin-left: 0; }
.mem-access button span { display: inline !important; }
.mem-access button small { font-size: 11px; opacity: .7; margin-left: 4px; }
.access-pick { gap: 4px; color: var(--ink-soft); }
.access-pick.in { background: var(--ok-soft); border-color: transparent; color: var(--ok-text); }
.rec.shut > .avatar, .rec.shut > .grow { opacity: .6; }
/* A phone has no room for name, role, access and two buttons on one line. */
@media (max-width: 480px) { .rec:has(.access-pick) { flex-wrap: wrap; } }

/* Back button and title on one line, the tags wrapping under them — a
   customer's own name should not be squeezed into a column three words wide
   by an "เพิ่มโดย บริษัท …" tag beside it. */
.detail-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.detail-head h2 { margin: 0; font-size: 17px; flex: 1 1 60%; min-width: 0; }
.detail-head .tag { flex: 0 0 auto; }

.facts {
  display: grid;
  grid-template-columns: 1fr;
  gap: 6px 16px;
  font-size: 13px;
  margin-bottom: 14px;
}

@media (min-width: 560px) {
  .facts { grid-template-columns: 1fr 1fr; }
}

.facts div { display: flex; gap: 6px; }
.facts span { color: var(--ink-soft); flex: 0 0 auto; }

/* --------------------------------------------------------------- calendar */

.cal-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.cal-head .month {
  flex: 1 1 auto;
  text-align: center;
  font-weight: 700;
  font-size: 16px;
}

.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
}

.cal-dow {
  text-align: center;
  font-size: 12px;
  color: var(--ink-soft);
  padding-bottom: 2px;
}

.cal-day {
  background: var(--surface);
  border-radius: var(--radius);
  min-height: 78px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  border: 1px solid transparent;
  text-align: left;
  overflow: hidden;
}

.cal-day.other { background: var(--surface-2); }
.cal-day.today { border-color: var(--brand); }
.cal-day .num { font-size: 12px; color: var(--ink-soft); }
.cal-day.today .num { color: var(--brand); font-weight: 700; }

.cal-ev {
  font-size: 11px;
  line-height: 1.25;
  border-left: 3px solid var(--line);
  padding: 1px 3px;
  background: var(--surface-2);
  border-radius: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cal-more { font-size: 10px; color: var(--ink-soft); }

.day-item {
  display: block;
  width: 100%;
  text-align: left;
  border: 1px solid var(--line);
  border-left: 4px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin-bottom: 8px;
  background: var(--surface);
}

.day-item small { display: block; color: var(--ink-soft); font-size: 12px; margin-top: 2px; }

/* ------------------------------------------------------------------ gantt */

.gantt {
  --day-w: 26px;
  --name-w: 124px;
  min-width: max-content;
}

@media (min-width: 700px) {
  .gantt { --name-w: 190px; }
}

.g-row {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--line);
}

.g-name {
  width: var(--name-w);
  flex: 0 0 auto;
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--surface);
  border-right: 1px solid var(--line);
  padding: 8px;
  font-size: 12px;
  line-height: 1.3;
  overflow: hidden;
}

.g-name small { color: var(--ink-soft); display: block; font-size: 10px; }
/* The one part of a Gantt row that opens the job. Said with a cursor and a
   press state, because it is now the exception on a row rather than the row. */
.g-open { cursor: pointer; }
.g-open:active { background: var(--surface-2); }


.g-track {
  position: relative;
  flex: 0 0 auto;
  min-height: 46px;
  /* The day grid. Only วันนี้ had a line, so every other date was measured by
     eye off the numbers in the header — "does this bar finish on the 12th or
     the 13th" is exactly the question a Gantt exists to answer.

     It is the track's own background rather than a div per day: a six-month
     chart is ~180 columns on every row, which is a lot of DOM for something
     that is a repeating pattern by definition. `--grid-w` is the period the
     renderer picked — one line per day while the columns are wide, one a week
     or a fortnight once they are squeezed — and it is always a divisor of the
     header's tick step, so every numbered day lands on a line. */
  background-image: repeating-linear-gradient(to right,
    var(--grid) 0 1px, transparent 1px var(--grid-w, var(--day-w)));
}

/* The company header inside the group Gantt is a label, not a timeline. */
.g-row.g-group .g-track { background-image: none; }

.g-head .g-track, .g-head .g-name { min-height: 0; padding: 0; }

.g-month {
  position: absolute;
  top: 0;
  font-size: 11px;
  color: var(--ink-soft);
  padding-left: 4px;
  border-left: 1px solid var(--line);
  white-space: nowrap;
}

.g-tick {
  position: absolute;
  top: 16px;
  font-size: 9px;
  color: var(--ink-soft);
  text-align: center;
}

.g-bar {
  position: absolute;
  top: 8px;
  height: 16px;
  border-radius: 6px;
  background: var(--brand);
  overflow: hidden;
  opacity: .85;
}

.g-bar > i {
  display: block;
  height: 100%;
  background: rgba(255, 255, 255, .45);
  float: right;
}

.g-stage {
  position: absolute;
  top: 28px;
  height: 8px;
  border-radius: 3px;
  opacity: .9;
}

.g-today {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--danger);
  opacity: .5;
}

/* ส่งออก PDF / แชร์แผนงาน — above the legend, so the chart itself still
   starts as high on the screen as it can. */
.g-actions { margin: 6px 0 0; flex-wrap: wrap; }

/* Everything above the chart stays put while the chart is dragged sideways.
   #gantt is the scrolling box and these are its other children, so without
   this they slide off to the left with it — which was survivable for a count
   and a legend and is not for two buttons: opening the view already scrolls it
   to วันนี้, so the first thing a long job did was carry them off the screen.
   `left: 0` is what pins them; they are as wide as the box, not as wide as the
   chart, so nothing else has to change. */
#gantt > .sub, #gantt > .g-actions, #gantt > .g-legend {
  position: sticky;
  left: 0;
}

.g-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 11px;
  color: var(--ink-soft);
  margin: 10px 0 4px;
  align-items: center;
}

.g-legend span { display: inline-flex; align-items: center; gap: 4px; }
.g-legend i { width: 12px; height: 8px; border-radius: 3px; display: inline-block; }

.g-flag { color: var(--danger); font-size: 10px; }

/* ------------------------------------------------------------------ board */

/* One swipe, one column.

   This used to be `proximity`, which only snaps when the scroll happens to
   stop near a column edge — so most swipes left the board parked between two
   columns, with half of each on screen and neither readable. `mandatory` plus
   `scroll-snap-stop: always` makes the board page: every swipe lands on a
   column, and a hard flick stops at the next one instead of sailing past four.
   `scroll-padding-left` is the board's own padding, so a snapped column sits
   where the first one does rather than flush against the edge. */
.board {
  flex: 1 1 auto;
  display: flex;
  gap: 12px;
  padding: 12px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-padding-left: 12px;
  -webkit-overflow-scrolling: touch;
}


.col {
  flex: 0 0 var(--col-w);
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: flex;
  flex-direction: column;
  background: var(--board-col);
  border-radius: var(--radius);
  max-height: 100%;
  min-height: 120px;
}

.col-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 2px solid transparent;
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--surface);
  position: sticky;
  top: 0;
}

.col-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex: 0 0 auto;
}

.col-name {
  font-weight: 700;
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.col-count {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--ink-soft);
  background: var(--surface-2);
  border-radius: 999px;
  padding: 2px 8px;
}

.col-count.over { background: var(--danger-soft); color: var(--danger-text); font-weight: 700; }
/* Every job in this step is behind you. Worth its own colour: it is the thing
   a foreman is scanning the board for. */
.col-count.all-done { background: var(--ok-soft); color: var(--ok-text); font-weight: 700; }

/* How much of this step is done, drawn to a width. The pill above says it
   exactly; this is the version readable at arm's length on a site. */
.col-meter {
  height: 3px;
  margin: 0 12px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}
.col-meter > i { display: block; height: 100%; transition: width .2s ease; }

.col-gate {
  font-size: 11px;
  color: var(--ink-soft);
  padding: 4px 12px 0;
}

.col-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 60px;
}


.card {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 10px 12px;
  border-left: 4px solid var(--line);
  touch-action: manipulation;
  user-select: none;
}

.card:active { background: var(--surface-2); }
.card.p-urgent { border-left-color: var(--danger); }
.card.p-high   { border-left-color: var(--warn); }
.card.p-medium { border-left-color: var(--brand); }
.card.p-low    { border-left-color: var(--ink-soft); }

.card-head { display: flex; align-items: flex-start; gap: 8px; }
.card-title { font-weight: 600; line-height: 1.35; word-break: break-word; flex: 1 1 auto; min-width: 0; }

/* The state of the job, as one glyph at the head of the card. Colour carries
   it at a glance; the icon carries it for anyone who cannot use the colour. */
.card-status { flex: 0 0 auto; font-size: 18px; line-height: 1.2; color: var(--ink-soft); }
.card.s-doing .card-status { color: var(--brand); }
.card.s-done  .card-status { color: var(--ok-text); }

/* A finished job stays on the board — the step is not over until all of them
   are — and it says so in green, the same green as the เสร็จแล้ว badge on the
   task page and as the column's own progress bar.

   Not crossed out, and not greyed: a struck-through title reads as cancelled
   rather than finished, and on a phone held in daylight a grey card among grey
   cards said nothing at all. Green is the one thing that survives both. The
   left edge stays the job's priority colour, because the column is sorted by
   that and the edge is what explains the order. */
.card.s-done { background: var(--ok-soft); }
.card.s-done .card-title { color: var(--ink); }
.card.s-done .card-meta { color: var(--ok-text); }
.card.s-done .chip { background: var(--surface); }
/* `.card:active` is the tap feedback for every other card; a green one needs
   its own or a finished card answers a tap with nothing. */
.card.s-done:active { background: var(--ok-soft); filter: brightness(.95); }

/* Held at the top of its column by hand. The ring is deliberately not a colour
   of its own: red and amber on this board mean something is wrong, and a
   pinned job is not wrong — it is first. */
.card.pinned { box-shadow: var(--shadow), 0 0 0 2px var(--brand-soft); }
.chip.pin { background: var(--brand-soft); color: var(--brand-text); font-weight: 600; }

/* One tap to move the job on, without opening it. This is the button the old
   board spent a drag on. */
.card-advance {
  display: block;
  width: 100%;
  margin-top: 10px;
  min-height: 34px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: 13px;
  font-weight: 500;
}
.card-advance:active { background: var(--brand-soft); color: var(--brand-text); }

.card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
  font-size: 12px;
  color: var(--ink-soft);
  align-items: center;
}

.chip {
  background: var(--surface-2);
  border-radius: 999px;
  padding: 2px 8px;
}

.chip.red { background: var(--danger-soft); color: var(--danger-text); }
.chip.amber { background: var(--warn-soft); color: var(--warn-text); }
.chip.green { background: var(--ok-soft); color: var(--ok-text); }



.empty {
  color: var(--ink-soft);
  font-size: 13px;
  text-align: center;
  padding: 18px 8px;
}

/* -------------------------------------------------------- stage manager */

.stage-row {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px;
  margin-bottom: 10px;
}

.stage-main {
  display: flex;
  align-items: center;
  gap: 8px;
}

.stage-main input[type="color"] {
  width: 38px;
  height: 38px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  flex: 0 0 auto;
}

.stage-main input[type="text"], .stage-main input:not([type]) {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 38px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 6px 10px;
}

.mini {
  width: 38px;
  height: 38px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-2);
  flex: 0 0 auto;
  font-size: 14px;
}

/* A `.mini` with a word in it.
   The base above is a 38px square — this page's icon-only button — so any text
   put inside one is folded down a 38px column, a letter or two per line. Two
   strips written before this modifier existed (`.mine-note`, `.ov-scope`) make
   the same override by hand; anything new says `class="mini wide"`. */
.mini.wide {
  width: auto;
  padding: 0 12px;
  gap: 5px;
  font-size: 13px;
  white-space: nowrap;
}

.mini:active { background: var(--line); }
.mini.danger { color: var(--danger); }

.stage-rules {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
  margin-top: 10px;
  font-size: 13px;
  color: var(--ink-soft);
  align-items: center;
}

.stage-rules label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.stage-rules input[type="checkbox"] { width: 20px; height: 20px; }

.stage-rules input[type="number"] {
  width: 68px;
  min-height: 34px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 4px 8px;
}

/* --------------------------------------------------------- sheet & modal */

.scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  z-index: 40;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

@media (min-width: 700px) {
  .scrim { align-items: center; }
}

.panel {
  background: var(--surface);
  width: 100%;
  max-width: 560px;
  max-height: 88vh;
  overflow-y: auto;
  border-radius: 12px 12px 0 0;
  padding: 16px;
  padding-bottom: max(16px, env(safe-area-inset-bottom));
}

@media (min-width: 700px) {
  .panel { border-radius: 12px; }
}

.panel h2 { margin: 0 0 4px; font-size: 18px; }
.panel h3 { margin: 18px 0 8px; font-size: 14px; color: var(--ink-soft); }
.panel .sub { color: var(--ink-soft); font-size: 13px; margin: 0 0 12px; }

.field { display: block; margin-bottom: 12px; }
.field > span { display: block; font-size: 13px; color: var(--ink-soft); margin-bottom: 4px; }

.field input, .field select, .field textarea {
  width: 100%;
  min-height: var(--tap);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  background: var(--surface);
}

.field textarea { min-height: 96px; resize: vertical; }

/* --------------------------------------------------------------- date field
   A dd/mm/yyyy text box with the phone's own calendar behind the button at its
   right edge. The native <input type="date"> is still there, stretched
   invisibly over that button: tapping it opens the real picker on every phone,
   with no dependency on showPicker(), which older handsets do not have. */

.datefield { position: relative; display: block; }

/* The widget's own box, rather than the one it used to borrow from `.field
   input`. It spent its whole life inside a <label class="field"> and so was
   styled as if that were part of it — until รายงาน put one in a toolbar, where
   it came out as a browser-default text box with a visible mm/dd/yyyy control
   spilling out beside it. What a datefield looks like is the datefield's
   business; `.field` only decides what is around it.

   Tabular figures keep 11/11 and 30/09 the same width, so a column of dates
   lines up instead of shimmering as it is typed. */
.datefield .date-text {
  width: 100%;
  min-height: var(--tap);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 10px 42px 10px 12px;
  font-variant-numeric: tabular-nums;
}

.date-pick {
  position: absolute;
  top: 1px; right: 1px; bottom: 1px;
  width: 40px;
  display: grid;
  place-items: center;
  color: var(--ink-soft);
  border-radius: 0 var(--radius) var(--radius) 0;
  cursor: pointer;
}
.date-pick:hover { color: var(--brand); background: var(--surface-2); }

/* Invisible, but really there and really on top — this is what the tap hits.
   Scoped to the widget, not to `.field`: an unhidden native date input is the
   most visible way this component can break, and it must not depend on where
   the component was put. */
.date-pick input.date-native {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  min-height: 0;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  background: none;
  cursor: pointer;
}

/* Two ways a date can be wrong, drawn the same: `bad` is a day that does not
   exist or was left half-typed, `crossed` is a range running backwards. */
.datefield.bad .date-text,
.datefield.crossed .date-text { border-color: var(--danger); }

.field-err {
  margin: -6px 0 12px;
  font-size: 13px;
  color: var(--danger-text);
  background: var(--danger-soft);
  border-radius: var(--radius);
  padding: 6px 10px;
}
.row { display: flex; gap: 10px; }
.row > * { flex: 1 1 0; min-width: 0; }
.actions { display: flex; gap: 10px; margin-top: 16px; }
.actions .btn { flex: 1 1 auto; justify-content: center; }

/* An action row with more than two buttons in it — การแปลภาษา gives each job
   up to four. The base row does not wrap, which is right for a dialog's
   confirm/cancel pair and wrong the moment a third button exists on a 430 px
   phone: the fourth simply leaves the screen. */
.actions.wrap { flex-wrap: wrap; row-gap: 8px; }

.edit-block {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin: 16px 0 4px;
}

.edit-block > summary {
  cursor: pointer;
  font-size: 14px;
  color: var(--ink-soft);
  min-height: 28px;
  display: flex;
  align-items: center;
}

.edit-block[open] > summary { margin-bottom: 10px; color: var(--ink); font-weight: 600; }

/* A <summary> is display:list-item; making it a flex row is what removes the
   default triangle and lets the caret be a real icon that can be spun. */
.edit-block > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  list-style: none;
}

.edit-block > summary::-webkit-details-marker { display: none; }
.edit-block > summary > span { flex: 1 1 auto; }
.edit-block > summary > .bx { opacity: .7; }
.sum-caret { transition: transform .15s ease; }
.edit-block[open] > summary .sum-caret { transform: rotate(180deg); }

/* Work is added to a column, not to a board in the abstract — so the button
   is in the column's own header, where the person adding it is already
   looking, and the stage arrives pre-filled. */
.col-add {
  width: 30px;
  height: 30px;
  flex: 0 0 auto;
  border-radius: var(--radius);
  background: var(--brand-soft);
  color: var(--brand-text);
  font-size: 17px;
  line-height: 1;
  display: grid;
  place-items: center;
}

.col-add:active { background: var(--brand); color: var(--brand-ink); }

.col-more {
  width: 100%;
  font-size: 13px;
  color: var(--brand);
  background: rgba(105, 108, 255, .08);
  border: 1px dashed rgba(105, 108, 255, .4);
  border-radius: var(--radius);
  min-height: 40px;
}

.col-more:active { background: rgba(105, 108, 255, .18); }

/* -------------------------------------------------------- media & thumbs */

.thumbs { display: flex; gap: 8px; flex-wrap: wrap; }

.thumb {
  position: relative;
  width: 84px;
  height: 84px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-2);
  overflow: hidden;
  display: grid;
  place-items: center;
  cursor: pointer;
}

.thumb img, .thumb video { width: 100%; height: 100%; object-fit: cover; display: block; }

/* HEIC before its JPEG derivative exists, and anything else with no preview:
   the name of the format beats a broken-image icon. */
.thumb-name {
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-soft);
  text-align: center;
  padding: 4px;
  line-height: 1.3;
}

.thumb-badge {
  position: absolute;
  left: 4px;
  bottom: 4px;
  background: rgba(0, 0, 0, .62);
  color: #fff;
  font-size: 11px;
  line-height: 1;
  padding: 3px 5px;
  border-radius: 4px;
}

.thumb-x {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(0, 0, 0, .55);
  color: #fff;
  font-size: 15px;
  line-height: 1;
}

.media-pick { margin-bottom: 12px; }
.media-label { display: block; font-size: 13px; color: var(--ink-soft); margin-bottom: 6px; }
.hidden-input { display: none; }

/* One button for everything the phone can hand over. It carries no `accept`,
   so nothing is greyed out in the chooser — see mediaPicker(). */
.media-btn {
  width: 100%;
  justify-content: center;
  font-size: 14px;
  gap: 8px;
  margin-bottom: 10px;
}

/* One photo or one video, full size. A single image is exactly what an
   overlay suits - unlike a whole task, which is why that became a page.

   Not `.viewer`: that is also the name of a role, and `.role-badge viewer`
   on the identity chip picked up this rule and covered the screen. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(0, 0, 0, .92);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 16px;
  padding-bottom: max(16px, env(safe-area-inset-bottom));
}

.lightbox img, .lightbox video { max-width: 100%; max-height: calc(100% - 60px); object-fit: contain; }

.viewer-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
  width: 100%;
}

.viewer-name { color: #d8dade; font-size: 12px; flex: 1 1 100%; text-align: center; word-break: break-all; }
.lightbox .btn.ghost { background: rgba(255, 255, 255, .1); color: #fff; border-color: rgba(255, 255, 255, .3); }

/* Where the picture goes, so the arrows can sit over the whole area rather
   than beside whatever width this particular photo happens to be. */
.viewer-stage {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.viewer-stage img, .viewer-stage video { max-width: 100%; max-height: 100%; object-fit: contain; }

/* ‹ and › for the rest of the set. Pinned to the edges and 48px across: on a
   phone this is thumbed one-handed, and a small target in the middle of a
   photo is a target that closes the viewer by accident instead. */
.viewer-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .25);
  background: rgba(0, 0, 0, .45);
  color: #fff;
  font-size: 26px;
  display: grid;
  place-items: center;
  cursor: pointer;
}

.viewer-nav:active { background: rgba(255, 255, 255, .2); }
.viewer-nav.prev { left: 10px; }
.viewer-nav.next { right: 10px; }

/* 3/8 — how far through the set this is, which is the thing the arrows raise
   the question of. */
/* What stands where the picture would be when the browser will not paint it —
   a HEIC whose JPEG the worker has not written yet, most often. Kept inside
   the viewer rather than bounced out to a new tab: inside LINE, a new tab is
   the task page gone. เปิดไฟล์ต้นฉบับ is still on the bar below. */
.viewer-none {
  max-width: 320px;
  padding: 24px 18px;
  text-align: center;
  color: #d8dade;
}

.viewer-none > .bx { font-size: 40px; opacity: .5; }
.viewer-none p { margin: 10px 0 0; font-size: 14px; }
.viewer-none p.sub { margin-top: 6px; font-size: 12.5px; color: #a9aeb4; }

.viewer-count {
  color: #d8dade;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  flex: 1 1 100%;
  text-align: center;
  margin-top: -4px;
}

@media (min-width: 700px) {
  .viewer-nav { width: 56px; height: 56px; font-size: 30px; }
  .viewer-nav.prev { left: 24px; }
  .viewer-nav.next { right: 24px; }
}

.stage-pick { display: flex; flex-wrap: wrap; gap: 8px; }

.stage-pick button {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 8px 14px;
  background: var(--surface);
  min-height: 40px;
}

.stage-pick button.current { background: var(--brand-soft); border-color: var(--brand); color: var(--brand-text); font-weight: 600; }

/* ------------------------------------------------------------- task page */
/* A view, not a dialog. The timeline is the reason the page is opened and it
   is longer than any sheet can hold, so it gets the room.

   This page was drawn for a phone and then simply stretched: on a laptop a
   single 1400 px column of cards put the "บันทึกใหม่" box a screen and a half
   below the title and left two thirds of the window empty. It is a grid now,
   from 980 px up. What goes on which side is decided by how the page is read,
   not by how much of it there is:

     left   the running record — what was written, what was attached, when.
            This is the tall side and the reason the page exists.
     right  the small set of things you *change* about the job: who is on it,
            which stage it is in, its dates. Sticky, so a manager reassigning
            work does not scroll a hundred timeline entries to reach it.

   Below 980 px the grid collapses and the same blocks stack in that order,
   which is the phone layout that was already there. */

#task { padding: 0; }

.task-top {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.task-top .back { font-size: 20px; }

.task-top-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 16px;
  color: var(--ink);
  /* two lines, then ellipsis — a site task name is rarely short */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Centred and capped: text is read in a column, and a 2560 px monitor showing
   one 2400 px line of a site note is not more readable, it is less. */
.task-body {
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 1240px;
  margin: 0 auto;
}

@media (min-width: 700px) {
  .task-body { padding: 20px; gap: 16px; }
}

.task-grid { display: flex; flex-direction: column; gap: 12px; }
.task-main, .task-side { display: flex; flex-direction: column; gap: 12px; min-width: 0; }

@media (min-width: 980px) {
  .task-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    align-items: start;
    gap: 16px;
  }
  .task-main, .task-side { gap: 16px; }

  /* Sticky under the title bar, which is itself sticky — hence the offset. */
  .task-side { position: sticky; top: 68px; }
}

/* --------------------------------------------------------------- the hero */
/* The bar above can only afford a clipped two-line title, which is no good as
   the page's own heading. The name is said again here at full size with the
   four things always asked next to it: which stage, who is on it, how far
   along, and whether anything is wrong. The stage's colour runs down the left
   edge — the same cue the board card carries, so the card and the page it
   opens are visibly the same object. */
.task-hero {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 4px solid var(--stage, var(--brand));
  border-radius: var(--radius-lg);
  padding: 16px;
}

@media (min-width: 700px) {
  .task-hero { padding: 20px 24px; }
}

.hero-top { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }

.hero-title {
  margin: 0 0 12px;
  font-size: 20px;
  line-height: 1.4;
  font-weight: 600;
  color: var(--ink);
  word-break: break-word;
}

@media (min-width: 700px) {
  .hero-title { font-size: 24px; }
}

.hero-who { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.hero-who-text { font-size: 14px; color: var(--ink); min-width: 0; }
.hero-who-text.muted { color: var(--ink-soft); }
.hero-who > .bx { color: var(--ink-soft); font-size: 18px; }

/* The hero used to carry a percentage bar. It was drawing the pipeline's
   shape, not the job's state — so it says the state. */
.hero-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  background: var(--muted-soft);
  color: var(--muted-text);
}
.hero-status.s-doing { background: var(--brand-soft); color: var(--brand-text); }
.hero-status.s-done  { background: var(--ok-soft); color: var(--ok-text); }

/* The three states as one row, with the current one held down. A segmented
   control rather than a dropdown: there are exactly three, they are ordered,
   and on a phone three buttons beat a select that opens a wheel. */
.status-steps {
  display: flex;
  gap: 6px;
  margin-top: 12px;
}
.status-step {
  flex: 1 1 0;
  min-width: 0;
  min-height: var(--tap);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 4px 2px;
  font-size: 12px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-soft);
}
.status-step > .bx { font-size: 18px; }
.status-step:disabled { opacity: 1; }          /* held down, not greyed out */
.status-step.on { border-color: transparent; background: var(--muted-soft); color: var(--muted-text); font-weight: 600; }
.status-step.on.s-doing { background: var(--brand-soft); color: var(--brand-text); }
.status-step.on.s-done  { background: var(--ok-soft); color: var(--ok-text); }

/* The forward step, full width under the row — the one pressed a hundred
   times for every one correction above it. */
.status-go { width: 100%; justify-content: center; margin-top: 10px; }


/* The stage as a pill in its own colour. `--stage` is set inline from the
   stage record, so a pipeline renamed and recoloured in the admin tab needs no
   CSS. `color-mix` gives the tint and keeps the text on it legible without a
   second variable per stage — the same trick Sneat's bg-label-* does by hand. */
.stage-pill, .pri-pill {
  border-radius: 999px;
  padding: 3px 12px;
  font-size: 12.5px;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.stage-pill {
  /* Plain first, mixed second. `color-mix` is a 2023 feature and this app is
     opened in whatever webview the crew's phone shipped with — an older one
     drops the two declarations below and keeps these, which is a neutral grey
     pill rather than an invisible one. */
  background: var(--surface-2);
  color: var(--ink);
  border: 1px solid var(--line);

  background: color-mix(in srgb, var(--stage) 16%, var(--surface));
  color: color-mix(in srgb, var(--stage) 78%, var(--ink));
  border-color: color-mix(in srgb, var(--stage) 34%, transparent);
}

:root[data-bs-theme="dark"] .stage-pill {
  background: color-mix(in srgb, var(--stage) 26%, var(--surface));
  color: color-mix(in srgb, var(--stage) 45%, #ffffff);
}

.stage-pill.sm { padding: 2px 9px; font-size: 12px; }
.stage-pill.sm small { opacity: .7; font-size: 11px; }

.pri-pill { background: var(--muted-soft); color: var(--muted-text); }
.pri-pill.p-urgent { background: var(--danger-soft); color: var(--danger-text); }
.pri-pill.p-high { background: var(--warn-soft); color: var(--warn-text); }
.pri-pill.p-low { background: var(--surface-2); color: var(--ink-soft); }

/* The project editor's "ขั้นตอนที่ใช้อยู่ตอนนี้" row. */
.stage-now { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }

.task-body .task-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 14px;
}

.task-body .task-card > h3:first-child { margin-top: 0; }
.task-body h3 { margin: 18px 0 10px; font-size: 14px; color: var(--ink-soft); }
.task-body .sub { color: var(--ink-soft); font-size: 13px; margin: 0 0 12px; }

/* "ขั้นนี้ต้องมี…" — an icon and a sentence. Flex rather than an inline glyph,
   so the second and later lines of a wrapped sentence align under the first
   word instead of under the icon. */
.sub.gate { display: flex; align-items: flex-start; gap: 7px; }
.sub.gate > .bx { font-size: 15px; margin-top: 2px; }
.task-desc { margin: 0 0 12px; color: var(--ink); white-space: pre-wrap; }

.flag {
  background: var(--warn-soft);
  color: var(--warn-text);
  border-radius: 999px;
  padding: 3px 12px;
  font-size: 12.5px;
  font-weight: 600;
}

/* Label/value pairs rather than one dot-separated run: on a phone that line
   wrapped into a paragraph nobody could read a single fact out of. */
.facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 14px;
  margin: 0;
  font-size: 13px;
}

.facts dt { color: var(--ink-soft); }
.facts dd { margin: 0; color: var(--ink); text-align: right; }

/* An icon, a label and a value. The plain two-column grid put a long Thai
   label and a long Thai date in a tug of war over one line; the icon gives the
   row a fixed left edge and the label something to align against. */
.icon-facts { gap: 10px 14px; font-size: 13.5px; align-items: baseline; }
.icon-facts dt { display: flex; align-items: center; gap: 7px; white-space: nowrap; }
.icon-facts dt > .bx { font-size: 15px; opacity: .65; }

.task-body .edit-block { margin: 0; }
.task-body .edit-block > summary { min-height: 32px; }

/* --------------------------------------------------------------- people */
/* One face, drawn the same everywhere someone appears — on a card, in the
   crew list, against a timeline entry — so that a face on a card and a face in
   the timeline are recognisably the same person. Initials where there is no
   picture: an HR account has one, a LINE account usually does, a member who
   has only ever been added by an admin has none. */

.avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--brand-soft);
  color: var(--brand-text);
  display: inline-grid;
  place-items: center;
  font-weight: 700;
  font-size: 13px;
  line-height: 1;
  flex: 0 0 auto;
  overflow: hidden;
  vertical-align: middle;
}

.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar.sm { width: 26px; height: 26px; font-size: 11px; }
.avatar.xs { width: 22px; height: 22px; font-size: 9.5px; }

/* The signed-in person's own face, so "is this mine" is answerable without
   reading a name. */
.avatar.me { background: var(--brand); color: var(--brand-ink); }
.avatar.more { background: var(--surface-2); color: var(--ink-soft); font-weight: 600; }

/* Overlapping faces. A ring in the surface colour is what separates them —
   which means the ring has to be the colour of whatever they sit *on*, hence
   the override inside a filled chip. */
.stack { display: inline-flex; align-items: center; flex: 0 0 auto; }
.stack .avatar { box-shadow: 0 0 0 2px var(--surface); }
.stack .avatar + .avatar { margin-left: -8px; }
.card .stack .avatar,
.chip.person .stack .avatar { box-shadow: 0 0 0 2px var(--brand-soft); }
.chip.person.me .stack .avatar { box-shadow: 0 0 0 2px var(--brand); }

/* ------------------------------------------------------------------ crew */
/* The people on a task. Chips rather than a <select multiple>: a native
   multi-select on a phone is a small scrolling box whose selection state is
   invisible until you scroll back to it, and "who is on this job" is the one
   thing this control exists to show. Add from the dropdown, remove with the ×,
   and every change saves on its own — a crew list that has to be confirmed
   afterwards is a crew list that gets left half-edited. */

.crew-edit { display: flex; flex-direction: column; gap: 10px; }
.crew { display: flex; flex-wrap: wrap; gap: 8px; }

.crew-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px 4px 4px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  max-width: 100%;
}

.crew-name {
  font-size: 13.5px;
  color: var(--ink);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The first name is the one the board card and the Gantt row show when there
   is room for only one, so it is worth saying which that is. */
.crew-lead {
  font-size: 10.5px;
  font-weight: 700;
  color: var(--brand-text);
  background: var(--brand-soft);
  border-radius: 999px;
  padding: 1px 7px;
  flex: 0 0 auto;
}

/* ผู้ประสานงาน, on the project form. Quieter than .crew-lead on purpose: the
   badge that matters is the one on the person who answers for the job, and two
   loud pills per chip would make the list harder to read than no roles at all.
   It is a <button> for everyone who may edit the list - pressing it hands the
   ผู้รับผิดชอบสูงสุด badge to that person - and a plain <span> for everyone
   else, which is why the hover and the press states are scoped to .crew-promote. */
.crew-role {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--ink-soft);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 1px 7px;
  flex: 0 0 auto;
  white-space: nowrap;
}

.crew-promote { cursor: pointer; }
.crew-promote:active { color: var(--brand-text); background: var(--brand-soft); }

.crew-x {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: inline-grid;
  place-items: center;
  color: var(--ink-soft);
  background: transparent;
  flex: 0 0 auto;
}

.crew-x:active { background: var(--danger-soft); color: var(--danger-text); }

.crew-none {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13.5px;
  color: var(--ink-soft);
  padding: 5px 0;
}

.crew-add { margin-bottom: 0; }

/* -------------------------------------------------------------- timeline */
/* Flat and newest-first, with no stage headings: grouping by stage read as if
   each note belonged to the column the task was standing in, so moving a task
   made its whole history appear to move with it. The stage is still on every
   row in `dataset`, for checking — just not on screen. */

.timeline { display: flex; flex-direction: column; }

.tl-item {
  position: relative;
  padding: 0 0 16px 18px;
  border-left: 2px solid var(--line);
  font-size: 14px;
  color: var(--ink);
}

.tl-item:last-child { border-left-color: transparent; padding-bottom: 0; }

.tl-item::before {
  content: "";
  position: absolute;
  left: -6px;
  top: 6px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid var(--surface);
}

.tl-move::before { background: var(--line); }

.tl-when {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
  font-size: 11.5px;
  color: var(--ink-soft);
}

/* Who wrote it, then when. That order on purpose: scanning a timeline for
   "what did the site say" is scanning for a person, and the timestamps are all
   the same shape so they make poor landmarks. */
.tl-who { font-weight: 600; font-size: 13px; color: var(--ink); }
.tl-time { display: inline-flex; align-items: center; gap: 4px; }
.tl-time > .bx { font-size: 13px; opacity: .7; }

.tl-body { margin: 4px 0 6px; white-space: pre-wrap; word-break: break-word; }
/* Links pasted into a post or a job's description. A long Drive address has
   no spaces to break at, so it is allowed to break anywhere. */
.tl-body a, .task-desc a { color: var(--brand); text-decoration: underline; overflow-wrap: anywhere; word-break: break-all; }

.tl-body.muted {
  color: var(--ink-soft);
  font-size: 13px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* The headline over an entry that is about an act rather than about its own
   words — "ทำเสร็จแล้ว" above the note and the photos that came with it. Set
   in the ink of a heading, because that is what it is. */
.tl-said {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 4px 0 2px;
  font-weight: 600;
  font-size: 13px;
  color: var(--ink);
}

.tl-said > .bx { color: var(--brand); font-size: 15px; }

/* The dot lines up with the avatar's middle, not with the first line of text. */
.tl-item { padding-left: 22px; }
.tl-item::before { top: 10px; }

/* ซ่อน / ลบ, on the entry itself.

   Pushed to the far end of the who-and-when line rather than given a row of
   their own: they are wanted rarely, by one person in four, and a report is
   not improved by two buttons sitting under it. Sized down from the standard
   tap target because they sit inside a 11.5px line — but not below 32px,
   which is as small as a thumb on a site can be asked to hit. */
.tl-tools { margin-left: auto; display: inline-flex; gap: 4px; }

.tl-tool {
  min-width: 32px;
  width: 32px;
  height: 32px;
  padding: 0;
  font-size: 14px;
  background: transparent;
  border-color: transparent;
  color: var(--ink-soft);
}

.tl-tool:hover { background: var(--surface-2); border-color: var(--line); }
.tl-tool.danger:hover { color: var(--danger-text); background: var(--danger-soft); }

/* An entry a หัวหน้างาน has taken down. Still legible — the point of keeping
   it is that somebody can read what was removed — but plainly not part of the
   feed any more, and never mistakable for a live report. */
.tl-off { opacity: .62; }
.tl-off:hover, .tl-off:focus-within { opacity: 1; }
.tl-off::before { background: var(--line); }

/* A rounded rectangle, not a pill: on a phone this sentence wraps to two
   lines more often than not, and a 999px radius on a two-line box reads as a
   button somebody forgot to finish. The icon sits with the first line. */
.tl-off-note {
  display: inline-flex;
  align-items: flex-start;
  gap: 6px;
  margin: 4px 0 6px;
  padding: 5px 10px;
  border-radius: var(--radius);
  background: var(--muted-soft);
  color: var(--muted-text);
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.45;
}

.tl-off-note > .bx { font-size: 13px; flex: 0 0 auto; margin-top: 1px; }

/* The note being deleted, quoted back before the button is pressed. */
.tl-quote {
  margin: 10px 0;
  padding: 8px 12px;
  border-left: 3px solid var(--line);
  background: var(--surface-2);
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--ink-2);
  font-size: 13px;
  white-space: pre-wrap;
  word-break: break-word;
}

.err {
  background: var(--danger-soft);
  color: var(--danger-text);
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: 13px;
  margin-bottom: 12px;
}

/* ------------------------------------------------------------- login view */

/* Two rows since the version line joined the panel: `align-content: center`
   as well as `place-items: center`, or the default stretch splits the free
   height between them and leaves the version floating in the lower half
   rather than sitting under the box it belongs to. */
#login {
  height: 100%;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 12px;
  padding: 20px;
}

/* Quiet on purpose. It is here to be found when somebody asks "is the fix
   live yet?", not to be read on the way past. */
.appver {
  margin: 0;
  font-size: 11px;
  color: var(--ink-soft);
  opacity: .75;
  text-align: center;
}

#login .panel {
  border-radius: 12px;
  max-width: 380px;
  box-shadow: var(--shadow);
}

.brand { text-align: center; margin-bottom: 18px; }
.brand h1 { font-size: 20px; margin: 8px 0 2px; }
.brand p { color: var(--ink-soft); font-size: 13px; margin: 0; }

.divider {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--ink-soft);
  font-size: 12px;
  margin: 16px 0;
}

.divider::before, .divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

.btn.line { background: #06c755; }

/* ----------------------------------------------------------------- toast */

#toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  background: #2b2c40;
  color: #fff;
  padding: 10px 16px;
  border-radius: 999px;
  font-size: 13px;
  z-index: 60;
  max-width: 90vw;
  text-align: center;
}

.spin {
  display: inline-block;
  width: 16px;
  height: 16px;
  border: 2px solid rgba(255, 255, 255, .4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin .7s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* --------------------------------------------------------- readability */
/* Added with the HR skin: the things that were only ever clear to whoever
   built the page — what a screen is for, what a level may do, why a list is
   empty — now say so in words. */

h1, h2, h3, .panel h2, .card-title { color: var(--ink); }
.panel h3 { color: var(--ink-soft); }

/* One line of plain Thai under a heading, explaining what the section is. */
.hint {
  color: var(--ink-soft);
  font-size: 12.5px;
  line-height: 1.5;
  margin: -4px 0 12px;
}

/* An empty list says what would be here and how to put something in it. */
.empty b { display: block; color: var(--ink); font-size: 14px; margin-bottom: 4px; }

/* ------------------------------------------------------------- AI CODE */
/* The latest Claude.ai sign-in links. The heading and รีเฟรช share the top
   line so refreshing is where the eye already is when a new link arrives. */
.aicode-head { display: flex; align-items: center; gap: 10px; }
.aicode-head .sec-head { margin: 0; flex: 1 1 auto; }
.aicode-refresh { flex: 0 0 auto; min-height: 36px; }

.aicode-body { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }

.aicode-row {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.aicode-when {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--ink-soft);
}

/* The link itself, shown so it can be read and long-pressed, but never allowed
   to push the sheet wide - it wraps inside its own box. */
.aicode-link {
  font-size: 12px;
  color: var(--ink-2);
  word-break: break-all;
  line-height: 1.4;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.aicode-link.none { color: var(--ink-soft); font-style: italic; font-family: inherit; }

.aicode-actions { margin-top: 0; flex-wrap: wrap; }
.aicode-actions .btn { flex: 0 0 auto; }

/* A note pinned above a shared list: this data belongs to all four companies. */
.banner {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  background: var(--brand-soft);
  color: var(--brand-text);
  border-radius: var(--radius);
  padding: 9px 12px;
  font-size: 12.5px;
  line-height: 1.5;
  margin-bottom: 12px;
}

.banner i { font-style: normal; flex: 0 0 auto; }

/* The same shape in the colour of a thing you should read twice. */
.banner.warn { background: var(--warn-soft); color: var(--warn-text); }
/* The settled, working state — แจ้งเตือนเข้า Teams uses it to say the channel
   is connected. Same shape as .warn so the two read as one control saying two
   things, rather than as two different kinds of notice. */
.banner.ok { background: var(--ok-soft); color: var(--ok-text); }

/* "เพิ่มโดย บริษัท ก" on a row of the shared customer list. */
.tag.owner { background: var(--muted-soft); color: var(--muted-text); }
.tag.owner.mine { background: var(--brand-soft); color: var(--brand-text); }

/* The account sheet: name, level, and what the level is allowed to do. */
.acct { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }

.acct .avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--brand-soft);
  color: var(--brand-text);
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: 18px;
  flex: 0 0 auto;
  overflow: hidden;
}

.acct .avatar img { width: 100%; height: 100%; object-fit: cover; }
.acct .who-name { font-size: 17px; font-weight: 600; color: var(--ink); }
.acct .who-sub { font-size: 12.5px; color: var(--ink-soft); }

/* ------------------------------------------------------ the two steps */
/* Before a board can mean anything, two questions have to be answered: which
   company the work belongs to, and which job inside it. Both are cards rather
   than dropdowns — what decides the answer (who the job is for, whether it is
   running, when it is due) does not fit in a <select>, and on a phone a long
   option list is the worst of both. */

.pick-title { margin: 4px 0 2px; font-size: 19px; color: var(--ink); }
.pick-sub { margin: 0 0 14px; font-size: 13px; color: var(--ink-soft); }

.pick-search {
  width: 100%;
  min-height: var(--tap);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  background: var(--surface);
  margin-bottom: 12px;
}

.pick-list { display: flex; flex-direction: column; gap: 10px; }

/* On a computer the job list is a grid. A single 1,400px-wide column of 62px
   cards is a phone layout that was never re-measured for a desk: eight jobs
   became a page of scrolling with the right two-thirds of every row empty.
   Two up on a tablet, three on a desktop — the card itself is unchanged, it
   just stops being asked to be a full-width row. */
@media (min-width: 700px) {
  #projpick .pick-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: stretch;
  }
}

@media (min-width: 1080px) {
  #projpick .pick-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* A full-width primary button is right under a thumb and absurd at 1,280px —
   สร้างโปรเจกต์ใหม่ stretched the whole way across the grid. It keeps its own
   width on a wide screen and stays where the eye leaves the last card. */
@media (min-width: 700px) {
  #projpick .actions { justify-content: flex-start; }
  #projpick .actions .btn { flex: 0 0 auto; }
  #projpick .pick-search { max-width: 420px; }
}

/* Search and the การ์ด / รายการ switch share one line; the switch keeps its
   own width at the right end, and the search takes whatever is left. */
.pick-tools { display: flex; align-items: flex-start; gap: 8px; }
.pick-tools .pick-search { flex: 1 1 auto; min-width: 0; }

.pick-layout {
  display: inline-flex;
  flex: 0 0 auto;
  margin-left: auto;
  margin-bottom: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

.pick-layout button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--tap);
  padding: 0 12px;
  border: 0;
  background: transparent;
  color: var(--ink-2);
  font-size: 13px;
}

.pick-layout button + button { border-left: 1px solid var(--line); }
.pick-layout button.on { background: var(--brand); color: var(--brand-ink); font-weight: 600; }

/* ทั้งหมด / ภายใน / ภายนอก sits at the left of the row, the layout switch at
   the right. The count is dimmed so the word is what the eye lands on. */
.pick-kind { margin-left: 0; }
.pick-kind button small { font-size: 11px; opacity: .7; font-variant-numeric: tabular-nums; }
.tag.kind-internal { background: var(--warn-soft); color: var(--warn-text); }
.tag.kind-external { background: var(--brand-soft); color: var(--brand-text); }

/* On a phone the switch is two icons: the words would push the search box
   down to a width nobody can type into. */
@media (max-width: 480px) {
  .pick-layout button span { display: none; }
  /* ...except the kind filter, whose words are the whole point. The search
     box drops to a line of its own under the two switches. */
  .pick-kind button span { display: inline; }
  .pick-kind button { padding: 0 10px; }
  .pick-tools { flex-wrap: wrap; }
  .pick-tools .pick-search { order: 3; flex-basis: 100%; }
}

/* The list layout is one table, not a grid of cards - the grid rules above
   would put the table in the first of three columns. */
#projpick .pick-list.as-table { display: flex; }

.proj-table { min-width: 720px; background: var(--surface); }
.proj-table td { vertical-align: middle; }
.proj-table td.nowrap { white-space: nowrap; }
.proj-table tr.current td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
@media (hover: hover) {
  .proj-table tr.rep-click:hover { background: var(--surface-2); }
}

/* ภาพรวมทุกบริษัท, the first card in that list. Tinted rather than white, so
   the one card that is not a job does not have to be read to be told apart. */
.pick-card.pick-all { background: var(--brand-soft); border-color: transparent; }
.pick-card.pick-all:active { background: var(--brand-soft); filter: brightness(.97); }
.pick-card.pick-all .pick-mark { background: var(--brand); color: var(--brand-ink); font-size: 20px; }
.pick-card.pick-all .pick-text b { color: var(--brand-text); }
.pick-card.pick-all .pick-text small,
.pick-card.pick-all .pick-go { color: var(--brand-text); opacity: .8; }

.pick-card {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow);
  min-height: 62px;
}

/* A job card is two decks: the row it always had, and its calendar under it.
   The company cards and ภาพรวมทุกบริษัท keep the single row — nothing to
   measure on either. */
.pick-card.proj { flex-direction: column; align-items: stretch; gap: 8px; }
.pick-card.proj .pick-row { display: flex; align-items: center; gap: 12px; width: 100%; }

.pick-time { display: flex; flex-direction: column; gap: 4px; }

.pick-note {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  font-size: 11.5px;
  color: var(--ink-soft);
  line-height: 1.35;
}

.pick-note .grow { flex: 1 1 auto; }
.pick-note.red .grow { color: var(--danger-text); font-weight: 600; }

.pick-card:active { background: var(--surface-2); }
.pick-card.current { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); }

.pick-mark {
  width: 40px;
  height: 40px;
  flex: 0 0 auto;
  border-radius: var(--radius);
  background: var(--brand-soft);
  color: var(--brand-text);
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: 14px;
}

.pick-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35; }
.pick-text b { color: var(--ink); font-weight: 600; word-break: break-word; }
.pick-text small { color: var(--ink-soft); font-size: 12px; }
.pick-side { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex: 0 0 auto; }
.pick-go { color: var(--ink-soft); font-size: 20px; line-height: 1; }

.pick-side .status,
.proj-table .status {
  font-size: 11px;
  border-radius: 999px;
  padding: 2px 9px;
  background: var(--muted-soft);
  color: var(--muted-text);
  white-space: nowrap;
}

.pick-side .s-active,
.proj-table .s-active { background: var(--ok-soft); color: var(--ok-text); }
.pick-side .s-on_hold,
.proj-table .s-on_hold { background: var(--warn-soft); color: var(--warn-text); }
.pick-side .s-done,
.proj-table .s-done { background: var(--brand-soft); color: var(--brand-text); }
.pick-side .s-cancelled,
.proj-table .s-cancelled { background: var(--danger-soft); color: var(--danger-text); }

/* ------------------------------------------------------- moving a task */
/* A row of every stage in the pipeline, open on the page, invited a mis-tap on
   the way past — and a move is a deliberate act done a handful of times in a
   task's life. One button; the stages are behind it. */

.move-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.move-now { font-size: 13px; color: var(--ink-soft); flex: 1 1 auto; }
.move-btn { flex: 0 0 auto; gap: 8px; }
.move-icon { font-size: 17px; }

.move-list { display: flex; flex-direction: column; gap: 8px; }

.move-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  min-height: 56px;
}

.move-item:active { background: var(--surface-2); }
.move-item.current { background: var(--brand-soft); border-color: var(--brand); }
.move-item:disabled { cursor: default; }
.move-dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }

/* --------------------------------------------------------------- the map */
/* No tiles are drawn here. These are the buttons that hand a point to the
   phone's own map app, and one embed that is fetched only if it is asked
   for — a page opened on mobile data at the roadside owes nobody a map. */

a.btn { text-decoration: none; }

.map-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 4px 0 8px;
}

.map-actions .btn { flex: 1 1 auto; }

.map-preview:empty { display: none; }

.map-preview iframe {
  width: 100%;
  height: 220px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  display: block;
  margin-top: 4px;
}

@media (min-width: 700px) {
  .map-preview iframe { height: 300px; }
}

/* One "งานที่ค้างอยู่จะย้ายไปไหน" row: the old column on the left, the new one
   picked on the right. Stacks on a phone like every other field. */
.map-row > span { color: var(--ink); font-weight: 500; }

/* ------------------------------------------------- ภาพรวมทุกบริษัท และแผนที่ */
/* The group admin's screen and the two maps built out of the morning
   check-in. Everything here reuses the tokens the rest of the page is built
   from, so the four companies' screen is the same product as one board. */

.ov-tabs {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding: 10px 12px 0;
  scrollbar-width: none;
}

.ov-tabs::-webkit-scrollbar { display: none; }

.ov-tabs button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  padding: 8px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
  font-size: 13px;
  min-height: 38px;
}

.ov-tabs button.on {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
  font-weight: 600;
}

.ov-body { padding: 12px; }

/* The four numbers that answer "is anything on fire" before anything is read. */
.ov-totals {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}

.ov-stat {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow);
}

.ov-stat i { font-size: 20px; color: var(--ink-soft); flex: 0 0 auto; }
.ov-stat b { font-size: 18px; color: var(--ink); }
.ov-stat span { font-size: 12px; color: var(--ink-soft); }
.ov-stat.red i, .ov-stat.red b { color: var(--danger); }

.ov-cards { display: flex; flex-direction: column; gap: 10px; }

.ov-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.ov-card.current { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); }

.ov-main {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
  text-align: left;
  padding: 14px;
  background: none;
}

.ov-main:active { background: var(--surface-2); }

.ov-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.ov-text b { color: var(--ink); font-weight: 600; }
.ov-text small { color: var(--ink-soft); font-size: 12px; }

/* How far along the whole company is. Same shape as the Gantt's own bar so
   the two screens read as one measurement. */
.ov-bar {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
  margin: 2px 0;
}

.ov-bar i { display: block; height: 100%; background: var(--brand); }

.ov-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 0 14px 12px;
  border-top: 1px solid var(--line);
  padding-top: 10px;
}

.ov-actions .hint { margin: 0; font-size: 12px; }

/* `.mini` is a 38px square everywhere else — an icon and nothing more. Here it
   carries a word too, so it grows to fit it rather than wrapping "เปิดบอร์ด"
   onto two lines inside a square. */
.ov-actions .mini {
  width: auto;
  gap: 6px;
  padding: 0 12px;
  white-space: nowrap;
  background: var(--brand-soft);
  color: var(--brand-text);
  border-color: transparent;
}

.tag.red { background: var(--danger-soft); color: var(--danger-text); }
.tag.amber { background: var(--warn-soft); color: var(--warn-text); }
.tag.group-admin { background: var(--brand-soft); color: var(--brand-text); }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }

/* The group Gantt scrolls inside itself, unlike the project one which lets
   its whole view slide. Four companies' worth of rows is a tall page, and
   dragging it sideways to find today should not take the section tabs and the
   company filter off the top of the screen with it. */
.ov-body .gantt {
  overflow-x: auto;
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

/* A company header inside the group Gantt: the rows under it are its jobs. */
.g-row.g-group { background: var(--surface-2); }
.g-row.g-group .g-name { display: flex; align-items: center; gap: 6px; color: var(--ink); }
.g-row.g-group .g-name b { font-size: 13px; }

.g-bar.inferred {
  background: repeating-linear-gradient(
    45deg, var(--ink-soft), var(--ink-soft) 4px, transparent 4px, transparent 8px);
}

.g-bar.late { outline: 1px solid var(--danger); }
.g-legend i.inferred {
  background: repeating-linear-gradient(
    45deg, var(--ink-soft), var(--ink-soft) 3px, transparent 3px, transparent 6px);
}

/* --------------------------------------------------------------- work map */

.map-head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

.map-head .grow { flex: 1 1 auto; }

.map-stat {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}

.map-stat b { color: var(--ink); font-size: 16px; }
.map-stat span { color: var(--ink-soft); font-size: 11px; }

.day-pick {
  min-height: 38px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
}

.map-canvas {
  height: 320px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-2);
  margin-bottom: 12px;
}

.map-canvas:empty { display: none; }

@media (min-width: 700px) {
  .map-canvas { height: 460px; }
}

/* Leaflet paints its own white panels; these keep the popup and the controls
   in the page's own palette, dark mode included. */
.map-canvas .leaflet-container { background: var(--surface-2); font: inherit; }
.map-canvas .leaflet-popup-content-wrapper,
.map-canvas .leaflet-popup-tip {
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
}
.map-canvas .leaflet-popup-content { margin: 10px 12px; font-size: 13px; }
.map-canvas .leaflet-control-attribution { background: var(--surface); color: var(--ink-soft); }
.map-canvas .leaflet-control-attribution a { color: var(--ink-2); }

/* The pin: how many people are standing there. */
.site-marker .pin {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-ink);
  font-weight: 700;
  font-size: 13px;
  border: 2px solid var(--surface);
  box-shadow: var(--shadow);
}

/* Nobody is there any more — they checked in here and moved on. */
.site-marker .pin.gone { background: var(--ink-soft); }

.site-pop b { color: var(--ink); }
.site-pop .sub { color: var(--ink-soft); font-size: 12px; margin: 2px 0 6px; }
.site-pop .sub.warn { color: var(--warn-text); }
.site-pop .pop-row { display: flex; flex-direction: column; margin-bottom: 6px; }
.site-pop .pop-row span { color: var(--ink-soft); font-size: 12px; }
.site-pop .pop-row i { color: var(--ink-soft); font-size: 11px; }

.site-list { display: flex; flex-direction: column; gap: 8px; }

.site-card {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow);
}

.site-card:active { background: var(--surface-2); }
.site-card.unpinned { border-style: dashed; box-shadow: none; }

.site-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.site-text b { color: var(--ink); font-weight: 600; }
.site-text small { color: var(--ink-soft); font-size: 12px; }

.site-count {
  flex: 0 0 auto;
  min-width: 28px;
  height: 28px;
  border-radius: 999px;
  background: var(--brand-soft);
  color: var(--brand-text);
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: 13px;
}

.site-card .crew-chip { margin-bottom: 0; font-size: 12px; }
.site-card .crew-chip.gone { opacity: .55; }

.absent { margin-top: 12px; }
.absent summary { color: var(--ink-soft); font-size: 13px; padding: 6px 0; cursor: pointer; }
.absent .crew-chip { margin-bottom: 0; font-size: 12px; }

/* One step back inside the check-in sheet. `.mini` is a square by default and
   this one carries a sentence, so it grows to hold it. */
.mini.back-step {
  width: auto;
  gap: 6px;
  padding: 0 12px;
  height: 34px;
  white-space: nowrap;
  margin-bottom: 6px;
}

/* The morning sheet's warning under a project with no pin. */
.pick-text small.warn { color: var(--warn-text); }

/* The colour of a stage, as a dot, on the read-only list of a project's own
   columns under ผู้ดูแล. */
.stage-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  flex: 0 0 auto;
  margin-top: 4px;
}

/* The customer link on the task page. A read-only field rather than text: on
   a phone, selecting a long URL out of a paragraph is a fight, and this way
   one tap selects the whole thing even when the copy button is unavailable. */
.share-link {
  width: 100%;
  margin-top: 4px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: 12.5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  text-overflow: ellipsis;
}

/* A quiet way to say a destructive thing. `.btn.danger` is the solid red of a
   button you are meant to hesitate over — closing a share is not that: it is
   reversible in the sense that matters (share again), and a full-width red bar
   under the link reads as an error on the page rather than an action. */
.btn.ghost.danger {
  background: var(--surface);
  color: var(--danger-text);
  border-color: var(--danger-soft);
  box-shadow: none;
}

/* ------------------------------------------------------------------ money */
/* Everything that shows or takes a baht figure. Two rules run through the
   lot: a number is right-aligned wherever it sits in a column with other
   numbers (so the ones and the thousands line up and a wrong order of
   magnitude is visible without reading), and tabular figures are used so the
   columns do not shimmy as the digits change under a poll. */

.moneyfield {
  display: flex;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

.moneyfield .money-sign {
  flex: 0 0 auto;
  padding: 0 8px 0 10px;
  color: var(--ink-soft);
  font-size: 14px;
}

.moneyfield .money-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  padding: 10px 10px 10px 0;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.moneyfield .money-input:focus { outline: none; }
.moneyfield:focus-within { border-color: var(--brand); }
.moneyfield.bad { border-color: var(--danger); background: var(--danger-soft); }

/* งบและค่าใช้จ่าย on the task page. */
.money-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.money-head .sec-head { margin: 0; flex: 1 1 auto; }

.money-block dd { font-variant-numeric: tabular-nums; }
.money-block dd.red { color: var(--danger-text); font-weight: 600; }

.money-meter { margin: 8px 0 4px; }

/* One meter, worn by two things: `.money-track` on a budget, `.time-track` on
   a calendar. Same instrument, so a card and a report read alike. */
.money-track,
.time-track {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}

.money-track i,
.time-track i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--brand);
}

.time-track { height: 6px; }
.time-track.amber i { background: var(--warn); }
.time-track.red i { background: var(--danger); }
.time-track.ok i { background: var(--ok); }

/* The bar stops at full and changes colour; the figure beside it keeps
   counting. A 131% bar drawn 131% wide breaks its row, and one clamped
   silently at 100% is indistinguishable from spending the budget exactly —
   which is the one distinction this whole screen exists to make. */
.money-track.amber i { background: var(--warn); }
.money-track.red i { background: var(--danger); }

.money-pct {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  color: var(--ink-soft);
}

.money-pct.amber { color: var(--warn-text); }
.money-pct.red { color: var(--danger-text); font-weight: 600; }

/* ------------------------------------------------------------- the ledger */

.ledger {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  border-top: 1px solid var(--line);
}

.ledger-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.ledger-cat {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--ink-soft);
  font-size: 15px;
}

.ledger-text { flex: 1 1 auto; min-width: 0; }
.ledger-text b { display: block; font-size: 14px; color: var(--ink); }
.ledger-text small {
  display: block;
  font-size: 11.5px;
  color: var(--ink-soft);
  overflow-wrap: anywhere;
}
.ledger-text small.muted { font-style: italic; }

.ledger-amt {
  flex: 0 0 auto;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  padding-top: 2px;
}

.ledger-row .tl-tools { flex: 0 0 auto; }

/* วางแผนไว้ -> ยืนยันยอด. A plan is listed but not counted, so its amount is
   drawn lighter and struck through nothing: it is a number nobody has paid. */
.money-block dd.amber { color: var(--warn-text); font-weight: 600; }
.ledger-row.plan .ledger-amt { color: var(--warn-text); font-style: italic; }
.ledger-state {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 4px;
}
.ledger-state .chip { font-size: 11.5px; }
.ledger-state .tl-tool { width: 26px; height: 26px; }
.ledger-state .mini.confirm-amt {
  height: 28px;
  background: var(--ok-soft);
  color: var(--ok-text);
  font-weight: 600;
}

/* The ฿ badge - kanban card, job card, job-list row. Green is money that is
   confirmed; amber with "แผน" means lines are still waiting for ยืนยันยอด. */
.baht {
  display: inline-grid;
  place-items: center;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  background: var(--ok-text);
  color: var(--ok-soft);
  flex: 0 0 auto;
}
.chip.money { background: var(--ok-soft); color: var(--ok-text); font-weight: 600;
  font-variant-numeric: tabular-nums; }
.chip.money.plan { background: var(--warn-soft); color: var(--warn-text); }
.chip.money.plan .baht { background: var(--warn-text); color: var(--warn-soft); }
.chip.money small { font-weight: 500; font-size: 10.5px; }
.mine-note .baht { width: 20px; height: 20px; font-size: 12px; }
.chip.money small { opacity: .85; }

/* Two figures per ledger line: คาดการณ์ over ใช้จริง, each labelled. */
.ledger-amt { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.ledger-fig { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
.ledger-fig small { font-size: 10.5px; font-weight: 500; color: var(--ink-soft); }
.ledger-fig.forecast b { font-size: 12.5px; font-weight: 500; color: var(--ink-soft); }
.ledger-fig.actual b { font-size: 14px; }
.ledger-row.plan .ledger-fig.actual b { color: var(--ink-soft); font-style: normal; }
.ledger-row.plan .ledger-amt { font-style: normal; }
.hint.amber, small.amber, td.amber { color: var(--warn-text); }
td.red, .sum-money b.red { color: var(--danger-text); }

/* ภาพรวมโปรเจกต์ - งบประมาณและค่าใช้จ่าย */
.sum-money { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; min-width: 0; }
.sum-money > *, .sum-money-split > *, .sum-money-bars > * { min-width: 0; }
.sum-money .sum-money-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.sum-money-bars { display: grid; gap: 10px; }
.sum-money-bar .money-track { height: 8px; }
.sum-money-facts { margin: 0; font-size: 12.5px; color: var(--ink-soft); }
.sum-money-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
.sum-money-split h4 { margin: 0 0 6px; font-size: 13px; color: var(--ink-soft); font-weight: 600; }
.sum-money .rep-table td .bx { color: var(--ink-soft); }
.sum-money .rep-table td small { display: block; }
/* Three or four short ฿ columns fit a phone, so these two drop the 520px floor
   the other summary tables keep, and the name column gives way first. */
.sum-money .rep-table.sum-table { min-width: 0; }
.sum-money .rep-table td:first-child,
.sum-money .rep-table th:first-child { min-width: 96px; }
.sum-money .rep-table th, .sum-money .rep-table td { padding-left: 6px; padding-right: 6px; }
@media (min-width: 900px) {
  .sum-money .sum-money-tiles { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .sum-money-bars { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
  .sum-money-split { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); gap: 16px; }
}

/* ------------------------------------------------------------------ รายงาน */

.rep-filter {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  padding: 10px 12px 0;
}

.rep-filter-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  color: var(--ink-soft);
  flex: 0 0 auto;
}

/* Wide enough for the whole of 27/08/2569 with the calendar button beside it.
   A date box that clips its own last digit is worse than one that wraps. */
.rep-filter .datefield { flex: 1 1 126px; min-width: 122px; }
.rep-filter .date-text { min-height: 38px; padding: 8px 34px 8px 9px; font-size: 13px; }
.rep-filter .date-pick { width: 30px; }

.rep-dash { color: var(--ink-soft); flex: 0 0 auto; }

.rep-head { margin-bottom: 10px; }
.rep-head b { display: block; font-size: 16px; color: var(--ink); }
.rep-head small { font-size: 12px; color: var(--ink-soft); }

/* Money tiles stack; the count tiles on the group overview do not.
   `.ov-stat` puts the icon, the figure and the label on one line, which works
   for "12 โปรเจกต์" and falls apart at "฿1.00 ล้าน / มูลค่างาน" — on a 412px
   phone the figure wrapped mid-number and the label wrapped mid-word. The
   label goes above the figure because it is the shorter of the two and because
   four tiles are read as a list of four questions, not four answers. */
.rep-totals { grid-template-columns: repeat(2, minmax(0, 1fr)); }

.rep-totals .ov-stat {
  display: block;
  padding: 10px 12px;
}

.rep-totals .ov-stat > span {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  line-height: 1.3;
  color: var(--ink-soft);
}

.rep-totals .ov-stat i {
  font-size: 14px;
  float: none;
}

.rep-totals .ov-stat b {
  display: block;
  margin-top: 2px;
  font-size: 17px;
  line-height: 1.25;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
.rep-chips { margin: 10px 0; }

.rep-card { margin-bottom: 12px; }
.rep-card .sec-head { margin-top: 0; }

/* Bars for "แยกตามหมวด" and "แยกตามเดือน". */
.rep-bars { list-style: none; margin: 4px 0 0; padding: 0; }
.rep-bars li { margin-bottom: 10px; }

.rep-bar-head {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 13px;
  margin-bottom: 4px;
}

.rep-bar-head i { color: var(--ink-soft); font-size: 14px; align-self: center; }
.rep-bar-label { flex: 1 1 auto; min-width: 0; color: var(--ink-2); }
.rep-bar-head b { color: var(--ink); font-variant-numeric: tabular-nums; }
.rep-bar-head small { color: var(--ink-soft); font-size: 11.5px; flex: 0 0 auto; }

.rep-bar-track {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}

.rep-bar-track i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--brand);
}

/* A table on a phone. It scrolls sideways inside its own box rather than
   pushing the page wide — a report whose body scrolls horizontally takes the
   tab strip and the header with it. */
.rep-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0 -12px;
  padding: 0 12px;
}

.rep-table {
  width: 100%;
  min-width: 460px;
  border-collapse: collapse;
  font-size: 13px;
}

/* The name column gets a floor and the number columns get a ceiling.
   Without it the three ฿ columns take the width they would like and the job's
   name — the only thing in the row that says which job it is — is folded into
   a four-line column two words wide. The table scrolls sideways inside
   `.rep-scroll` instead, which is the trade a phone should make here: a name
   you can read and a swipe to see the last column. */
.rep-table td:first-child,
.rep-table th:first-child { min-width: 148px; }
.rep-table .num { width: 1%; }

.rep-table th {
  text-align: left;
  font-weight: 500;
  font-size: 11.5px;
  color: var(--ink-soft);
  padding: 6px 8px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.rep-table td {
  padding: 8px;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

.rep-table td b { display: block; color: var(--ink); font-size: 13px; }
.rep-table td small { display: block; color: var(--ink-soft); font-size: 11px; }

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

.rep-table td.num { color: var(--ink-2); }
.rep-table td.num.red { color: var(--danger-text); font-weight: 600; }

/* The whole row is the target. A tap anywhere on a line of numbers opens the
   job those numbers are about — which is the next thing anybody does after
   finding the row that is red. */
.rep-table tr.rep-click { cursor: pointer; }
.rep-table tr.rep-click:active { background: var(--surface-2); }
.rep-table tr.rep-click.red td:first-child { box-shadow: inset 3px 0 0 var(--danger); }

/* ------------------------------------------ นำเข้าแผนงานจาก Excel */

/* The "start here" card on a board with no work on it yet.
   It sits as the first item in the board's horizontal row rather than above
   it, because `.board` is that row — a block dropped in before the columns
   would become a column. Sized like one, so it reads as the card that comes
   before the first stage and scrolls away once the work arrives. */
.board-empty {
  flex: 0 0 min(84vw, 320px);
  align-self: flex-start;
  scroll-snap-align: start;
  border: 1px dashed var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 14px 12px;
}

.board-empty .actions { margin-top: 12px; }
.board-empty .btn { flex: 0 1 auto; }

/* The three steps on the first import screen. Numbered, because they happen
   in three different places — this app, a spreadsheet, and whatever AI the
   office uses — and the order is the only thing holding them together. */
.steps {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  counter-reset: step;
}

.steps li {
  position: relative;
  padding: 0 0 16px 14px;
  border-left: 2px solid var(--line);
  margin-left: 6px;
}

.steps li:last-child { border-left-color: transparent; padding-bottom: 0; }

.steps li::before {
  content: "";
  position: absolute;
  left: -7px;
  top: 4px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--brand);
}

.steps b { display: block; font-size: 14px; color: var(--ink); margin-bottom: 2px; }
.steps li > span {
  display: block;
  font-size: 12.5px;
  color: var(--ink-soft);
  line-height: 1.55;
  margin-bottom: 8px;
}
.steps .actions { margin-top: 0; flex-wrap: wrap; }
.steps .hint { margin-top: 6px; }
.steps .btn { margin-top: 2px; }

/* The prompt itself, on screen. Read-only and monospaced: it is 5 KB of
   instructions that must be copied intact, and one tap in a textarea selects
   the lot — which on a phone is the only way to get it out by hand. */
.prompt-box {
  width: 100%;
  min-height: 42vh;
  margin-top: 4px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--ink-2);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  line-height: 1.6;
  resize: vertical;
}

/* Two of them in one sheet — the Thai to read and the English to paste back
   (แปลในหน้านี้). At 42vh each the second one is below the fold on every
   phone, and a box you have to scroll to find is a box that gets left empty. */
.prompt-box.pair { min-height: 22vh; }

/* The paste box says what it just read, live, under itself: how many lines
   matched, how many keys it did not recognise, what is still missing. Red only
   when the paste cannot be read at all — a partial answer is a normal state
   here, not an error. */
.hint.warn { color: var(--warn-text); }

/* ตรวจก่อนสร้าง. */
.import-preview .rep-card { margin-bottom: 10px; }
.import-preview .rep-totals { margin-bottom: 12px; }
.import-preview .ov-stat b { font-size: 15px; }

.import-problems { margin-bottom: 12px; }
.import-problems ul { margin: 6px 0 0; padding-left: 18px; }
.import-problems li { margin-bottom: 4px; line-height: 1.5; }
.import-problems b { display: block; }

.import-stages { list-style: none; margin: 4px 0 0; padding: 0; }

.import-stage {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}

.import-stage:last-child { border-bottom: 0; }
.import-stage .stage-dot { margin-top: 5px; flex: 0 0 auto; }
.import-stage-text { flex: 1 1 auto; min-width: 0; }
.import-stage-text b { display: block; font-size: 13.5px; color: var(--ink); }
.import-stage-text .chips { margin-top: 4px; }

.import-stage-count {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  padding-top: 3px;
}

/* A preview table can run to hundreds of rows. It scrolls inside its own box
   rather than making the dialog taller than the phone. */
.import-table { min-width: 340px; }
.import-preview .rep-scroll { max-height: 46vh; overflow-y: auto; }

.tag.ok { background: var(--ok-soft); color: var(--ok-text); }

/* The export offer, under the three import steps. Separated by a rule rather
   than sat beside them: it is the answer to a different question — this board
   already exists and needs editing — and putting it in the numbered list would
   make it look like step four of getting a plan in. */
.import-export {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

.import-export b { display: block; font-size: 14px; color: var(--ink); margin-bottom: 2px; }
.import-export > span {
  display: block;
  font-size: 12.5px;
  color: var(--ink-soft);
  line-height: 1.55;
  margin-bottom: 8px;
}

/* A row in the preview that is in the file but changes nothing. Dimmed rather
   than hidden: it is proof the row was read, which is what stops "my task
   vanished from the import" being a question. */
.rep-table tr.quiet td { opacity: .55; }
.import-table .tag { margin-top: 4px; }

/* The old value beside the new one, where an import replaces a figure. Struck
   through rather than merely grey: this is the one place on the preview where
   something is being *lost*, and it should look like it. */
.import-preview .was { text-decoration: line-through; color: var(--ink-soft); }

/* ---------------------------------------------------------------- ย้ายงาน */
/* The destination picker: which job first, then which column of it. Two lists
   rather than a select and a select, because both answers are things you
   recognise rather than remember — a project by its customer, a column by its
   colour. */

.move-projects {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 32vh;
  overflow-y: auto;
  margin-bottom: 4px;
  -webkit-overflow-scrolling: touch;
}

.move-project {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  min-height: 48px;
}

.move-project:active { background: var(--surface-2); }
.move-project.on,
.move-item.on { background: var(--brand-soft); border-color: var(--brand); }

.move-text { flex: 1 1 auto; min-width: 0; }
.move-text b { display: block; font-size: 13.5px; color: var(--ink); }
.move-text small { display: block; font-size: 11.5px; color: var(--ink-soft); }

/* The column the card is already in. Left on screen and disabled rather than
   removed: a list that silently omits where you are is a list you cannot
   orient yourself in. */
.move-item.here { opacity: .5; }
.move-item.here .move-text b { font-weight: 500; }

.move-list { max-height: 30vh; overflow-y: auto; -webkit-overflow-scrolling: touch; }

/* ------------------------------------------------------- ประวัติการใช้งาน */
/* One entry is a line somebody skims, and — where fields moved — a table they
   open. The two live inside a <details>, so the browser owns the open/closed
   state and a row stays open while the list around it is filtered. */

.activity { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }

.act {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.act details > summary {
  list-style: none;                    /* the arrow is the row's own affordance */
  cursor: pointer;
}
.act details > summary::-webkit-details-marker { display: none; }

.act-head {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
}

.act-what { flex: 1; min-width: 0; }

/* The summary wraps rather than truncating: it is a sentence, and half a
   sentence about a deleted project is not worth the tidier row. */
.act-line { color: var(--ink-2); line-height: 1.45; overflow-wrap: anywhere; }
.act-line b { color: var(--ink); }

.act-meta { display: block; margin-top: 2px; color: var(--ink-soft); }

.act-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px; height: 28px;
  border-radius: 50%;
  background: var(--muted-soft);
  color: var(--muted-text);
}
.act-icon.new  { background: var(--ok-soft);     color: var(--ok-text); }
.act-icon.gone { background: var(--danger-soft); color: var(--danger-text); }

/* A row of "field: was → is". Fixed layout so the arrow column stays put down
   the page instead of stepping left and right with the length of each value. */
.act-diff {
  width: calc(100% - 24px);
  border-collapse: collapse;
  margin: 0 12px 10px;
  font-size: .875rem;
}
.act-diff th,
.act-diff td {
  padding: 6px 8px;
  border-top: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
  overflow-wrap: anywhere;
}
.act-diff th { width: 30%; color: var(--ink-soft); font-weight: 500; }
.act-diff .was { color: var(--ink-soft); text-decoration: line-through; }
.act-diff .arrow { width: 1.5em; color: var(--ink-soft); text-align: center; }
.act-diff .now { color: var(--ink); font-weight: 500; }

/* An entry that opens sits slightly proud of one that does not, so a list of
   both does not read as broken. */
.act details[open] { box-shadow: var(--shadow); }

/* Held by the admin drawer while it is open. A page that scrolls behind a
   drawer reads as the drawer having come loose from it. */
body.no-scroll { overflow: hidden; }

/* ------------------------------------------------------------ form screens
   โปรเจกต์ใหม่ · แก้ไขโปรเจกต์ · งานใหม่ — the three long forms.

   They were `.panel` dialogs, which cap at 88vh and scroll inside themselves:
   on a phone that is a form in a box in a page, two scrollers deep, with the
   save button somewhere below the fold of the inner one. The task page had
   already been made a screen for the same reason ("ไทม์ไลน์ยาวเกินกว่าจะอยู่ใน
   กล่องเล็ก ๆ"); these are the rest of that decision.

   Three parts, and only the middle one scrolls: the bar keeps the title and
   the way back on screen, the actions stay under the thumb. */

#form.view {
  padding: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.formbar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
  padding: 8px 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.formbar-title {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* How wide the form's column is. One variable because two things have to
   agree on it — the fields and the button row under them — and they are not
   siblings, so nothing else would keep them in step. */
#form { --form-w: 560px; }

.form-body {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 16px;
  /* The form is the width of a form, not of a desktop. Same 560px the dialog
     it replaces used, so nothing inside it had to be re-measured. */
  width: 100%;
  max-width: var(--form-w);
  margin: 0 auto;
}

/* Under the thumb, always — the reason for the whole change is that บันทึก
   used to be below the fold of a box inside a page.

   The bar spans the window because it is the floor of the screen; the buttons
   in it do not, because they belong to the form above. Padding rather than an
   inner wrapper: it needs no extra element, and it collapses to the plain 16px
   gutter the moment the window is narrower than the column. */
.form-actions {
  flex: none;
  display: flex;
  gap: 10px;
  padding: 12px max(16px, calc((100% - var(--form-w)) / 2));
  padding-bottom: max(12px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  background: var(--surface);
}

.form-actions .btn { flex: 1 1 auto; justify-content: center; }
.form-actions .btn.ghost { flex: 0 1 auto; }

/* --- a form on a computer ---------------------------------------------- */
/* One field per line is the only thing that fits on a phone. The same column
   at 1,440px is a 560px ribbon down the middle of an empty screen, and twice
   as much scrolling as the window needs — โปรเจกต์ใหม่ was 1,366px of form in
   a 770px viewport with 880px of nothing either side of it.

   The rule reads backwards on purpose: *everything* spans both columns, and
   only a plain labelled input opts back into the flow. A section heading, a
   hint, a `.row` that is already a pair, the map block, the crew editor — none
   of those is half a line wide. Listing what may pair is a short list that
   stays true; listing what may not is a long one that the next field group
   added to a form would quietly fall off the end of. */
@media (min-width: 1024px) {
  #form { --form-w: 900px; }

  .form-fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 20px;
  }

  .form-fields > * { grid-column: 1 / -1; }
  /* The two that are half a line wide: a plain labelled input, and a select
     with its own hint and ＋ button kept together underneath it. */
  .form-fields > label.field,
  .form-fields > .field-group { grid-column: auto; }

  /* …and back out again for the two that are never half a line: a `wide` field
     (the thing's own name, where what follows is already a pair, so it would
     sit alone with an empty column beside it) and anything holding a textarea,
     which is a paragraph and reads badly at half width. */
  .form-fields > label.field.wide,
  .form-fields > label.field:has(textarea) { grid-column: 1 / -1; }

  /* A 900px-wide primary button is the same mistake as a 1,327px one, just
     smaller. With room to spare the pair sits at the end of the row at its own
     size, the way a form's footer does on every other desktop app. */
  .form-actions { justify-content: flex-end; }
  .form-actions .btn { flex: 0 0 auto; min-width: 148px; }
}

/* A hint with the way out of it beside it: "ใช้ลูกค้าร่วมกันทุกบริษัทในเครือ"
   next to ＋ เพิ่มลูกค้าใหม่. Wraps rather than squeezes, so on a narrow phone
   the button drops under the sentence instead of crushing it to two words. */
.field-aside {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: -4px 0 12px;
}

.field-aside .hint { flex: 1 1 140px; margin: 0; }
.field-aside .btn { flex: 0 0 auto; }

/* ------------------------------------------------------------ สรุปรายวัน */
/* The date controls sit on one line at every width: two arrows that stay the
   size of a thumb, and the picker taking whatever is left. */
.day-nav { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.day-nav input[type="date"] { flex: 1 1 auto; min-width: 0; }
.day-nav .btn { flex: 0 0 auto; }

.day-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.day-chips.company { margin: 0 0 12px; }

.day-person {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  margin-bottom: 10px;
}

.day-person > summary { list-style: none; cursor: pointer; }
.day-person > summary::-webkit-details-marker { display: none; }
.day-person[open] > summary .sum-caret { transform: rotate(180deg); }

.day-who { display: flex; align-items: center; gap: 10px; padding: 12px; }
.day-name { flex: 1 1 auto; min-width: 0; }
.day-name b { display: block; color: var(--ink); overflow-wrap: anywhere; }
.day-name small { color: var(--ink-soft); font-size: 12px; }

/* The figure is the point of the closed row, so it is the only thing on it
   allowed to be large. */
.day-count { flex: 0 0 auto; }
.day-count b { font-size: 20px; color: var(--ink); font-variant-numeric: tabular-nums; }
.day-person > summary .sum-caret { flex: 0 0 auto; color: var(--ink-soft); }

.day-entries {
  list-style: none;
  margin: 0;
  padding: 4px 12px 8px;
  border-top: 1px solid var(--line);
}

.day-entry { display: flex; gap: 10px; padding: 7px 0; }
.day-entry + .day-entry { border-top: 1px dashed var(--line); }
.day-at {
  flex: 0 0 40px;
  color: var(--ink-soft);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  padding-top: 1px;
}
.day-what { flex: 1 1 auto; min-width: 0; color: var(--ink-2); overflow-wrap: anywhere; }
.day-what small { display: block; color: var(--ink-soft); font-size: 11px; }
.day-what.new { color: var(--ok-text); }
.day-what.gone { color: var(--danger-text); }
.day-entry.more .day-what { color: var(--ink-soft); font-size: 12px; }

/* The date controls stop growing on a wide screen. A date picker stretched
   across 900px is a target nobody needs to be that big, and the numbers below
   it are what the width is for. */
@media (min-width: 1024px) {
  .day-nav { max-width: 520px; }
  /* Four figures on one line rather than a 2x2 block of very wide tiles. The
     numbers are meant to be read together — that is what makes them a
     summary — and a tile 600px wide holding one integer is mostly air. */
  .day-summary .ov-totals { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ------------------------------------------------- ต่อ AI กับ PM (MCP) */

/* A checkbox that belongs beside a label rather than above one. `.field`
 * stacks its span over its control, which is right for every input except
 * this: a tick needs its words next to it or it reads as a heading with an
 * orphan box under it. */
.field.check {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.field.check input { width: 20px; height: 20px; flex: 0 0 auto; }

/* Two columns on a desk, one on a phone. `auto-fill` rather than a media
 * query because this grid appears inside a sheet *and* inside the admin
 * column, which are different widths at the same screen size. */
.tool-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 6px;
  margin-top: 6px;
}
.tool-pick {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--surface);
  min-width: 0;
}
.tool-pick input { width: 18px; height: 18px; flex: 0 0 auto; }
.tool-pick span { display: flex; flex-direction: column; min-width: 0; }
.tool-pick b { font-weight: 600; }
/* The tool's real name under its label: it is what appears in a refusal in
 * somebody's chat window, so this is where they learn to recognise it. */
.tool-pick code { color: var(--ink-soft); font-size: 11px; }
.tool-pick input:disabled { opacity: .45; }
/* A write tool is marked, not hidden. The line down the side is the only
 * difference between a box that answers questions and one that changes the
 * board, and it has to be visible while somebody is ticking quickly. */
.tool-pick.writes { border-left: 3px solid var(--warn); }

.tool-group { margin-top: 10px; }
.tool-group-head { color: var(--ink-soft); font-size: 12px; }
.tool-details { margin-top: 10px; }
.tool-details summary { cursor: pointer; padding: 6px 0; }

.tok-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.tok-row.off { opacity: .55; }
.tok-name { display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap; }
.tok-row .chips { margin: 4px 0; }
.tok-row .sub { font-size: 12px; color: var(--ink-soft); }

.policy-row {
  border: 1px solid var(--line);
  border-radius: 11px;
  padding: 10px 12px;
  margin-top: 8px;
  background: var(--surface);
}
.policy-row summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.policy-row summary .sub { color: var(--ink-soft); font-size: 12px; }

/* "แก้ไว้" beside a tick whose state is not the role's default. A word, not a
 * colour: the row already carries a count, and this is the one place a reader
 * asks "which ones, though". */
.tool-mark {
  margin-left: auto;
  font-style: normal;
  font-size: 11px;
  color: var(--warn-text);
  background: var(--warn-soft);
  border-radius: 6px;
  padding: 1px 6px;
  flex: 0 0 auto;
}
.tool-mark:empty { display: none; }

/* The QR, sized so a phone camera can read it off a laptop screen without
 * anybody leaning in. White plate on purpose in both themes: a dark-mode QR
 * with inverted colours is readable by some scanners and not others, and the
 * failure looks like a broken feature rather than a contrast problem. */
.qr-box {
  background: #fff;
  border-radius: 12px;
  padding: 12px;
  margin: 10px auto;
  width: fit-content;
  max-width: 100%;
}
.qr-box svg { display: block; width: 216px; height: 216px; max-width: 100%; }

/* ---------------------------------------------------------- ตรวจการส่งงาน */
/* A reading column on a wide screen: the rows are sentences, not a table. */
.wl { max-width: 900px; margin: 0 auto; }
.wl-quick { display: flex; flex-wrap: wrap; gap: 6px; margin: -4px 0 12px; }
.wl-kind { margin: 0 0 12px; margin-left: 0; }
/* The layout switch drops its words on a phone; these buttons are only words. */
.wl-kind button span { display: inline !important; }
.wl-people { margin: 12px 0 4px; }
.wl-people .chip { display: inline-flex; align-items: center; gap: 6px; border: 1px solid transparent;
                   padding: 3px 10px 3px 3px; color: var(--ink-2); cursor: pointer; }
.wl-people .chip b { color: var(--ink); }
.wl-people .chip.on { background: var(--brand-soft); border-color: var(--brand); color: var(--brand-text); }

.wl-day { margin-top: 16px; }
.wl-date { display: flex; align-items: baseline; gap: 8px; font-size: 15px; margin: 0 0 8px;
           position: sticky; top: -12px; z-index: 1; padding: 6px 0; background: var(--bg); }
.wl-date small { font-weight: 400; color: var(--ink-soft); font-size: 12px; }
.wl-list { display: flex; flex-direction: column; gap: 6px; }

.wl-entry {
  display: flex; align-items: flex-start; gap: 10px; width: 100%;
  padding: 10px 12px; text-align: left; font: inherit; color: inherit;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
button.wl-entry { cursor: pointer; }
@media (hover: hover) { button.wl-entry:hover { background: var(--surface-2); } }
.wl-entry.gone { opacity: .7; }
.wl-at { flex: 0 0 auto; width: 42px; padding-top: 4px; font-size: 12px; color: var(--ink-soft);
         font-variant-numeric: tabular-nums; }
.wl-what { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.wl-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.wl-task { color: var(--ink); overflow-wrap: anywhere; }
.wl-meta { color: var(--ink-soft); overflow-wrap: anywhere; }
.wl-body { color: var(--ink-2); font-size: 13px; white-space: pre-line; overflow-wrap: anywhere;
           display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.wl-files { display: flex; gap: 10px; font-size: 12px; color: var(--ink-soft); }
.wl-files span { display: inline-flex; align-items: center; gap: 3px; }
.wl-go { flex: 0 0 auto; align-self: center; color: var(--ink-soft); }
.tag.wl-note { background: var(--brand-soft); color: var(--brand-text); }
.tag.wl-close { background: var(--ok-soft); color: var(--ok-text); }

/* ------------------------------------------------------ ภาพรวมโปรเจกต์
   One job on one screen. A column on a phone; on a wide screen the KPI tiles
   go four across, then three cards, then two, the way a dashboard reads. */

/* The ⏲ beside a job card / at the end of its row. */
.proj-cell { position: relative; min-width: 0; }
.proj-cell .pick-card.proj { padding-right: 58px; height: 100%; }
.proj-summary-btn {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--brand-soft);
  color: var(--brand-text);
  font-size: 20px;
  cursor: pointer;
}
.proj-cell .proj-summary-btn { position: absolute; top: 12px; right: 10px; }
.proj-summary-btn:hover { background: var(--brand); color: var(--brand-ink); }
.proj-table th.icon-col, .proj-table td.icon-col { width: 1%; padding-left: 4px; padding-right: 8px; }

.sum {
  padding: 12px;
  max-width: 1440px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.sum-banner {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: flex-end;
  justify-content: space-between;
  padding: 18px;
  border-radius: var(--radius-lg);
  background: linear-gradient(120deg, #3b3fb8 0%, var(--brand) 60%, #8d8fff 100%);
  color: #fff;
  box-shadow: var(--shadow);
}
.sum-banner-main { flex: 1 1 320px; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.sum-banner h2 { margin: 0; font-size: 22px; line-height: 1.3; color: #fff; word-break: break-word; }
.sum-banner-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.sum-banner .status {
  font-size: 11px; border-radius: 999px; padding: 2px 9px;
  background: rgba(255, 255, 255, .22); color: #fff; font-weight: 600;
}
.sum-banner .tag { background: rgba(255, 255, 255, .92); color: #2a2b66; }
.sum-banner .tag.red { background: #ffe0db; color: #66190c; }
.sum-banner-facts { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 13px; opacity: .95; }
.sum-banner-facts > span { display: inline-flex; align-items: center; gap: 5px; }
.sum-banner .pick-time { max-width: 520px; }
.sum-banner .pick-note, .sum-banner .pick-note.red .grow { color: rgba(255, 255, 255, .9); }
.sum-banner .time-track { background: rgba(255, 255, 255, .25); }
.sum-banner .time-track i { background: #fff; }
.sum-banner .time-track.red i { background: #ffb4a6; }
/* Who runs the job, on the banner. Faces and names rather than a
   comma-separated line: this is the card somebody opens to find out who to
   ring, and a name with a face beside it is found faster than a name in a
   list. The letter says which of the two roles it is - M for
   ผู้รับผิดชอบสูงสุด, C for ผู้ประสานงาน - and the chip's title spells that
   out, because one letter is a reminder, not an explanation.

   Top right, above เปิดบอร์ด/Gantt, which is where it was asked for. On a
   phone the banner wraps, this column goes full width, and the row lands in
   the same place relative to the buttons. */
.sum-people { display: flex; flex-wrap: wrap; gap: 6px; }

.sum-person {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 3px 9px 3px 3px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .14);
  border: 1px solid rgba(255, 255, 255, .28);
  max-width: 100%;
}

.sum-person .avatar {
  background: rgba(255, 255, 255, .92);
  color: #3b3fb8;
}

/* `.me` tints the signed-in person's own face brand-purple everywhere else,
   which on this banner is purple on purple. */
.sum-person .avatar.me { background: #fff; color: var(--brand); }

.sum-person-name {
  font-size: 13px;
  color: #fff;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sum-person-role {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  display: inline-grid;
  place-items: center;
  font-size: 10.5px;
  font-weight: 800;
  line-height: 1;
}

/* Solid for the one person who answers for the job, outlined for everybody
   helping run it - the same weighting the two roles have on the project form. */
.sum-person-role.is-m { background: #fff; color: #3b3fb8; }
.sum-person-role.is-c { background: transparent; color: #fff; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .7); }

/* The right-hand column. `align-self: stretch` beats the banner's own
   `align-items: flex-end`, so the column is as tall as the banner and has a
   top for the people to sit at; `margin-top: auto` on the actions then holds
   the buttons at the bottom whether or not there is anybody above them - a
   `justify-content: space-between` here would float the buttons up to the top
   on a job with nobody on it. */
.sum-banner-side {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
}

.sum-banner-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: auto; }
.sum-banner-actions .btn { background: #fff; color: #3b3fb8; border-color: #fff; }
.sum-banner-actions .btn.ghost { background: transparent; color: #fff; border-color: rgba(255, 255, 255, .6); }

.sum-kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.sum-kpi {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow);
  min-width: 0;
}
.sum-kpi-mark {
  flex: 0 0 auto;
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: var(--radius);
  background: var(--brand-soft); color: var(--brand-text);
  font-size: 22px;
}
.sum-kpi.red .sum-kpi-mark { background: var(--danger-soft); color: var(--danger-text); }
.sum-kpi.ok .sum-kpi-mark { background: var(--ok-soft); color: var(--ok-text); }
.sum-kpi.info .sum-kpi-mark { background: var(--warn-soft); color: var(--warn-text); }
.sum-kpi-text { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 4px; }
.sum-kpi-label { font-size: 12.5px; color: var(--ink-2); font-weight: 600; }
.sum-kpi-value { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; }
.sum-kpi-value b { font-size: 24px; line-height: 1.15; color: var(--ink); font-variant-numeric: tabular-nums; }
.sum-kpi.red .sum-kpi-value b { color: var(--danger); }
.sum-kpi-note { font-size: 11.5px; color: var(--ink-soft); }
.sum-delta { font-size: 12px; font-weight: 600; }
.sum-delta.up { color: var(--ok-text); }
.sum-delta.down { color: var(--danger-text); }
:root[data-bs-theme="dark"] .sum-delta.up { color: var(--ok); }

.sum-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
.sum-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 14px;
  min-width: 0;
}
.sum-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.sum-card-head .sec-head { margin: 0; }
.sum-card .sub { color: var(--ink-soft); font-size: 12px; margin: -4px 0 10px; }
.sum-card .link-btn {
  display: inline-flex; align-items: center; gap: 2px;
  border: 0; background: none; color: var(--brand); font-size: 13px; cursor: pointer; padding: 4px;
}
:root[data-bs-theme="dark"] .sum-card .link-btn { color: var(--brand-text); }

.sum-stages { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.sum-stage-head { display: flex; align-items: baseline; gap: 8px; font-size: 13px; color: var(--ink); margin-bottom: 5px; }
.sum-stage-head .grow { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sum-stage-head small { color: var(--ink-soft); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.sum-stage-head b { font-variant-numeric: tabular-nums; min-width: 38px; text-align: right; }
.sum-stage-track { height: 8px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.sum-stage-track i { display: block; height: 100%; border-radius: 999px; }

/* Status donut: four slices that partition the jobs, each with a word and a
   count beside it, so the colour is never the only thing saying which is which. */
.sum-donut-wrap { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; justify-content: center; }
.sum-donut-box { position: relative; width: 168px; height: 168px; flex: 0 0 auto; }
.sum-donut { width: 100%; height: 100%; transform: rotate(-90deg); }
.sum-donut .arc { fill: none; stroke-width: 14; }
.sum-donut .arc:hover { stroke-width: 16; }
.sum-donut-center { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.sum-donut-center b { font-size: 28px; color: var(--ink); line-height: 1.1; }
.sum-donut-center small { font-size: 11.5px; color: var(--ink-soft); }
.sum-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; min-width: 170px; flex: 1 1 170px; }
.sum-legend li { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); }
.sum-legend .grow { flex: 1 1 auto; }
.sum-legend b { color: var(--ink); font-variant-numeric: tabular-nums; }
.sum-legend small { color: var(--ink-soft); min-width: 38px; text-align: right; font-variant-numeric: tabular-nums; }
.sum-legend .dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.sum-donut .arc.done { stroke: #2e9e5b; }  .sum-legend .dot.done { background: #2e9e5b; }
.sum-donut .arc.doing { stroke: var(--brand); } .sum-legend .dot.doing { background: var(--brand); }
.sum-donut .arc.todo { stroke: #a3abb4; }  .sum-legend .dot.todo { background: #a3abb4; }
.sum-donut .arc.late { stroke: var(--danger); } .sum-legend .dot.late { background: var(--danger); }

.sum-weather { display: flex; flex-direction: column; gap: 14px; }
.sum-weather-now { display: flex; align-items: center; gap: 14px; }
.sum-weather-icon { font-size: 54px; color: var(--warn); line-height: 1; }
.sum-weather-now > div { display: flex; flex-direction: column; }
.sum-weather-now b { font-size: 34px; color: var(--ink); line-height: 1.1; }
.sum-weather-now small { font-size: 12.5px; color: var(--ink-soft); }
.sum-weather-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.sum-weather-facts > span {
  display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 6px;
  align-items: center; padding: 8px; border-radius: var(--radius); background: var(--surface-2);
}
.sum-weather-facts .bx { grid-row: span 2; font-size: 20px; color: var(--info); }
.sum-weather-facts small { font-size: 11px; color: var(--ink-soft); }
.sum-weather-facts b { font-size: 14px; color: var(--ink); }
.sum-weather .hint { margin: 0; }

.sum-table { min-width: 520px; }
.sum-table td { vertical-align: middle; }
.sum-table td.nowrap { white-space: nowrap; }
.sum-table td.red span { color: var(--danger-text); font-weight: 600; }
.sum-pill { font-size: 11px; border-radius: 999px; padding: 2px 9px; white-space: nowrap; font-weight: 600; }
.sum-pill.todo { background: var(--muted-soft); color: var(--muted-text); }
.sum-pill.doing { background: var(--brand-soft); color: var(--brand-text); }
.sum-pill.late { background: var(--danger-soft); color: var(--danger-text); }

.sum-notes { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.sum-notes li { display: flex; gap: 10px; padding: 10px 4px; border-top: 1px solid var(--line); cursor: pointer; }
.sum-notes li:first-child { border-top: 0; }
.sum-notes li:hover { background: var(--surface-2); }
.sum-notes .grow { flex: 1 1 auto; min-width: 0; }
.sum-note-head { display: flex; gap: 8px; justify-content: space-between; align-items: baseline; }
.sum-note-head b { font-size: 13px; color: var(--ink); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sum-note-head small { font-size: 11px; color: var(--ink-soft); white-space: nowrap; }
.sum-notes p {
  margin: 2px 0; font-size: 13px; color: var(--ink-2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.sum-note-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: 11.5px; color: var(--ink-soft); }
.sum-note-meta .bx { margin-left: 6px; }

/* Two tiles to a row on a phone leaves ~150px each: the glyph goes above
   the words instead of beside them, so the figure keeps the width. */
@media (max-width: 480px) {
  .sum-kpi { flex-direction: column; gap: 8px; padding: 12px; }
  .sum-kpi-mark { width: 32px; height: 32px; font-size: 18px; }
  .sum-kpi-value b { font-size: 21px; }
  .sum-banner h2 { font-size: 19px; }
}

@media (min-width: 760px) {
  .sum { padding: 18px; gap: 16px; }
  .sum-kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
  .sum-row { gap: 16px; }
  .sum-row.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Right-aligned only where the side column is genuinely beside the title
   rather than wrapped under it. Below this the banner stacks, and names pushed
   to the right edge over left-aligned buttons read as two different columns.
   900 rather than the 760 the rest of this page turns at: the banner needs
   room for the title *and* two name chips before they stop fighting. */
@media (min-width: 900px) {
  .sum-banner-side { align-items: flex-end; }
  .sum-people { justify-content: flex-end; }
}

@media (min-width: 1080px) {
  .sum-row.three { grid-template-columns: 1.25fr 1fr .9fr; }
}

/* ---------------------------------------------------- ปัญหา / ข้อบกพร่อง
   The four states wear the palette the rest of the board already uses for the
   same meanings: open = danger, fixing = brand (work in hand), รอตรวจ = warn
   (waiting on somebody), ปิด = ok. Each pill carries its word too. */
.issue-pill {
  font-size: 11px; font-weight: 600; border-radius: 999px; padding: 2px 9px; white-space: nowrap;
  background: var(--muted-soft); color: var(--muted-text);
}
.issue-pill.s-open { background: var(--danger-soft); color: var(--danger-text); }
.issue-pill.s-fixing { background: var(--brand-soft); color: var(--brand-text); }
.issue-pill.s-inspect { background: var(--warn-soft); color: var(--warn-text); }
.issue-pill.s-closed { background: var(--ok-soft); color: var(--ok-text); }

.issue-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.issue-list.closed { margin-top: 8px; }
.issue-row {
  border: 1px solid var(--line);
  border-left: 4px solid var(--danger);
  border-radius: var(--radius);
  padding: 10px 12px;
  display: flex; flex-direction: column; gap: 4px;
  background: var(--surface);
}
.issue-row.s-fixing { border-left-color: var(--brand); }
.issue-row.s-inspect { border-left-color: var(--warn); }
.issue-row.s-closed { border-left-color: var(--ok); opacity: .85; }
.issue-head { display: flex; align-items: center; gap: 8px; }
.issue-head b { flex: 1 1 auto; min-width: 0; color: var(--ink); font-size: 14px; word-break: break-word; }
.issue-sev {
  flex: 0 0 auto; font-size: 10.5px; font-weight: 700; border-radius: 4px; padding: 1px 6px;
  background: var(--surface-2); color: var(--ink-2);
}
.sev-high .issue-sev { background: var(--danger); color: #fff; }
.sev-medium .issue-sev { background: var(--warn-soft); color: var(--warn-text); }
.issue-detail { margin: 0; font-size: 13px; color: var(--ink-2); white-space: pre-wrap; }
.issue-facts, .issue-by { font-size: 11.5px; color: var(--ink-soft); }
.issue-facts .red { color: var(--danger-text); font-weight: 600; }
.issue-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.mini.ok { color: var(--ok-text); }
:root[data-bs-theme="dark"] .mini.ok { color: var(--ok); }
.link-btn {
  display: inline-flex; align-items: center; gap: 4px;
  border: 0; background: none; color: var(--brand); font-size: 13px; cursor: pointer; padding: 6px 0;
}
:root[data-bs-theme="dark"] .link-btn { color: var(--brand-text); }
.check-row { display: flex; align-items: center; gap: 8px; margin: 4px 0 12px; font-size: 14px; color: var(--ink); }
.check-row input { width: 20px; height: 20px; }
.sum-donut .arc.i-open { stroke: var(--danger); }   .sum-legend .dot.i-open { background: var(--danger); }
.sum-donut .arc.i-fixing { stroke: var(--brand); }  .sum-legend .dot.i-fixing { background: var(--brand); }
.sum-donut .arc.i-inspect { stroke: var(--warn); }  .sum-legend .dot.i-inspect { background: var(--warn); }
.sum-donut .arc.i-closed { stroke: #2e9e5b; }       .sum-legend .dot.i-closed { background: #2e9e5b; }

/* รายการปัญหา on ภาพรวม: a quiet heading row between the unclosed and the
   closed, and the closed rows a step back so the open ones lead. */
.sum-table tr.issue-divider td {
  background: var(--surface-2); color: var(--ink-2); font-size: 12px; font-weight: 600;
  padding-top: 6px; padding-bottom: 6px;
}
.sum-table tr.issue-done td b { color: var(--ink-2); font-weight: 500; }
.sum-table tr.issue-done { opacity: .8; }

/* รายการโปรเจกต์: two money columns of two lines each (ยอดโปรเจกต์ / ยอดคงเหลือ,
   Forecast / Actual). The header says both lines in the same order. */
.proj-table th.num span, .proj-table th.num small { display: block; }
.proj-table th.num small { font-weight: 500; opacity: .75; }
.proj-table td.money2 b { display: block; font-size: 13px; font-weight: 600; }
.proj-table td.money2 small { display: block; font-size: 12px; color: var(--ink-soft); }
.proj-table td.money2 small.red { color: var(--danger-text); font-weight: 600; }
.proj-table td.money2 small.amber { color: var(--warn-text); }
.proj-table td.money2 b.red { color: var(--danger-text); }

/* The name is no longer the first column, so it takes the floor
   `.rep-table td:first-child` used to give it; the code column gives that back. */
.proj-table td:first-child, .proj-table th:first-child { min-width: 0; }
.proj-table .proj-name { min-width: 180px; }
.proj-table td.muted { color: var(--ink-soft); }
