/* Nowa Design Tracker — light mode, system font stack, no framework. */

:root {
  --color-bg: #f5f6f8;
  --color-surface: #ffffff;
  --color-border: #e1e4e8;
  --color-text: #1a1d21;
  --color-text-muted: #6b7280;
  --color-primary: #2563eb;
  --color-primary-hover: #1d4ed8;
  --color-danger: #dc2626;
  --color-danger-bg: #fef2f2;
  --color-success: #16a34a;
  --color-success-bg: #f0fdf4;
  --radius: 8px;
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.08);
  --shadow-lg: 0 10px 24px rgba(16, 24, 40, 0.16), 0 2px 6px rgba(16, 24, 40, 0.08);
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
    "Inter", sans-serif;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

body {
  font-family: var(--font-sans);
  background: var(--color-bg);
  color: var(--color-text);
  font-size: 14px;
  line-height: 1.5;
}

a {
  color: var(--color-primary);
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
}

button {
  font-family: inherit;
  font-size: inherit;
  cursor: pointer;
}

input {
  font-family: inherit;
  font-size: inherit;
}

/* --- Buttons --------------------------------------------------------------------
   Exactly 4 sizes app-wide (Huy, 2026-07-22) — no other button padding/font-size should
   exist anywhere in this file:
     - Mini    (.art-modal-version-edit-btn): padding 2px 3px, font-size 8px — the ✏️
       corner-overlay badge on a version thumbnail; nothing else uses this size.
     - Small   (.icon-btn, .art-modal-zoom-btn): padding 4px 10px, font-size 12px,
       font-weight 600, line-height 16px — pin/delete/download icons, zoom 1x/.../10x.
     - Normal  (.btn-secondary) / Primary (.btn-primary): SAME total rendered height as
       the app's text inputs (.art-modal-text-input etc: 9px 11px padding + 1px border +
       20px line-height = 40px) — padding 9px 16px, explicit font-size 14px + line-height
       20px (explicit, not the inherited 1.5 ratio, so ancestor font-size/line-height
       contexts — e.g. a button sitting inside .art-row — can never perturb the height).
       .btn-primary gets a `transparent` 1px border (invisible) purely so its box is the
       same total height as .btn-secondary's real 1px border — a color difference is the
       ONLY visual distinction between the two. */

.btn-primary {
  background: var(--color-primary);
  color: #fff;
  /* A DARKER-primary border, not a transparent one (Huy 2026-07-28: "primary trông cao hơn
     normal một xíu"). Both buttons have always measured the same 40px — 9px+9px padding,
     1px border, 20px line-height — and nothing else in the stylesheet touches either. What
     differed was the COLOURED area inside that box: the secondary's fill stops 1px short of
     the edge at its grey border, while the primary's fill ran all the way out through its
     transparent one, so the solid block was 2px taller than the white block beside it and
     read as a taller button. Giving the primary its own 1px ring makes both fills 38px. */
  border: 1px solid var(--color-primary-hover);
  border-radius: var(--radius);
  padding: 9px 16px;
  font-size: 14px;
  line-height: 20px;
  font-weight: 600;
}
.btn-primary:hover {
  background: var(--color-primary-hover);
  /* Keep the 1px ring visible on hover too — otherwise the fill and the border become the
     same colour and the button grows back its extra 2px of solid block. */
  border-color: #1e40af;
}
.btn-primary:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* --- Topbar --------------------------------------------------------------------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 56px;
  padding: 0 20px;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  position: sticky;
  top: 0;
  z-index: 20;
}

.topbar-left {
  display: flex;
  align-items: center;
  gap: 20px;
}

/* Now a link to Home (Huy, 2026-07-23) instead of a plain div — reset the usual <a>
   underline/color so it still reads as a plain title, not a hyperlink. */
.topbar-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 16px;
  letter-spacing: -0.01em;
  color: var(--color-text);
}
.topbar-title:hover {
  text-decoration: none;
  color: var(--color-text);
}
.topbar-icon {
  width: 24px;
  height: 24px;
  image-rendering: pixelated;
}

.topbar-nav {
  display: flex;
  gap: 4px;
}

.topbar-link {
  color: var(--color-text-muted);
  font-weight: 600;
  font-size: 13px;
  padding: 6px 10px;
  border-radius: var(--radius);
}
.topbar-link:hover {
  background: var(--color-bg);
  text-decoration: none;
  color: var(--color-text);
}

.profile-menu-wrap {
  position: relative;
}

.profile-trigger {
  background: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 6px 12px;
  color: var(--color-text);
  display: flex;
  align-items: center;
  gap: 6px;
}
.profile-trigger:hover {
  background: var(--color-bg);
}
.profile-trigger .chevron {
  font-size: 10px;
  color: var(--color-text-muted);
}

.profile-dropdown {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  min-width: 180px;
  overflow: hidden;
  padding: 4px;
}

.dropdown-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: 6px;
  padding: 8px 10px;
  color: var(--color-text);
}
.dropdown-item:hover {
  background: var(--color-bg);
}

/* --- Breadcrumbs ------------------------------------------------------------------ */

/* Pinned right below the sticky topbar (56px tall) — same "stays visible while scrolling
   long content" idea as the art modal's pinned header (Huy, 2026-07-22). Explicit
   background so scrolled-under content (e.g. a white .project-header card) doesn't show
   through the sticky bar. */
/* The breadcrumb IS the page title now (Huy 2026-07-28) — /project and /folder used to
   carry a generic "Project"/"Folder" heading under it, and Home an "Home" heading, all of
   which the trail already says, better. So it renders at title scale: the ancestors stay
   muted links, the current page is the heading. Still sticky under the topbar. */
.breadcrumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 14px 20px 10px;
  font-size: 20px;
  line-height: 28px;
  color: var(--color-text-muted);
  position: sticky;
  top: 56px;
  z-index: 19;
  background: var(--color-bg);
}

.crumb {
  color: var(--color-text-muted);
  font-weight: 400;
}
a.crumb:hover {
  color: var(--color-text);
}
.crumb-current {
  color: var(--color-text);
  font-weight: 700;
}
.crumb-sep {
  color: var(--color-border);
}

/* --- Page content ------------------------------------------------------------------ */

#page-content {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.empty-state {
  border: 1px dashed var(--color-border);
  border-radius: var(--radius);
  padding: 60px 20px;
  text-align: center;
  color: var(--color-text-muted);
  background: var(--color-surface);
}

/* --- Forms --------------------------------------------------------------------- */

form label {
  display: block;
  margin-bottom: 14px;
  font-weight: 600;
  font-size: 13px;
  color: var(--color-text);
}

/* font-size + line-height are explicit (not inherited) on every text-control rule in this
   file (form input/select/textarea, .control below, .art-modal-text-input) — all the same
   14px / 20px — so an ancestor's own font-size (e.g. .inline-label's 13px) can never
   silently shrink one specific control relative to the others (Huy, 2026-07-22: "textbox,
   dropdown đều đang không tương đồng"). Buttons (.btn-primary/.btn-secondary above) use
   the identical 14px/20px pair so the two total the same 40px height (9px padding + 1px
   border × 2 + 20px line-height). */
form input {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 9px 11px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font-weight: 400;
  font-size: 14px;
  line-height: 20px;
}
form input:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
}

form select,
form textarea {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 9px 11px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font-weight: 400;
  font-family: inherit;
  font-size: 14px;
  line-height: 20px;
}
form select:focus,
form textarea:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
}
form textarea {
  resize: vertical;
}

form .checkbox-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 400;
}
form .checkbox-label input {
  display: inline;
  width: auto;
  margin: 0;
}

.inline-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  font-size: 13px;
  margin-bottom: 12px;
}
.inline-label select {
  margin-top: 0;
  width: auto;
  min-width: 220px;
}

/* Standalone select/input controls living OUTSIDE a <form> (sort dropdowns, admin
   permission picker, metadata type picker + "new type" name field) don't inherit the
   `form select`/`form input` styling above at all — no <form> ancestor to hang it off of
   — so they were rendering with bare, inconsistent browser-native chrome. `.control` gives
   them the identical look explicitly; width/margin stay whatever each context's own layout
   rule already sets (e.g. `.inline-label select` above), `.control` only supplies the
   border/padding/background/focus-ring "chrome" that was missing. */
.control {
  display: block;
  padding: 9px 11px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font-weight: 400;
  font-family: inherit;
  font-size: 14px;
  line-height: 20px;
}
/* --- One dropdown arrow for the whole app (Huy 2026-07-28) ------------------------------
   Three different indicators were in play: the "▾" character in the Tag / Trạng thái filter
   buttons, a smaller muted "▾" on the progress combos, and each browser's own native arrow
   on every <select>. Huy picked the Tag button's one, so that glyph is now the single
   source: the selects get their native arrow removed and this SVG (a triangle drawn to the
   same proportions as ▾) painted in its place, and the combo caret below is restyled to the
   same size/colour as the text in those buttons. Kept as a background image rather than a
   pseudo-element because a <select> can't host one. */
/* Selectors deliberately more specific than every per-component padding rule that comes
   later in this file (.status-select's 3px 8px, .progress-table select's 5px 7px): those
   would otherwise win the tie on source order and the arrow would sit on top of the text.
   `:not([multiple])` also keeps list-boxes, which have no arrow, out of it. */
select.control,
form select:not([multiple]),
select.status-select,
.progress-table select.control {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'><path d='M2 3.6h6L5 7.4z' fill='%231a1d21'/></svg>");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 10px 10px;
  padding-right: 28px;
}
/* Firefox keeps drawing a focus outline on the removed arrow's slot without this. */
select.control:-moz-focusring,
form select:not([multiple]):-moz-focusring,
select.status-select:-moz-focusring {
  color: transparent;
  text-shadow: 0 0 0 var(--color-text);
}

.control:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
}

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

.form-error {
  background: var(--color-danger-bg);
  color: var(--color-danger);
  border-radius: var(--radius);
  padding: 8px 11px;
  font-size: 13px;
  margin-bottom: 14px;
}

.form-success {
  background: var(--color-success-bg);
  color: var(--color-success);
  border-radius: var(--radius);
  padding: 8px 11px;
  font-size: 13px;
  margin-bottom: 14px;
}

/* --- Naming modal (download) ---------------------------------------------------- */

/* Clickable token chips under each pattern field (Huy 2026-07-28) — Small tier, monospace
   so "{name}" reads as the literal text it types into the box. */
.naming-tokens {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 10px;
}
.naming-token-btn {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.naming-hint {
  font-size: 12px;
  /* Was -6px on top to hug the input directly; the token chips sit in that gap now. */
  margin: 0 0 14px;
}

.naming-preview {
  font-size: 13px;
  margin-bottom: 14px;
  padding: 8px 11px;
  background: var(--color-bg);
  border-radius: var(--radius);
  word-break: break-all;
}

.naming-preview-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* --- Login page ------------------------------------------------------------------ */

.login-body {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
}

.login-card {
  width: 100%;
  max-width: 360px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 12px;
  box-shadow: var(--shadow);
  padding: 32px;
}

.login-card h1 {
  font-size: 18px;
  margin: 0 0 24px;
  text-align: center;
}

.login-card button[type="submit"] {
  width: 100%;
  margin-top: 4px;
}

/* --- Modal --------------------------------------------------------------------- */

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 18, 22, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.modal {
  width: 100%;
  max-width: 360px;
  background: var(--color-surface);
  border-radius: 12px;
  box-shadow: var(--shadow-lg);
  padding: 20px;
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}
.modal-header h2 {
  font-size: 16px;
  margin: 0;
}

.modal-close {
  background: none;
  border: none;
  font-size: 20px;
  line-height: 1;
  color: var(--color-text-muted);
  padding: 2px 6px;
  border-radius: 6px;
}
.modal-close:hover {
  background: var(--color-bg);
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 4px;
}

/* --- Secondary / icon buttons ---------------------------------------------------- */

.btn-secondary {
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 9px 16px;
  font-size: 14px;
  line-height: 20px;
  font-weight: 600;
}
.btn-secondary:hover {
  background: var(--color-bg);
}

/* Danger modifier, same tier as whichever base button class it's paired with (Normal
   .btn-secondary or Small .icon-btn below) — just recolors text/hover, no size change.
   Added 2026-07-23 for "Xoá version" (art-modal.js), which needs to sit at Normal size
   next to "Đặt làm Final"/Upload rather than the Small size .icon-btn-danger gives it. */
.btn-secondary.btn-danger {
  color: var(--color-danger);
}
.btn-secondary.btn-danger:hover {
  background: var(--color-danger-bg);
}

/* Small tier (see the 4-size comment above .btn-primary) — same padding/font-size/weight
   as .art-modal-zoom-btn below, just without the "active" state that only zoom needs. */
.icon-btn {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 4px 10px;
  font-size: 12px;
  font-weight: 600;
  line-height: 16px;
}
.icon-btn:hover {
  background: var(--color-bg);
}
/* The frame tools disable themselves at the ends of the strip / on the last frame, so the
   Small tier needs a disabled look too (Huy 2026-07-29 moved those buttons down to it). */
.icon-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.icon-btn:disabled:hover {
  background: var(--color-surface);
}
.icon-btn-danger {
  color: var(--color-danger);
}
.icon-btn-danger:hover {
  background: var(--color-danger-bg);
}

/* --- Page header ------------------------------------------------------------------ */

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}
.page-header h1 {
  font-size: 18px;
  margin: 0;
}
/* Right side of the Folder section header on the Project page: tag filter + "Tạo
   folder" grouped together (reuses .tag-filter-wrap/-dropdown/-option from the Folder
   page's art-level tag filter, styled generically above). */
.page-header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* --- Project grid (Home) --------------------------------------------------------- */

.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}

.project-card {
  position: relative;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  overflow: hidden;
}

.project-card-link {
  display: block;
  color: inherit;
}
.project-card-link:hover {
  text-decoration: none;
}

.project-card-image {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--color-bg);
}
.project-card-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.project-card-placeholder {
  width: 100%;
  height: 100%;
  background: repeating-linear-gradient(
    45deg,
    var(--color-bg),
    var(--color-bg) 10px,
    #eceef1 10px,
    #eceef1 20px
  );
}

.pin-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  font-size: 14px;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4));
}

.project-card-body {
  padding: 12px;
}

.project-card-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
}

.project-card-name {
  font-weight: 700;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-card-desc {
  margin: 0;
  color: var(--color-text-muted);
  font-size: 12px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Gap above the status pills on the HOME card (Huy 2026-09-16). Scoped to
   .project-card-body on purpose: .status-badges itself must stay margin-top-less, because on
   the folder card the 8px above it already comes from .folder-card-name-row's margin-bottom —
   putting it on the shared rule would double to 16px there.
   8px is that same folder-card gap, so the pills sit identically on both cards.
   margin-bottom: 0 because here the pills are the LAST thing in the body (on the folder card
   they are followed by .folder-last-updated, which is what that 6px is separating) — without
   this the card would carry 18px of padding below and 12px above. */
.project-card-body .status-badges {
  margin-top: 8px;
  margin-bottom: 0;
}

.project-card-actions {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  gap: 4px;
}
.project-card-actions .icon-btn {
  background: rgba(255, 255, 255, 0.92);
}

/* --- Type badges ------------------------------------------------------------------ */

.badge {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
}
.badge-character {
  background: #e0e7ff;
  color: #4338ca;
}
.badge-action {
  background: #ffedd5;
  color: #c2410c;
}
.badge-background {
  background: #dcfce7;
  color: #15803d;
}

/* --- Section headers (Home Pinned/Project, Project page Pinned/Folder) ------------- */

.section-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 24px 0 12px;
}
.section-header-row:first-child {
  margin-top: 0;
}
.section-title {
  font-size: 15px;
  margin: 0;
  color: var(--color-text);
}

/* Page-level category title (Project page / Folder page) — 2x .section-title's 15px,
   sits above the header card. */
/* --- Project header (Project page) ------------------------------------------------- */

.project-header {
  display: flex;
  gap: 20px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 20px;
  margin-bottom: 24px;
}
/* Height is set EXPLICITLY via JS (project.js's syncProjectHeaderImageHeight(), called
   after every render + on window resize) to match .project-header-info's actual rendered
   height — NOT via CSS flex `align-items: stretch`, which turned out unreliable here: an
   `aspect-ratio` item with both width/height left `auto` inside a ROW flex container whose
   OWN height is itself content-driven (`auto`) hits a genuine circular-sizing case that
   browsers resolve inconsistently in practice (Huy observed the image ballooning huge and
   dragging the whole card up with it, instead of following the info column). Once JS sets
   an explicit pixel `height`, `aspect-ratio` reliably derives `width` from THAT (that
   direction — width-from-definite-height — is the well-supported, unambiguous case).
   `height: 105px` below is only the pre-JS fallback (original fixed size), overwritten
   synchronously right after render so it's never actually visible. */
.project-header-image {
  aspect-ratio: 4 / 3;
  height: 105px;
  flex-shrink: 0;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--color-bg);
}
.project-header-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Only the box that actually HAS an image gets the affordance — the placeholder has nothing
   to preview (Huy 2026-08-27). */
.project-header-image.is-zoomable {
  cursor: zoom-in;
}
.project-header-info {
  flex: 1;
  min-width: 0;
}
.project-header-title-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}
.project-header-title-row h1 {
  font-size: 18px;
  margin: 0;
}
.project-header-desc {
  color: var(--color-text-muted);
  font-size: 13px;
  margin: 0 0 14px;
  white-space: pre-wrap;
}
.project-header-actions {
  display: flex;
  gap: 8px;
}

/* --- Folder grid (Project page) ---------------------------------------------------- */

.folder-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
  margin-bottom: 8px;
}

.folder-card {
  position: relative;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  overflow: hidden;
}
.folder-card-link {
  display: block;
  color: inherit;
}
.folder-card-link:hover {
  text-decoration: none;
}

.folder-preview-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 2px;
  aspect-ratio: 1 / 1;
  background: var(--color-border);
}
.folder-preview-cell {
  background: var(--color-surface);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.folder-preview-cell img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  image-rendering: pixelated;
}
.folder-preview-placeholder {
  background: repeating-linear-gradient(
    45deg,
    var(--color-bg),
    var(--color-bg) 8px,
    #eceef1 8px,
    #eceef1 16px
  );
}

.folder-card-body {
  padding: 12px;
}
/* Name + tags share one row now (Huy, 2026-07-23: was name, then tags stacked below) — the
   8px gap is the row's own flex `gap`, same technique as .art-row-name-line. */
.folder-card-name-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}
.folder-card-name {
  font-weight: 700;
  font-size: 14px;
  min-width: 0;
  flex-shrink: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.folder-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.tag-chip {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-size: 11px;
}

.status-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 6px;
}
.status-badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 600;
  white-space: nowrap;
}
/* Count > 0 colors (folder card badge, project page only — the folder page's own
   status pill/dropdown, .art-status-*, is untouched): Empty = red (needs attention),
   Reviewing = yellow, Improving = orange (Huy 2026-07-23: matches .art-status-improving
   below — the previous amber was too close to Reviewing's yellow to tell apart at a
   glance), Done = green. */
.status-badge-empty {
  background: var(--color-danger-bg);
  color: var(--color-danger);
}
.status-badge-reviewing {
  background: #fef9c3;
  color: #854d0e;
}
.status-badge-improving {
  background: #ffedd5;
  color: #c2410c;
}
.status-badge-done {
  background: #dcfce7;
  color: #15803d;
}
/* Count = 0 always wins, regardless of status — muted gray. Two classes on the element
   (.status-badge-zero applied alongside the status-specific class above) keeps this at
   higher specificity than any single .status-badge-* rule, so source order doesn't matter. */
.status-badge.status-badge-zero {
  background: #f3f4f6;
  color: var(--color-text-muted);
}

.folder-last-updated {
  font-size: 11px;
}

.folder-card-actions {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  gap: 4px;
}
.folder-card-actions .icon-btn {
  background: rgba(255, 255, 255, 0.92);
}

/* --- Folder header (Folder page) --------------------------------------------------- */

.folder-header {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 20px;
  margin-bottom: 24px;
}
.folder-header-title-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}
.folder-header-title-row h1 {
  font-size: 18px;
  margin: 0;
}
.folder-header .folder-tags {
  margin-bottom: 10px;
}
.folder-header-desc {
  color: var(--color-text-muted);
  font-size: 13px;
  margin: 0 0 14px;
  white-space: pre-wrap;
}
.folder-header-actions {
  display: flex;
  gap: 8px;
}

/* --- Art list (Folder page) --------------------------------------------------------- */

.art-list {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  /* Was `overflow: hidden` (just to clip each row's own :hover background into this
     container's rounded corners). Changed to `visible` because the 💬 comment tooltip
     bubble (.comment-tooltip-bubble below) pops up ABOVE its row, and for the very FIRST
     row in the list that popup sits right at this container's top edge — `hidden` would
     have clipped it. The rounded-corner look for :hover backgrounds is preserved explicitly
     below via :first-child/:last-child radius instead, so this is a no-op visually except
     for unblocking the tooltip. */
  overflow: visible;
  margin-bottom: 8px;
}

.art-row {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 60px;
  padding: 6px 14px;
  border-bottom: 1px solid var(--color-border);
  cursor: pointer;
}
.art-row:first-child {
  border-top-left-radius: 10px;
  border-top-right-radius: 10px;
}
.art-row:last-child {
  border-bottom: none;
  border-bottom-left-radius: 10px;
  border-bottom-right-radius: 10px;
}
.art-row:hover {
  background: var(--color-bg);
}

.art-row-thumb {
  flex: 0 0 52px;
  width: 52px;
  height: 52px;
  border-radius: 6px;
  overflow: hidden;
  background: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
}
.art-row-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  image-rendering: pixelated;
}
.art-row-thumb-empty {
  background: repeating-linear-gradient(
    45deg,
    var(--color-bg),
    var(--color-bg) 8px,
    #eceef1 8px,
    #eceef1 16px
  );
}

.art-row-name {
  /* Absorbs the old .art-row-desc's flex-basis too now that description sits as a second
     line inside this same column instead of its own sibling column (Huy feedback).
     NO LONGER GROWS (2026-07-30): "Updated at" needs to sit around the middle of the row
     with a real gap before it, and the only way to free that space is to stop this column
     from eating every spare pixel. 480px still shows ~80 chars of description before the
     ellipsis — Huy's "vừa đủ để hiện một phần description". */
  flex: 0 1 480px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: hidden;
}
.art-row-name-line {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.art-row-name-text {
  font-weight: 600;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Duration (Huy, 2026-07-22): shows next to the art name when set — the 8px gap comes
   free from .art-row-name-line's own `gap: 8px` (flex row), no extra margin needed. */
.art-row-duration {
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 600;
  color: #b68962;
}

.art-row-tags {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}
.art-row-tags .tag-chip {
  font-size: 10px;
  padding: 0 6px;
}
.art-row-name-desc {
  color: var(--color-text-muted);
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "Updated: dd/MM/yyyy at HH:mm" (Huy, 2026-07-30) — FIXED width, so its left edge lands at
   the same x on every row (Huy: "cột Last update chưa thẳng hàng").
   It used to absorb the leftover width and center the date in it, which looked centered but
   could never line up: the leftover differs per row because .art-row-status is content-sized
   and its content varies — exactly as Huy guessed, the 💬 button only exists on
   reviewing/improving, and "Reviewing" is far wider than "Done". The cure is that NO column
   between the thumb and the action buttons is content-sized any more (see the fixed
   .art-row-status below): the row's slack lands in one place only, the gap before
   .art-row-actions, and under compression the single shrinkable column (.art-row-name)
   gives up the same number of pixels on every row. Text is left-aligned rather than centered
   so alignment doesn't depend on digits being equal-width.
   185px covers the longest string ("Uploaded: 30/07/2026 at 06:13" measures 173px at 11px in
   DejaVu, which is wider than any font in --font-sans). */
.art-row-updated {
  flex: 0 0 185px;
  font-size: 11px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.art-row-status {
  /* Fixed, not content-sized (2026-07-30): this column is what used to make every row's
     leftover width different — the 💬 button is only rendered for reviewing/improving, and
     the status control's own width follows its label ("Reviewing" ~100px vs "Done" ~50px).
     160px = 💬 button (~36) + gap (6) + the widest select ("Reviewing": 64px text + 8/28
     padding) = ~142, plus room to spare on purpose: the 💬 glyph's width is platform
     dependent, and a column too tight would shrink the select and clip "Reviewing" — the
     exact complaint from 2026-07-29. Contents keep their natural widths inside it. */
  flex: 0 0 160px;
  /* display:flex added for the 💬 comment-shortcut icon-btn (comment revamp, July 2026),
     which now sits before the status select/pill — gap matches .art-row-actions below. */
  display: flex;
  align-items: center;
  gap: 6px;
}

/* 💬 comment tooltip (Folder page) — CSS-only custom tooltip replacing the native `title=`
   attribute, which Huy confirmed does not reliably show on hover (2026-07-23). No JS: pure
   :hover/:focus-within visibility toggle. See folder.js's commentBtnHtml for the markup
   (.comment-tooltip-wrap wraps the 💬 button + this bubble as a sibling). */
.comment-tooltip-wrap {
  position: relative;
  display: inline-flex;
}
.comment-tooltip-bubble {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-text);
  color: #fff;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.4;
  white-space: normal;
  max-width: 260px;
  width: max-content;
  padding: 6px 10px;
  border-radius: 6px;
  box-shadow: var(--shadow-lg);
  text-align: left;
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.12s ease, visibility 0.12s ease;
  /* Bubble must never intercept the click meant for the button underneath it, or for the
     row-click-opens-modal handler in folder.js. */
  pointer-events: none;
  /* Higher than anything else a row can stack against (later rows in the same .art-list,
     which paint over earlier ones in normal flow) so the bubble is never hidden behind a
     sibling row below it. */
  z-index: 40;
}
.comment-tooltip-wrap:hover .comment-tooltip-bubble,
.comment-tooltip-wrap:focus-within .comment-tooltip-bubble {
  visibility: visible;
  opacity: 1;
}

.art-row-actions {
  flex: 0 0 auto;
  /* Takes the row's slack so it stays pinned to the right edge while the columns before it
     keep fixed positions — see .art-row-updated above for why the slack has to land here. */
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
}
/* Upload sits at the front of the cluster (ahead of Download), same Small tier as its
   Download/Pin/Delete siblings (.icon-btn) but keeps its text label — unlike those
   icon-only buttons — so it still reads clearly as the primary per-row action (Huy,
   2026-07-23). Applies identically in Grid view (.art-card-actions .icon-btn below). */
.art-row-actions .icon-btn,
.art-card-actions .icon-btn {
  white-space: nowrap;
}

/* --- Art grid (Folder page, Grid view — Huy, 2026-07-23) ---------------------------------
   Same auto-fill/minmax formula as .folder-grid (Project page): at #page-content's own
   max-width: 1200px, `repeat(auto-fill, minmax(210px, 1fr))` naturally lands on 5 columns
   per row (5 x 210px + 4 x 16px gap ≈ 1114px, fits; a 6th column would need ~1330px) — this
   satisfies "tối đa 5 card/hàng" WITHOUT hardcoding a column count, and it reflows down to
   fewer columns on any narrower viewport/#page-content on its own. Used for BOTH
   #pinned-art-list and #art-list when viewMode === "grid" (folder.js's renderArtLists());
   `.art-list`'s own box (background/border/shadow) is intentionally NOT reused here — each
   `.art-card` carries its own box instead, same relationship as .folder-grid/.folder-card. */
.art-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 16px;
  margin-bottom: 8px;
}

.art-card {
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  cursor: pointer;
  /* Deliberately NOT `overflow: hidden` on the card itself (unlike .folder-card) — the 💬
     comment tooltip bubble (.comment-tooltip-bubble, shared with List view) can be wider
     than a ~210px card and must not get clipped, same reasoning that made .art-list switch
     from `overflow: hidden` to `visible` earlier. Only .art-card-thumb below clips its own
     image to the card's rounded top corners — background-clip already rounds the card's
     own surface/border to `border-radius` with no extra overflow rule needed for that. */
}
.art-card:hover {
  background: var(--color-bg);
}
/* Optional accent for a pinned art's card in the Pinned section (Huy: "có thể thêm
   border/accent nhẹ để phân biệt") — a thin primary-colored ring, no layout shift. */
.art-card-pinned {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 1px var(--color-primary), var(--shadow);
}

.art-card-thumb {
  aspect-ratio: 1 / 1;
  background: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-top-left-radius: 9px;
  border-top-right-radius: 9px;
}
.art-card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  image-rendering: pixelated;
}
/* .art-row-thumb-empty (checkerboard placeholder) is reused as-is on this element when the
   art has no New version yet — see folder.js's renderArtCard. */

.art-card-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 12px 12px;
}

.art-card-name-row {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}
.art-card-name {
  font-weight: 700;
  font-size: 13px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Duration + tags share one row (Huy, 2026-07-23: was two stacked rows before) — same
   idea as List view's .art-row-name-line, which already puts both on the name's line. */
.art-card-meta-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}
.art-card-duration {
  font-size: 11px;
  font-weight: 600;
  color: #b68962;
  flex-shrink: 0;
}
.art-card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.art-card-tags .tag-chip {
  font-size: 10px;
  padding: 0 6px;
}

/* Grid card's description is a single truncated line (`title=` carries the full text) —
   unlike List's .art-row-name-desc, which has room to show more before ellipsis kicks in. */
.art-card-desc {
  color: var(--color-text-muted);
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.art-card-status-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 2px;
}

/* Grid counterpart of .art-row-updated — sits right under the status row (Huy, 2026-07-30).
   The 6px gap comes free from .art-card-body's own flex `gap`; no margin needed. */
.art-card-updated {
  font-size: 11px;
  /* A ~210px card leaves ~186px of content width and the longest string measures ~173px, so
     it fits — ellipsis is the guard for a narrower card, not the normal case. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.art-card-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  margin-top: 4px;
  padding-top: 8px;
  border-top: 1px solid var(--color-border);
}

/* "⋯" Pin/Xoá overflow menu (Huy, 2026-07-23: two rarely-used icon buttons collapsed into
   one, in both List's .art-row-actions and Grid's .art-card-actions) — same
   position:relative/absolute dropdown technique as .profile-dropdown, reusing
   `.dropdown-item` for the menu rows (just the danger-red variant is new). */
.art-more-menu,
.folder-more-menu {
  position: relative;
}
.art-more-menu-dropdown,
.folder-more-menu-dropdown {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  min-width: 140px;
  padding: 4px;
  z-index: 30;
}
.dropdown-item-danger {
  color: var(--color-danger);
}
.dropdown-item-danger:hover {
  background: var(--color-danger-bg);
}

/* --- Art list controls: sort + view toggle + tag filter (Folder page) --------------- */

.art-list-controls {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
/* .inline-label carries its own `margin-bottom: 12px` (meant for when it stands alone
   above a block, e.g. the metadata modal's type picker) — inside this flex row that extra
   margin inflates the label's own box, pushing its actual content (the "Sắp xếp" select)
   up relative to `align-items: center`, while the sibling "Tag ▾" button (no such margin)
   centers normally — the mismatch reads as "Tag ▾ bị lệch xuống" (Huy, 2026-07-23). */
.art-list-controls .inline-label {
  margin-bottom: 0;
}
/* "+ Tạo art" / "Cập nhật metadata" moved out of their own header row and into this one
   (Huy 2026-07-28): actions on the left, sort/status/view/tag pushed to the right. The
   push lives on the sibling selector, not on the right-hand group itself, so when there
   are no action buttons at all (non-admin) the filters simply stay left instead of
   floating alone at the right edge. */
.art-list-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}
.art-list-actions ~ .art-list-controls-right {
  margin-left: auto;
}

/* The tab bar already names the section, so its heading is gone (Huy 2026-07-28) — keep
   the actions pinned right rather than letting them slide left into the gap it left. */
.page-header-actions {
  margin-left: auto;
}

/* List/Grid view toggle (Huy, 2026-07-23): sits between "Sắp xếp" and "Tag ▾". Two ALWAYS
   visible `.btn-secondary`s — whichever matches the current view mode gets
   `.art-view-btn.active`, same primary-bg/white-text look as `.cm-mode-btn.active` /
   `.art-modal-zoom-btn.active` above. Rendered as ONE fused segmented control (Huy:
   "cảm giác như toggle thật") instead of two separate buttons with a gap between them: no
   gap, only the outer corners rounded, and the second button's left border overlaps the
   first's right border (negative margin) so the shared edge is a single 1px line instead
   of a doubled-up one — the standard segmented-control technique. */
.art-view-toggle {
  display: flex;
}
.art-view-btn {
  border-radius: 0;
}
.art-view-btn:first-child {
  border-top-left-radius: var(--radius);
  border-bottom-left-radius: var(--radius);
}
.art-view-btn:last-child {
  border-top-right-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
  margin-left: -1px;
}
.art-view-btn.active {
  position: relative; /* stack above the neighboring button's border at the shared edge */
  z-index: 1;
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.art-view-btn.active:hover {
  background: var(--color-primary-hover);
}

.tag-filter-wrap,
.status-filter-wrap {
  position: relative;
}

.tag-filter-dropdown,
.status-filter-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 8px;
  min-width: 160px;
  max-height: 240px;
  overflow-y: auto;
  z-index: 30;
}

.tag-filter-option,
.status-filter-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 6px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 400;
  white-space: nowrap;
  cursor: pointer;
}
.tag-filter-option:hover,
.status-filter-option:hover {
  background: var(--color-bg);
}
.tag-filter-option input,
.status-filter-option input {
  width: auto;
  margin: 0;
}

/* Status filter (Huy, 2026-07-23): draft-then-Confirm, like Merge's tag panels — unlike
   the Tag ▾ filter above, which applies each checkbox instantly. */
.status-filter-actions {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--color-border);
  display: flex;
  justify-content: flex-end;
}

/* Status control shared colors — empty gray / reviewing yellow / improving orange /
   done green (deliberately separate from .status-badge-* used on the project page's
   folder cards, per this page's explicit color spec). */
.status-select,
.status-pill {
  font-size: 11px;
  font-weight: 600;
  border-radius: 6px;
  padding: 3px 8px;
  border: none;
}
.status-select {
  cursor: pointer;
  font-family: inherit;
}
.status-pill {
  display: inline-block;
  white-space: nowrap;
}
.art-status-empty {
  background: #f3f4f6;
  color: #4b5563;
}
.art-status-reviewing {
  background: #fef9c3;
  color: #854d0e;
}
.art-status-improving {
  background: #ffedd5;
  color: #c2410c;
}
.art-status-done {
  background: #dcfce7;
  color: #15803d;
}

/* --- Data table (Admin users) ----------------------------------------------------- */

/* --- Tabs (Huy 2026-07-27) — Folder|Tiến độ on /project, Art|Tiến độ on /folder ------
   Underline style rather than the app's segmented-control look (.art-view-btn,
   .cm-mode-btn): those switch how ONE list is displayed, these switch which section of the
   page you are on, and the two shouldn't read as the same kind of control. */
.tab-bar {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--color-border);
  margin-bottom: 16px;
}
.tab-btn {
  padding: 9px 16px;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px; /* sit the active underline ON the bar's own border */
  background: none;
  color: var(--color-text-muted);
  font-size: 14px;
  line-height: 20px;
  font-weight: 600;
  cursor: pointer;
}
.tab-btn:hover {
  color: var(--color-text);
}
.tab-btn.active {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}

/* --- Progress table (Tiến độ tab) -------------------------------------------------- */
.progress-table-wrap {
  overflow-x: auto; /* six columns of inputs need room on a narrow window */
  margin-bottom: 16px;
}
.progress-table td {
  vertical-align: middle;
}
/* Sortable date headers (Huy 2026-07-28): the whole header cell is the button, styled to
   look like the plain <th> text it replaces until it's active. */
.progress-sort-btn {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 600;
  text-transform: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}
.progress-sort-btn:hover {
  color: var(--color-text);
}
.progress-sort-btn.active {
  color: var(--color-primary);
}
.progress-sort-icon {
  font-size: 11px;
}
/* Borderless in-cell inputs, same "spreadsheet cell" language as the metadata table:
   the grid lines come from the table, not from every control. */
.progress-table input[type="text"],
.progress-table input[type="date"],
.progress-table select {
  width: 100%;
  min-width: 120px;
  padding: 5px 7px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--color-text);
  font-size: 13px;
  line-height: 18px;
  font-family: inherit;
}
.progress-table input[type="text"]:hover,
.progress-table input[type="date"]:hover,
.progress-table select:hover {
  border-color: var(--color-border);
}
.progress-table input[type="text"]:focus,
.progress-table input[type="date"]:focus,
.progress-table select:focus {
  outline: none;
  border-color: var(--color-primary);
  background: var(--color-surface);
}
/* Content cell: text input + our own dropdown menu (a native <datalist> filtered its
   suggestions against the current value and drew a second arrow of its own — Huy
   2026-07-28). The caret is a real button, so it can toggle the menu; the menu itself is
   positioned under the cell. */
.progress-combo {
  position: relative;
  display: block;
}
.progress-table .progress-combo input {
  padding-right: 20px; /* keep text clear of the caret button */
}
.progress-combo-caret {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  padding: 0;
  border: none;
  background: none;
  /* Same ▾ as the Tag / Trạng thái buttons — same colour and size, so every dropdown on the
     page reads as one control (Huy 2026-07-28: it used to be a smaller, muted variant). */
  color: var(--color-text);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.progress-combo-caret:hover {
  color: var(--color-primary);
}
/* `fixed`, not `absolute`: the menu is anchored to the input's viewport rect from JS so it
   can live outside .progress-table-wrap, whose overflow-x would otherwise clip it at the
   table's edge (Huy 2026-07-28). */
.progress-combo-menu {
  position: fixed;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 4px;
  z-index: 30;
}
.progress-combo-option {
  display: block;
  width: 100%;
  text-align: left;
  padding: 6px 10px;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--color-text);
  font-size: 13px;
  line-height: 18px;
  white-space: nowrap;
  cursor: pointer;
}
.progress-combo-option:hover {
  background: var(--color-bg);
}

/* Our own calendar button, replacing the native icon below. tabindex="-1" keeps it out of
   the tab run (dd -> mm -> yyyy -> next cell), and putting the picker behind a click here
   instead of on the field itself is what lets the field stay typeable — an open picker
   takes the keyboard (Huy 2026-07-28). */
.progress-date-cell {
  position: relative;
  display: block;
}
.progress-table .progress-date-cell input {
  padding-right: 24px;
}
.progress-date-picker-btn {
  position: absolute;
  right: 3px;
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  padding: 0;
  border: none;
  cursor: pointer;
  /* The browser's own calendar glyph, redrawn as SVG (Huy 2026-07-28: keep the old icon,
     not an emoji) — the native one had to go because it sat in the tab order, so this is
     the same picture on a button we control. */
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><g fill='none' stroke='%236b7280' stroke-width='1.3'><rect x='2.2' y='3.6' width='11.6' height='10.6' rx='1.6'/><path d='M2.2 7.1h11.6'/><path d='M5.6 1.8v2.6M10.4 1.8v2.6' stroke-linecap='round'/></g></svg>")
    center / 15px 15px no-repeat;
  opacity: 0.64; /* 20% fainter than before (Huy 2026-07-28) */
}
.progress-date-picker-btn:hover {
  opacity: 1;
}

/* Actual date later than the estimate (Huy 2026-07-28) — red on the value itself, not the
   whole row: the point is which date slipped. Beats .is-empty by specificity order below
   (a late cell is never empty anyway) and stays red on focus, unlike the placeholder dim. */
.progress-table input.progress-date.is-late,
.progress-table input.progress-date.is-late::-webkit-datetime-edit,
.progress-table .is-late {
  color: var(--color-danger);
  font-weight: 600;
}

/* Derived day counts ("Trễ", "TG review") — plain numbers, right-aligned so they scan. */
.progress-days {
  display: block;
  text-align: right;
  padding-right: 6px;
  font-variant-numeric: tabular-nums;
}

/* Left-hand toggles on the progress header row ("Tiến độ folder", "Số ngày"). */
.progress-header {
  gap: 16px;
}
.progress-toggles {
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: 13px;
}
.progress-toggles label {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}

/* Chrome puts the calendar icon in the tab order, so tabbing off "yyyy" lands on that
   button instead of the next cell's "dd" (Huy 2026-07-28). The icon lives in the input's
   shadow DOM — no tabindex reachable from here — so it's removed outright, and progress-tab
   .js opens the calendar on click instead, which is the affordance the cell was specified
   with anyway ("bấm vào sẽ mở dropdown lịch chọn ngày"). */
.progress-table input[type="date"]::-webkit-calendar-picker-indicator {
  display: none;
}

/* An empty date cell still renders the browser's own "dd/mm/yyyy" hint — dim it so it reads
   as a placeholder instead of data (Huy 2026-07-28). The class is toggled in JS: CSS has no
   way to match an empty <input type="date">. The ::-webkit-datetime-edit rule is what
   actually recolours the hint in Chrome; plain `color` covers Firefox. */
.progress-table input.progress-date.is-empty,
.progress-table input.progress-date.is-empty::-webkit-datetime-edit {
  color: var(--color-text-muted);
  opacity: 0.7;
}
.progress-table input.progress-date.is-empty:focus,
.progress-table input.progress-date.is-empty:focus::-webkit-datetime-edit {
  color: var(--color-text);
  opacity: 1;
}

.progress-table td:last-child {
  width: 1%;
  white-space: nowrap;
}

/* Radio list in the "chọn milestone" modal (+ Thêm dòng with several open milestones). */
.progress-milestone-picker {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 12px 0;
}
.progress-milestone-option {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  cursor: pointer;
}
/* A destination with no open milestone can't take a new row — still listed, so it's clear
   the folder exists and why it can't be picked. */
.progress-milestone-option.is-disabled {
  color: var(--color-text-muted);
  cursor: not-allowed;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
}
.data-table th,
.data-table td {
  text-align: left;
  padding: 10px 14px;
  border-bottom: 1px solid var(--color-border);
  font-size: 13px;
}
.data-table th {
  color: var(--color-text-muted);
  font-weight: 600;
  background: var(--color-bg);
}
.data-table tbody tr:last-child td {
  border-bottom: none;
}

/* --- Admin page -------------------------------------------------------------------- */

.admin-section {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  padding: 20px;
  margin-bottom: 20px;
}
.admin-section h1 {
  font-size: 16px;
  margin: 0 0 14px;
}
.admin-section h3 {
  font-size: 13px;
  margin: 0 0 8px;
  color: var(--color-text-muted);
}

.permission-columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}

.permission-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.permission-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 13px;
}
.permission-list li:last-child {
  border-bottom: none;
}

/* --- Art preview modal (Folder page, PLAN.md P4) ----------------------------------- */

/* Size override for openModal()'s default small (360px) modal — applied via classList.add
   after creation, core.js itself is untouched. Title + close button stay pinned while the
   body scrolls (Huy, 2026-07-22): the MODAL BOX itself no longer scrolls — only
   .modal-body does — so .modal-header, sitting outside that scrolling region entirely,
   never moves. Simpler and more robust than position:sticky on the header (which would
   need extra math to account for .modal's own 20px padding). */
.modal-lg {
  max-width: 1100px;
  width: 90vw;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.modal-lg .modal-header {
  flex: 0 0 auto;
}
.modal-lg .modal-body {
  flex: 1 1 auto;
  min-height: 0; /* lets a flex child actually shrink below its content size and scroll */
  overflow-y: auto;
}

/* --- Full-screen image preview (Huy 2026-08-27) ------------------------------------
   Fills the viewport and centres the image inside whatever is left after the header.
   `max-width: none` is needed to beat `.modal`'s own 360px cap, and `min-height: 0` so the
   body can shrink below the image's intrinsic size instead of pushing past the modal. */
.modal-image-preview {
  max-width: none;
  width: 96vw;
  height: 96vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.modal-image-preview .modal-header {
  flex: 0 0 auto;
}
.modal-image-preview .modal-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  border-radius: var(--radius);
}
.image-preview-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: block;
}
/* Only when the image is drawn bigger than its own pixels — see openImagePreviewModal. */
.image-preview-img.is-upscaled {
  image-rendering: pixelated;
}

.modal-header-left {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  row-gap: 6px;
  min-width: 0;
  flex: 1 1 auto;
}
.modal-header-left h2 {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.art-modal-header-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

.art-modal-controls-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 12px;
}

.art-modal-zoom-row {
  display: flex;
  gap: 4px;
}
/* Small tier reference (see the 4-size comment above .btn-primary) — .icon-btn matches
   this exact padding/font-size/weight; only the "active" state below is zoom-specific. */
.art-modal-zoom-btn {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 4px 10px;
  font-size: 12px;
  font-weight: 600;
  line-height: 16px;
  color: var(--color-text);
}
.art-modal-zoom-btn:hover {
  background: var(--color-bg);
}
.art-modal-zoom-btn.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

/* ◀/▶ prev-next art buttons, between the modal title and its tag chips (Huy 2026-07-27).
   Small tier (.icon-btn) since they repeat as a pair next to a title, same as every other
   icon-only control in the app. */
.art-modal-art-nav {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  /* No margin: .modal-header-left is a flex row with its own 10px gap. */
}

/* Round Back/Next on the art modal's two edges (Huy 2026-10-07). Children of the overlay, not
   of .modal (which clips overflow); art-modal.js sets `left` to the modal's left / right edge
   and translate(-50%) centres the circle ON that edge, so exactly half sits over the modal.
   top: 50% is the modal's own middle because the overlay centres it vertically. */
.art-modal-side-nav {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
}
.art-modal-side-nav svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.art-modal-side-nav:hover:not(:disabled) {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
/* Same "you're at the end of the list" look as the header pair's .icon-btn:disabled. */
.art-modal-side-nav:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.art-modal-guides-row {
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: 12px;
  color: var(--color-text);
}
/* Small number box sitting right after its own guide checkbox (safe-zone height, ruler
   size) — pulled back against that checkbox with a negative margin so it reads as part of
   the same control instead of a third item in the row's 16px rhythm.

   Every selector here is `.art-modal-guides-row input.art-modal-guide-num`, NOT the bare
   class: `.art-modal-guides-row input { width: auto; margin: 0 }` further down this file is
   both more specific (class + element) and later, so a bare `.art-modal-guide-num` width
   never applied at all — the boxes sat at the browser's default width the whole time
   (Huy 2026-07-27: "hai ô textbox vẫn rất lớn, không thay đổi"). Adding the element
   qualifier here outranks it for good, whatever the order ends up being. */
.art-modal-guides-row input.art-modal-guide-num {
  width: 39px; /* 29px was too tight once actually applied; +10px (Huy 2026-07-27) */
  margin: 0 0 0 -10px;
  padding: 3px 6px;
  font-size: 12px;
  line-height: 16px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-surface);
  color: var(--color-text);
}
/* Spinner arrows would eat ~15px of a box this narrow, leaving little room for the value.
   The value is typed, not stepped, so drop them. */
.art-modal-guides-row input.art-modal-guide-num[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}
.art-modal-guides-row input.art-modal-guide-num[type="number"]::-webkit-outer-spin-button,
.art-modal-guides-row input.art-modal-guide-num[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
/* The ruler box holds "10,100" — six characters, so it stays 10px wider than the safe-zone
   box next to it rather than clipping the value. Splitting it into two boxes would be the
   way to make it as narrow as that one; say the word and it can be. */
.art-modal-guides-row input.art-modal-guide-num-wide {
  width: 56px; /* 46px + the same 10px */
}
.art-modal-guides-row label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 400;
  cursor: pointer;
}
.art-modal-guides-row input {
  width: auto;
  margin: 0;
}

/* Frame panel OVERLAYING the top of the canvas (Huy 2026-07-28, made a true overlay
   2026-07-29: collapsing it must not move the art). The dock is a zero-height sticky box,
   so it takes no space in the flow — the canvas sits where it always did — while staying in
   the corner as a zoomed canvas scrolls under it on either axis. */
.art-modal-frame-dock {
  position: sticky;
  top: 0;
  left: 0;
  height: 0;
  z-index: 5;
}
.art-modal-frame-panel {
  position: absolute;
  /* The dock already starts at the viewport's content box, i.e. 12px in from its edges
     (.art-modal-canvas-viewport's own padding), so zero offsets here put the panel exactly
     one padding-width from the top and left edges — the gap Huy asked it to match. */
  top: 0;
  left: 0;
  max-width: 100%;
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 6px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.94);
  box-shadow: var(--shadow);
}
/* Frame tooltip: the app's own bubble style (.comment-tooltip-bubble, shared with the 💬
   indicator on art cards) rather than a native title=, so tooltips match everywhere. Sits
   BELOW the panel — the panel hugs the top of the canvas, and there's room under it — and
   is positioned by JS because the strip it belongs to scrolls sideways and would clip it. */
.art-modal-frame-tip {
  bottom: auto;
  top: calc(100% + 6px);
  z-index: 6;
}
.art-modal-frame-tip.visible {
  visibility: visible;
  opacity: 1;
}

/* Play (primary) above collapse, in their own column at the panel's left edge. */
.art-modal-frame-panel-col {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 0 0 auto;
}
.art-modal-frame-play.btn-primary {
  /* Primary colour, compact box — a full-size Primary button would tower over the strip. */
  padding: 4px 10px;
  font-size: 12px;
  line-height: 16px;
}
/* Collapsed: the strip folds away and the panel shrinks to that little column in the
   corner. Play stays — pausing is the one thing you still want while collapsed. */
.art-modal-frame-panel.collapsed .art-modal-frame-strip {
  display: none;
}
.art-modal-frame-panel.collapsed {
  width: fit-content;
}
.art-modal-frame-strip {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  flex: 1 1 auto;
  padding-bottom: 2px;
}
.art-modal-frame-item {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 2px solid transparent;
  border-radius: 6px;
  background: var(--color-surface);
  cursor: pointer;
}
.art-modal-frame-item.selected {
  border-color: var(--color-primary);
}
.art-modal-frame-item img {
  width: 44px;
  height: 44px;
  object-fit: contain;
  image-rendering: pixelated;
  background: var(--color-bg);
  border-radius: 4px;
}
.art-modal-frame-dur {
  font-size: 10px;
  color: var(--color-text-muted);
  line-height: 12px;
}
.art-modal-frame-play,
.art-modal-frame-collapse {
  flex: 0 0 auto;
}

.art-modal-canvas-viewport {
  width: 100%;
  max-height: 70vh;
  overflow: auto;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  margin-bottom: 12px;
  padding: 12px;
}

.art-modal-canvas-spacer {
  /* Sized in JS to CANVAS_W/H * zoom so the scrollable area matches the visually scaled
     canvas even though CSS transform:scale() doesn't itself affect layout size. Height is
     NOT fixed on the viewport itself — it grows with the spacer up to max-height above, so
     no scrollbar appears until the scaled canvas actually exceeds that box.
     Centering uses margin: 0 auto (NOT a flex justify-content: center on the viewport) —
     with overflow: auto, a flex-centered child clips its start-side overflow once it grows
     past the container, while an auto-margined block child keeps both edges reachable by
     scroll. */
  position: relative;
  margin: 0 auto;
}

.art-modal-canvas {
  width: 320px;
  height: 240px;
  position: relative;
  overflow: hidden;
  /* Checkerboard "transparent" background (like Photoshop) instead of solid white.
     `--checker-tile` is a full 2-cell tile size set by applyZoom() in art-modal.js /
     file-editor.js so the ON-SCREEN cell size is max(4, zoom)px regardless of the
     canvas's own transform: scale(zoom) — see the comment there for the exact formula.
     8px fallback (-> 4px on-screen cell) covers the brief instant before JS runs. */
  background: conic-gradient(#e5e5e5 25%, #fff 0 50%, #e5e5e5 0 75%, #fff 0) 0 0 / var(--checker-tile, 8px) var(--checker-tile, 8px);
  border: 1px solid var(--color-border);
  transform-origin: 0 0;
}

.art-modal-file-frame {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  outline: 1px solid #1184ff;
}
.art-modal-file-frame img {
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
}

.art-modal-safezone {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 32px;
  background: rgba(220, 38, 38, 0.2);
  pointer-events: none;
}

.art-modal-centerline {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  transform: translateX(-50%);
  /* Bright red (Huy 2026-07-27) — the old navy #1e3a8a sat too close to the skyblue file
     outline to pick out at a glance. Same element/class in both canvas modals, so this one
     rule covers art detail and the file editor. */
  background: #ff0000;
  pointer-events: none;
}

/* Ruler guide (Huy 2026-07-27) — a sized rectangle you DRAG over the character to measure
   or line things up, so unlike the other two guides it must receive pointer events. Violet
   to stay distinct from the red safe-zone/center line and the skyblue file outline. */
.art-modal-ruler {
  position: absolute;
  box-sizing: border-box;
  border: 1px solid #7c3aed;
  background: rgba(124, 58, 237, 0.25);
  cursor: move;
  touch-action: none; /* let pointermove drive the drag instead of the browser scrolling */
}

/* Title + "Upload version mới" share one row now (Huy, 2026-07-23: was stacked as two
   separate blocks before) — same title+button side-by-side pattern as
   .art-modal-comment-header below. */
.art-modal-version-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}
.art-modal-version-title {
  margin: 0;
}

.art-modal-version-strip {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding: 6px;
  margin-bottom: 6px; /* the info line below carries the rest of the gap */
}

/* One-line facts about the version currently selected in the strip (Huy 2026-07-27):
   upload date | uploader | measured playback duration | the duration the art is supposed to
   have. Pipe separators are their own spans so they can be dimmed independently. */
.art-modal-version-info {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 0 6px;
  margin-bottom: 16px;
  font-size: 12px;
  color: var(--color-text-muted);
}
.art-modal-version-info:empty {
  display: none;
}
.art-modal-version-info-sep {
  opacity: 0.45;
}
/* Measured duration that doesn't meet the art's required one (exact / Max / Min) — Huy
   2026-07-27. Bold as well as red so it doesn't rely on colour alone. */
.art-modal-duration-bad {
  color: var(--color-danger);
  font-weight: 700;
}

.art-modal-version-item {
  /* Anchor for the ✏️ button / "New" badge (both direct children, absolutely positioned —
     see .art-modal-version-badge / .art-modal-version-edit-btn below). This box has no
     overflow rule, so nothing positioned inside it (with non-negative insets) can ever be
     clipped by it — unlike .art-modal-version-thumb-wrap just below, which DOES clip
     (overflow: hidden, for the thumbnail's object-fit crop) and used to be where these two
     elements lived, corners cut off. */
  position: relative;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  background: var(--color-surface);
  border: 2px solid transparent;
  border-radius: 6px;
  padding: 3px;
  width: 98px; /* +20% again (68 -> 82 -> 98), Huy 2026-07-27 */
  cursor: pointer;
}
.art-modal-version-item:hover {
  background: var(--color-bg);
}
.art-modal-version-item.selected {
  border-color: var(--color-primary);
}

.art-modal-version-thumb-wrap {
  position: relative;
  width: 86px; /* +20% again (60 -> 72 -> 86), matching .art-modal-version-item */
  height: 86px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  border-radius: 4px;
  overflow: hidden;
}
.art-modal-version-thumb-wrap img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  image-rendering: pixelated;
}
.art-modal-version-badge {
  /* Positioned relative to .art-modal-version-item (see its `position: relative` above),
     NOT .art-modal-version-thumb-wrap — small POSITIVE inset so it sits fully inside the
     item's own box (top-right corner) with zero chance of being clipped. */
  position: absolute;
  top: 2px;
  right: 2px;
  background: var(--color-primary);
  color: #fff;
  font-size: 8px;
  font-weight: 700;
  line-height: 1;
  padding: 2px 3px;
  border-radius: 3px;
}

.art-modal-version-label {
  font-size: 10px;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 82px;
}

/* Đặt làm Final / Xoá version / Upload version mới — same row (Huy, 2026-07-23: moved the
   first two up from their own row below the version strip to sit together with Upload,
   in that left-to-right order). */
.art-modal-upload-row {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
}
.art-modal-upload-row button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Fields row (Description / Status / Tags), directly above the Comment section.
   Description fills the remaining width; Status and Tags stay compact. */
.art-modal-fields-row {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  flex-wrap: wrap;
  /* No bottom margin on purpose (Huy 2026-07-27: "khoảng cách tới Comment lớn hơn hẳn so
     với Comment -> So sánh"). Each field already reserves a line under its input for the
     hidden "Đã lưu" hint (4px column gap + ~13px of text), so ~17px of air is baked in —
     right about the 16px every .art-modal-section uses between each other. Adding the old
     20px on top of that is what made this one gap almost twice as wide as its neighbour. */
  margin-bottom: 0;
}
.art-modal-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.art-modal-field-description {
  flex: 1 1 260px;
  min-width: 200px;
}
.art-modal-field-status {
  flex: 0 0 auto;
}
.art-modal-field-tags {
  flex: 0 1 240px;
  min-width: 160px;
}
.art-modal-field-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

/* Plain <input> fields living outside a <form> (Description / Tags) — same visual
   language as `form input` but scoped to this class so it doesn't affect other forms. */
.art-modal-text-input {
  display: block;
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font-family: inherit;
  /* Same explicit 14px/20px pair as every other text control in the app (form
     input/select/textarea, .control) — this is the reference height every button/textbox/
     dropdown is now built to match (9px padding + 1px border ×2 + 20px line-height = 40px,
     see the comment above .btn-primary). */
  font-size: 14px;
  line-height: 20px;
}
.art-modal-text-input:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
}
/* A non-admin gets these fields as `readonly`, not `disabled` — a disabled input can't be
   focused OR scrolled, so a long description was unreadable past the field's width (Huy
   2026-07-29). Same muted look either way; readonly keeps a text caret because the point is
   that you CAN scroll, select and copy it. */
.art-modal-text-input:disabled,
.art-modal-text-input[readonly] {
  background: var(--color-bg);
  color: var(--color-text-muted);
}
.art-modal-text-input:disabled {
  cursor: not-allowed;
}
.art-modal-text-input[readonly] {
  cursor: text;
}

/* The read-only status pill (role `user`) in the art modal. The admin/mod side of this rule
   used to cover a <select> too; that became 4 buttons on 2026-08-15, which bring their own
   box model, so only the pill is styled here now. Scoped to .art-modal-field-status so the
   Folder page's own compact .status-pill is untouched. */
.art-modal-field-status .status-pill {
  display: inline-flex;
  align-items: center;
  padding: 9px 11px;
  font-size: 14px;
  line-height: 20px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}
.art-modal-field-status .status-pill {
  padding-right: 11px; /* a pill has no arrow to clear */
}

/* "Đã lưu" hint — appears instantly on save, then fades out. */
.art-modal-save-hint {
  display: inline-block;
  font-size: 11px;
  color: var(--color-success);
  opacity: 0;
  transition: opacity 0.8s ease;
  pointer-events: none;
}
.art-modal-save-hint.visible {
  opacity: 1;
  transition: opacity 0.15s ease;
}

.art-modal-section {
  margin-bottom: 16px;
}
.art-modal-section h3 {
  margin: 0 0 8px;
}

.art-modal-comment-meta {
  font-size: 11px;
  margin-top: 6px;
}

/* Comment textarea lives outside a <form> too — give it the same radius/border/padding
   language as the rest of the app instead of relying on browser defaults. Scoped to this
   class only so other textareas (which already get styled via the `form textarea` rule)
   are untouched. */
.art-modal-comment-textarea {
  display: block;
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font-family: inherit;
  font-size: 13px;
  resize: vertical;
}
.art-modal-comment-textarea:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
}
.art-modal-comment-textarea:disabled {
  background: var(--color-bg);
  color: var(--color-text-muted);
  cursor: not-allowed;
}

/* --- Art comment table (comment revamp, July 2026) ---------------------------------
   Replaces the single-textarea Comment block above (.art-modal-comment-textarea/-meta,
   left in place but now unused — nothing else in the app renders those two classes
   anymore) with a per-art comment LIST. ".art-modal-comment-header" is the same
   title+button side-by-side row as .cm-compare-header-row (compare-merge.js); the table
   itself borrows the metadata-table "spreadsheet cell" language (borderless/transparent
   inputs, inset focus ring, full grid) but is its OWN class — a different modal, different
   columns/permissions, so the two must not share style leakage if either changes later. */
.art-modal-comment-header {
  /* Containing block for the history popover — see .comment-history-popover. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}
.art-modal-comment-header .section-title {
  margin: 0;
}

/* The comment table needs no header row (Huy 2026-07-28): four columns of one obvious
   kind each — the text, who wrote it, when, and the delete button. */
.art-comment-content-text {
  padding: 8px 10px;
  font-size: 14px;
  white-space: pre-wrap;
  word-break: break-word;
}
.art-comment-content-input {
  display: block;
  width: 100%;
  margin: 0;
  padding: 8px 10px;
  border: none;
  border-radius: 0;
  background: transparent;
  font-family: inherit;
  /* 14px/20px — the same pair as the Description field right above this section (Huy
     2026-07-28); the comment text was a size smaller than the field it sits under. */
  font-size: 14px;
  line-height: 20px;
  color: var(--color-text);
  resize: vertical;
  min-height: 36px;
  line-height: 1.4;
}
.art-comment-content-input:focus {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--color-primary);
}

/* --- File edit modal (Folder/Art modal, PLAN.md P5) -------------------------------- */
/* Canvas/guides classes (.art-modal-canvas, -viewport, -spacer, -safezone, -centerline,
   -controls-row, -zoom-row/-btn, -guides-row) are reused as-is from the block above —
   only the file-editor-specific bits (draggable content clip + crop rect + frame strip +
   tools) get new classes here. */

.file-editor-mode-note {
  margin: 0 0 12px;
}
.file-editor-hint {
  margin: 6px 0 12px;
  font-size: 11px;
}

.file-editor-file-frame {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  /* No static outline here anymore — .file-editor-outline-rect below is now the SINGLE
     source of truth for the output boundary (item D.12), live-positioned in JS to either
     the full file bounds (not cropped) or the crop rect (cropped). */
}

.file-editor-clip {
  position: absolute;
  top: 0;
  left: 0;
  /* No overflow:hidden here (Huy feedback): dragging content past the file's own bounds
     must stay fully VISIBLE, not silently clipped — the box-shadow dim mask on
     .file-editor-outline-rect below is what communicates "this will be lost if saved
     without cropping/padding to include it". */
}
.file-editor-clip img {
  position: absolute;
  top: 0;
  left: 0;
  image-rendering: pixelated;
  cursor: grab;
  user-select: none;
}

/* The output-boundary outline (item D.12) — replaces the old dashed-red
   .file-editor-crop-rect AND the file-frame's static skyblue outline with one element:
   always visible, positioned/sized in file coordinates by renderOutline() in
   file-editor.js. Solid skyblue border (2px, matches the app's existing "file boundary"
   color convention) so it reads clearly as "this is what gets saved". */
.file-editor-outline-rect {
  position: absolute;
  pointer-events: none;
  /* `outline` (not `border`): the app's global `box-sizing: border-box` reset means a
     `border` here would draw INSIDE this element's own declared width/height, eating 1-2px
     into the crop area itself. `outline` always draws OUTSIDE the border-box without
     affecting it — exactly "bao bên ngoài vùng sẽ crop, chứ không phải inside" (Huy). */
  outline: 1px solid #1184ff;
  /* Dim mask: darkens everything OUTSIDE this rect so content dragged past the output
     boundary previews as "will be lost" instead of just disappearing. Clipped to the
     ancestor .art-modal-canvas's own overflow:hidden (320x240), so the huge spread simply
     fills that visible workspace — no JS math needed, and it tracks renderOutline()'s
     positioning automatically since it's just a box-shadow on this same element. */
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
}

.file-editor-strip-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
}
.file-editor-frame-strip {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding-bottom: 4px;
  flex: 1 1 auto;
  min-width: 0;
}
.file-editor-frame-item {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  background: var(--color-surface);
  border: 2px solid transparent;
  border-radius: 6px;
  padding: 3px;
  width: 48px;
}
.file-editor-frame-item:hover {
  background: var(--color-bg);
}
.file-editor-frame-item.selected {
  border-color: var(--color-primary);
}
.file-editor-frame-item img {
  width: 100%;
  height: 30px;
  object-fit: contain;
  image-rendering: pixelated;
  background: var(--color-bg);
  border-radius: 4px;
}
.file-editor-frame-label {
  font-size: 9px;
  color: var(--color-text-muted);
}

.file-editor-tools {
  margin-bottom: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.file-editor-tool-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  font-size: 13px;
}
.file-editor-tool-row label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 400;
}
.file-editor-tool-row input[type="number"] {
  width: 70px;
  padding: 4px 6px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-surface);
  color: var(--color-text);
}
/* Frame tools row (Huy 2026-07-29) — sits above the position/pad tools and acts on the
   frame currently selected in the strip. */
/* "Frame 9th (Từ 1200ms - 1350ms)" — bold, per Huy, and wide enough not to reflow the row
   as the numbers change. */
.file-editor-frame-label-line {
  font-weight: 700;
  min-width: 210px;
}

.file-editor-frame-tools {
  padding-bottom: 10px;
  border-bottom: 1px solid var(--color-border);
}
/* A hidden frame stays in the strip (it can be brought back) but reads as excluded: dimmed,
   dashed outline, and its label says "ẩn" instead of a duration. */
.file-editor-frame-item.is-hidden-frame {
  opacity: 0.45;
}
.file-editor-frame-item.is-hidden-frame img {
  outline: 1px dashed var(--color-text-muted);
  outline-offset: -1px;
}

.file-editor-crop-row label {
  font-size: 12px;
}
/* dx/dy offset boxes: they share the Move checkbox + "Căn giữa ngang" row (Huy 2026-07-27),
   so they're narrower than the pad inputs to keep that row on one line. */
.file-editor-offset-field {
  gap: 4px;
}
.file-editor-tool-row input.file-editor-num-sm {
  width: 56px;
}

/* Move tool (checkbox "Di chuyển") — gates drag + arrow-key repositioning, OFF by default.
   ON: both the canvas border and this chip turn amber — a clear "you're in an editing
   mode, dragging/arrows will move content" cue (Huy feedback). */
/* Plain label, no chip (Huy 2026-07-29: "Bỏ outline"). The checkbox itself carries the
   state — muted while off, normal text colour once on — instead of an amber box. The canvas
   keeps its own amber border as the "you are in move mode" cue. */
.file-editor-move-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  font-size: 13px;
  color: var(--color-text-muted);
  cursor: pointer;
  width: fit-content;
}
.file-editor-move-toggle.active {
  color: var(--color-text);
}
.file-editor-canvas.move-active {
  border-color: #ca8a04;
  border-width: 2px;
}

.file-editor-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}
.file-editor-actions-right {
  display: flex;
  gap: 10px;
}

/* Version strip edit (✏️) button — art-modal-version-item is a <div> (not <button>) so it
   can host this as a real nested <button> without invalid HTML nesting. Top-left corner so
   it doesn't collide with the "New" badge (top-right, see .art-modal-version-badge).
   Positioned relative to .art-modal-version-item (its `position: relative`), NOT
   .art-modal-version-thumb-wrap — small POSITIVE inset so it sits fully inside the item's
   own box with zero chance of being clipped (see the item's own comment above).
   This IS the app's "Mini" size tier (see the 4-size comment above .btn-primary) — the
   smallest of the 4, reserved for tiny corner-overlay badges like this one (paired with
   the "New" badge opposite it on the same 86x86px thumbnail), not toolbar/action
   buttons. */
.art-modal-version-edit-btn {
  position: absolute;
  top: 2px;
  left: 2px;
  /* Small tier now (.icon-btn supplies padding/font-size) rather than the Mini badge it
     used to be — the cards are 98px wide since 2026-07-28, so it fits (Huy). Rendered on
     every card but only shown on the selected one, so the strip stays uncluttered while
     any version can be edited, not just the Final. */
  display: none;
}
.art-modal-version-item.selected .art-modal-version-edit-btn {
  display: inline-flex;
}


/* --- Compare + Merge (Art preview modal, PLAN.md P6) -------------------------------
   Kept separate from the P4 rules above: this whole block belongs to
   static/assets/compare-merge.js, mounted at art-modal.js's P6 marker. */

.cm-empty {
  color: var(--color-text-muted);
  font-size: 13px;
}

.cm-grid {
  /* One horizontal row of fixed-width 320px frames — scrolls sideways instead of
     wrapping, for both the Compare grid and the Merge grid (same class, same rule). */
  display: flex;
  flex-wrap: nowrap;
  gap: 16px;
  overflow-x: auto;
  padding-bottom: 4px;
}

.cm-frame-card {
  flex: 0 0 320px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Same visual language as .art-modal-canvas (P4): white background, light border,
   content anchored bottom-center at natural size, pixelated. No zoom/guides here — just
   a static 320x240 comparison/preview frame. */
.cm-frame {
  width: 320px;
  height: 240px;
  position: relative;
  overflow: hidden;
  background: #ffffff;
  border: 1px solid var(--color-border);
  border-radius: 6px;
}

.cm-frame-layer {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
}
.cm-frame-layer img {
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
}
/* Merge frames stack up to 3 layers — stacking order must hold even when a layer in the
   middle is null (DOM order alone isn't reliable once elements are conditionally
   omitted), so pin z-index by layer identity: 1 background (bottom) / 2 character /
   3 action (top), per ARCHITECTURE §5. */
.cm-frame-layer.cm-layer-background {
  z-index: 1;
}
.cm-frame-layer.cm-layer-character {
  z-index: 2;
}
.cm-frame-layer.cm-layer-action {
  z-index: 3;
}

/* Card label row: folder name (bold, Compare only) · art name(s), one wrapping line — no
   tag chips (Huy 2026-07-23: dropped the combined folder+art tag chips this used to also
   show, per item C.5's original design). Shared between Compare (one art name, plus its
   folder name) and Merge (up to 3 stacked art names, one per enabled layer, no folder
   name — added 2026-07-23, see loadMergeSample()). */
.cm-frame-label-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  row-gap: 4px;
}
.cm-frame-folder-name {
  font-weight: 700;
}
/* Only rendered when a Compare card comes from ANOTHER project (the "Project" column of the
   filter panel, 2026-07-27) — bold like the folder name so the two read as one path, muted
   so the folder still wins the eye. */
/* Merge cards stack one "folder · art" line per ADDED layer (Huy 2026-07-28), so their
   label block is a column, unlike Compare's single inline line. */
.cm-frame-label-stack {
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
.cm-frame-label-line {
  display: flex;
  align-items: center;
  gap: 4px;
}

.cm-frame-project-name {
  font-weight: 700;
  color: var(--color-text-muted);
}
.cm-frame-sep {
  color: var(--color-text-muted);
}
.cm-frame-art-name {
  color: var(--color-text-muted);
}
/* Compare only: both names are links (Huy 2026-10-02). They keep the label row's own colors
   instead of turning link-blue — the row reads as a path, not as prose with links in it —
   and announce themselves on hover the same way `a.crumb` does. Scoped to `a.` so Merge's
   label stack, which renders the very same classes as plain <span>, is untouched.
   `a.cm-frame-folder-name` needs the explicit color: the span inherits body text, but an
   <a> would otherwise pick up the global `a { color: var(--color-primary) }`. The art name
   needs no such rule — `.cm-frame-art-name` (0,1,0) already outranks that `a` (0,0,1). */
a.cm-frame-folder-name {
  color: var(--color-text);
}
a.cm-frame-folder-name:hover,
a.cm-frame-art-name:hover {
  color: var(--color-primary);
}
/* The current-art card (always first in the Compare grid) gets a light primary-colored
   frame border so it reads as "this one" at a glance (Huy 2026-07-23: dropped the
   "(hiện tại)" text badge that used to sit next to it — the border alone reads clearly). */
.cm-frame-card-current .cm-frame {
  border-color: var(--color-primary);
  border-width: 2px;
}
.cm-frame-empty-note {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
  font-size: 12px;
  text-align: center;
  padding: 0 16px;
}

/* Compare section header row: title + "Tag ▾" filter button (item C.7) side by side. */
.cm-compare-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}
.cm-compare-header-row .section-title {
  margin: 0;
}

.cm-frame-note {
  font-size: 11px;
  color: var(--color-text-muted);
  font-style: italic;
}

.cm-merge-header-row {
  /* Was .cm-merge-toggle-row, holding the section title plus an on/off checkbox. Merge is
     always on since 2026-08-15, so the checkbox is gone and with it the `label` rule that
     styled it; the row is just the title now, and the name no longer claims otherwise. */
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.cm-merge-header-row h3 {
  margin: 0;
}

.cm-merge-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 18px;
  margin-bottom: 12px;
}

.cm-merge-layer-control {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
}
.cm-merge-layer-control label {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}
.cm-merge-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}
/* "Toàn bộ" / "Một vài" mode buttons (Huy feedback, 2026-07-23 — replaced the old single
   toggle button whose label flipped between the two): both are always-visible
   `.btn-secondary`s, and whichever matches the CURRENT mode gets this "active" look — same
   primary-bg/white-text pattern as `.art-modal-zoom-btn.active` above, just applied to a
   Normal-tier button here instead of Small-tier, since these already sat at Normal size. */
.cm-mode-btn.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.cm-mode-btn.active:hover {
  background: var(--color-primary-hover);
}

/* --- Merge layer tag filter: "Tag ▾" button + 2-column checkbox panel + Confirm --------
   Same floating-dropdown language as .tag-filter-wrap/.tag-filter-dropdown (Folder page),
   just two checkbox columns side by side instead of one, plus a Confirm button at the
   bottom — ticking a checkbox never re-fetches by itself, only Confirm does (see
   compare-merge.js's wireTagPanel). */
.cm-tag-filter-wrap {
  position: relative;
}

.cm-tag-filter-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 10px;
  z-index: 30;
}

/* Compare's own "Tag ▾" button sits at the modal's far-right edge (.cm-compare-header-row
   is `justify-content: space-between`, title left / button right — see below), so opening
   this ~300-370px-wide panel LEFT-aligned (the shared default above) ran it straight past
   the modal's right edge, clipped by .modal-lg's overflow (Huy feedback: "cắt 80%").
   Merge's per-layer "Tag ▾" buttons (same .cm-tag-filter-dropdown class) don't have this
   problem: they sit left-of-center in .cm-merge-controls, since it's the Random/mode-toggle
   actions that get pushed to the far right there (.cm-merge-actions's margin-left: auto) —
   so only Compare's own wrap is overridden to open RIGHT-aligned (expanding leftward)
   instead; Merge keeps the shared left-aligned default. */
#cm-compare-tag-wrap .cm-tag-filter-dropdown {
  left: auto;
  right: 0;
}

.cm-tag-filter-columns {
  display: flex;
  gap: 14px;
}

.cm-tag-col {
  min-width: 130px;
  max-width: 180px;
}

.cm-tag-col-header {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-muted);
  margin-bottom: 6px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--color-border);
}

.cm-tag-col-list {
  max-height: 220px;
  overflow-y: auto;
}

.cm-tag-col-empty {
  font-size: 12px;
  margin: 0;
}

.cm-tag-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 400;
  white-space: nowrap;
  cursor: pointer;
}
.cm-tag-option:hover {
  background: var(--color-bg);
}
.cm-tag-option input {
  width: auto;
  margin: 0;
}

.cm-tag-filter-actions {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--color-border);
  display: flex;
  justify-content: flex-end;
}
/* --- Metadata type modal (Folder page, admin) --------------------------------------- */

.metadata-modal-toolbar {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 10px;
}
.metadata-modal-toolbar .inline-label {
  flex: 1 1 260px;
  min-width: 0;
  /* .inline-label carries its own 12px bottom margin (meant for standing alone above a
     block). With the row's align-items: flex-end that margin lifts the SELECT 12px above the
     buttons beside it — which read as the buttons sitting too low (Huy 2026-07-29). Same fix
     .art-list-controls needed for the same reason. */
  margin-bottom: 0;
}
.metadata-modal-toolbar .inline-label select {
  width: 100%;
}

/* Name of the selected type + the row actions beside it (Huy 2026-07-29 flow). Replaces the
   old "new type name + Tạo/Huỷ" row, which is gone with that flow. */
.metadata-name-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.metadata-name-row input {
  flex: 1 1 auto;
}

/* Spreadsheet look (Huy: "trông chẳng giống sheet excel gì cả"). Root cause: this table's
   <input>/<textarea> cells are NOT inside a <form> (unlike every other modal in the app),
   so none of the app's normal form-control styling applied — they were rendering with
   bare, inconsistent browser-native chrome. Fix: give the TABLE's own cell borders the
   grid (vertical + horizontal, not just row-bottom like .data-table elsewhere) and strip
   the inputs down to borderless/transparent so they blend into their cell — a focused
   cell gets an inset ring instead of the usual outer form-input ring, reading as "selected
   cell" rather than "a text box sitting inside a table". */
.metadata-table-wrap {
  max-height: 46vh;
  overflow-y: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}
.metadata-table-wrap .data-table {
  border: none;
  border-radius: 0;
}
.metadata-table th,
.metadata-table td {
  padding: 0;
  border-right: 1px solid var(--color-border);
}
.metadata-table th:last-child,
.metadata-table td:last-child {
  border-right: none;
}
.metadata-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 9px 10px;
}
.metadata-table tbody tr:nth-child(even) {
  background: var(--color-bg);
}
.metadata-table th:nth-child(1),
.metadata-table td:nth-child(1) {
  width: 22%;
}
.metadata-table th:nth-child(3),
.metadata-table td:nth-child(3) {
  width: 16%;
}
.metadata-table th:nth-child(4),
.metadata-table td:nth-child(4) {
  width: 36px;
}
.metadata-table td:nth-child(4) {
  text-align: center;
}
.metadata-table td input[type="text"],
.metadata-table td textarea {
  display: block;
  width: 100%;
  margin: 0;
  padding: 8px 10px;
  border: none;
  border-radius: 0;
  background: transparent;
  font-family: inherit;
  font-size: 13px;
  color: var(--color-text);
}
.metadata-table td input[type="text"]:focus,
.metadata-table td textarea:focus {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--color-primary);
}
.metadata-table td textarea {
  resize: vertical;
  min-height: 36px;
  line-height: 1.4;
}
.metadata-table td:nth-child(4) .icon-btn {
  margin: 6px auto;
}

.metadata-modal-actions {
  justify-content: space-between;
}

/* Xoá account sits opposite Lưu rather than beside it — a destructive action shouldn't be
   the neighbour your thumb lands on (Huy 2026-07-29). */
.user-modal-actions {
  justify-content: space-between;
}

/* Admin > Backup dữ liệu (Huy 2026-07-30) — toggle + chu kỳ + "Backup ngay" on one row. */
.admin-backup-note {
  font-size: 12px;
  margin: 0 0 10px;
}
.admin-backup-row {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.admin-backup-row .inline-label {
  margin-bottom: 0; /* .inline-label carries 12px, which would tilt this row (see 2026-07-30) */
}
input.admin-backup-hours {
  width: 64px;
}

/* Ghi chú dưới tiêu đề "Phân quyền project" — chỉ freelancer cần gán (Huy 2026-07-30). */
.permission-note {
  font-size: 12px;
  margin: 0 0 10px;
}

/* Download nằm NGAY CẠNH title "Các version" (Huy 2026-07-30). Header này là
   `justify-content: space-between`, nên nếu không có margin-right:auto thì 3 phần tử sẽ dàn
   đều và nút rơi ra giữa hàng thay vì đứng cạnh title. */
#art-modal-download-btn {
  margin-right: auto;
}

/* --- Sound detail + sound cards (project type `sounds`, Huy 2026-07-30) ------------------
   The art modal swaps its canvas block for this one when the art is a .mp3/.wav: the same
   waveform PNG the folder card shows, plus the browser's own audio player. No zoom, no frame
   strip, no safe-zone/center/ruler — none of them mean anything for a sound. */
.art-modal-sound {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 14px;
}
.art-modal-sound-wave {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}
.art-modal-sound-wave img {
  width: 100%;
  max-width: 640px;
  height: 160px;
  object-fit: contain;
}
.art-modal-sound-player {
  width: 100%;
}
/* Waveform in a square thumbnail slot: `contain` would letterbox a 4:3 image into a thin
   strip, so let it fill the width and sit centred instead. */
.art-thumb-sound img {
  object-fit: contain;
  padding: 2px;
}

/* Player + Loop trên một hàng: nút loop nằm ngay cạnh cụm control gốc (chỗ nút ⋮) của
   <audio controls> (Huy 2026-08-03). Player tự giãn, nút giữ nguyên bề rộng. */
.art-modal-sound-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.art-modal-sound-row .art-modal-sound-player {
  flex: 1 1 auto;
  min-width: 0;
}
.art-modal-loop-btn {
  flex: 0 0 auto;
  line-height: 16px;
}
/* Bật = nền primary, chữ trắng — cùng trạng thái active với .art-modal-zoom-btn.active. */
.art-modal-loop-btn.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}


/* Sort dropdown trong hàng header của /project (Huy 2026-08-03) — `.inline-label` mang sẵn
   `margin-bottom: 12px`, mà hàng này là flex `align-items: center`, nên không zero thì label
   bị đẩy lệch so với các nút bên cạnh (đúng lỗi đã gặp ở `.art-list-controls` và ở hàng
   metadata). `min-width: 220px` của `.inline-label select` cũng quá rộng cho 4 option ngắn. */
.page-header-actions .inline-label {
  margin-bottom: 0;
}
.page-header-actions .inline-label select {
  min-width: 0;
}

/* Sửa tên art ngay trên header của modal (Huy 2026-08-10): ◀/▶ trước tên, ✏️ ngay sau tên,
   bấm vào thì tên thành textbox và ✏️ thành ✓. .modal-header-left đã là flex row có gap 10px
   nên hai phần tử này không cần margin riêng. */
.art-modal-rename-btn {
  flex: 0 0 auto;
}
input.art-modal-rename-input {
  /* Rộng bằng khoảng chữ thường thấy của tên art, và co lại được trên màn hình hẹp thay vì
     đẩy nút ✓ ra khỏi header. */
  flex: 0 1 260px;
  min-width: 0;
  width: 260px;
  font-size: 18px;
  font-weight: 700;
  padding: 2px 8px;
}

/* --- Comment history popover (art modal, Huy 2026-08-15) --------------------------- */
.comment-history-wrap {
  position: relative;
  /* The header row is `display:flex` with the "+ Thêm" button pushed right; this sits
     between them, so it must not stretch or the popover anchors to a wide invisible box. */
  flex: 0 0 auto;
  margin-right: auto;
}
.comment-history-popover {
  position: absolute;
  /* Right-anchored so it expands LEFTWARD from the column's right edge, and capped at 100% of
     that column — the header is the containing block, so both refer to the column's width.
     Was `left: 0` against the button's own 30px wrap, which pushed 360px of list past the
     right edge and made the whole comment area scroll sideways (Huy 2026-08-15). */
  right: 0;
  top: calc(100% + 4px);
  width: 360px;
  max-width: 100%;
  max-height: 320px;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 4px;
  z-index: 30;
}
.comment-history-empty {
  margin: 0;
  padding: 8px 10px;
  font-size: 13px;
}
.comment-history-item {
  display: flex;
  align-items: flex-start;
  gap: 4px;
}
.comment-history-item:hover {
  background: var(--color-bg);
}
.comment-history-text {
  flex: 1;
  min-width: 0;
  display: block;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  padding: 7px 8px;
  font-size: 13px;
  line-height: 18px;
  color: var(--color-text);
}
.comment-history-content {
  display: block;
  /* Two lines then ellipsis: enough to recognize a phrase without one long comment
     taking over the whole popover. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.comment-history-meta {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.comment-history-open {
  flex: 0 0 auto;
  margin-top: 4px;
}
/* Word shared with the art currently open, in the history popover's art-name line
   (Huy 2026-08-15) — the point is to spot "this comment was about the same thing" fast, so
   it's the accent color plus weight, not a background that would fight the row hover. */
.comment-history-hit {
  color: var(--color-primary);
  font-weight: 700;
}
/* Collapsed version block in the art modal (Huy 2026-08-15) — the strip, the info line and
   the two selected-version buttons go; the toggle, Download and Upload stay. */
.art-modal-version-block.collapsed .art-modal-version-strip,
.art-modal-version-block.collapsed .art-modal-version-info,
.art-modal-version-block.collapsed .art-modal-version-expanded-only {
  display: none;
}
/* --- Art modal two-column layout (Huy 2026-08-15) --------------------------------------
   Left = the file (canvas, versions, description/tags, Compare/Merge). Right = the
   conversation (status, then comments). Each column scrolls on its own, so a long comment
   list no longer pushes the canvas out of view — which means the MODAL BODY must not scroll,
   or the inner scrollers never get a bounded height to scroll inside. */
.modal-lg.art-modal-wide {
  max-width: 1500px;
  width: 95vw;
}
.modal-lg.art-modal-wide .modal-body {
  /* Body stops scrolling AND becomes the flex parent of the columns row. `height: 100%` on
     .art-modal-cols was the first attempt and did not resolve — a percentage height needs a
     definite parent height, and the body's is decided by flex layout, so the columns grew to
     their content and the taller one (left) simply got clipped with no way to reach the rest
     (Huy: "bên trái chưa scroll được"). Making the body a flex column and letting the row
     flex to fill gives the columns a real height to scroll inside. */
  overflow-y: hidden;
  display: flex;
  flex-direction: column;
}
.art-modal-cols {
  display: flex;
  gap: 20px;
  align-items: stretch;
  flex: 1 1 auto;
  min-height: 0;
}
.art-modal-col-main {
  flex: 1 1 auto;
  /* min-width/min-height: 0 — without them a flex item refuses to shrink below its content,
     and the wide Compare/Merge grid would push the side column off the modal instead. */
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  padding-right: 4px;
}
.art-modal-col-side {
  flex: 0 0 380px;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  border-left: 1px solid var(--color-border);
  padding-left: 20px;
}
.art-modal-col-side .art-modal-field-status {
  margin-bottom: 16px;
}
.art-modal-col-side .art-modal-comment-section {
  margin-top: 0;
}
/* Narrow viewport: two columns of 380px+canvas don't fit, so stack them and give the scroll
   back to the modal body — one scrollbar beats two squeezed ones. */
@media (max-width: 1024px) {
  .modal-lg.art-modal-wide .modal-body {
    overflow-y: auto;
    display: block;
  }
  .art-modal-cols {
    flex-direction: column;
  }
  .art-modal-col-main,
  .art-modal-col-side {
    flex: 0 0 auto;
    overflow: visible;
    border-left: none;
    padding-left: 0;
    padding-right: 0;
  }
}

/* --- Comment bubbles (art modal, Huy 2026-08-15) ---------------------------------------
   Was a 4-column table, which read as a spreadsheet in a 380px column. Now a chat thread:
   your own comments right-aligned on light grey, everyone else's left-aligned on white.
   Side is decided by AUTHOR, not by edit permission — an admin may edit anyone's comment,
   but someone else's comment is still theirs and stays on the left. */
.art-comment-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.art-comment-empty {
  margin: 0;
  font-size: 13px;
}
.art-comment-item {
  display: flex;
}
.art-comment-item.is-mine {
  justify-content: flex-end;
}
.art-comment-item.is-other {
  justify-content: flex-start;
}
.art-comment-bubble {
  /* 80% (Huy 2026-08-15): the empty space on the other side is what makes the alignment
     readable at a glance — a full-width bubble has no side. */
  max-width: 80%;
  min-width: 0;
  border: 1px solid var(--color-border);
  border-radius: 10px;
  padding: 6px 4px 4px;
  background: var(--color-surface);
}
.art-comment-item.is-mine .art-comment-bubble {
  background: var(--color-bg);
}
.art-comment-bubble-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 6px;
}
.art-comment-author {
  font-size: 12px;
  font-weight: 700;
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.art-comment-bubble-head .icon-btn {
  margin-left: auto;
  flex: 0 0 auto;
}
.art-comment-date {
  padding: 0 6px;
  font-size: 11px;
  color: var(--color-text-muted);
  text-align: right;
}
/* The textarea keeps its own borderless look from the rules above; inside a bubble it also
   has to stop being a full-width block that stretches the bubble to the column's edge. */
.art-comment-bubble .art-comment-content-input {
  background: transparent;
  padding: 4px 6px;
}
.art-comment-bubble .art-comment-content-input:focus {
  box-shadow: inset 0 0 0 2px var(--color-primary);
  border-radius: 6px;
}
.art-comment-bubble .art-comment-content-text {
  padding: 4px 6px;
}

/* A bubble hugs its text and stops at 80% — in BOTH directions, which a <textarea> cannot do
   on its own: its intrinsic width comes from `cols` (20 by default), so as a flex item the
   bubble sized itself to ~20 characters and never reached the 80% cap however long the comment
   was (Huy 2026-08-15: "chưa thấy comment có max width là 80%"). Height had the mirror problem
   and was being patched from JS.
   Both are solved by one trick: a grid cell holding the textarea AND an invisible ::after
   carrying the same text. The ::after is normal flow text, so IT decides the cell's width and
   height — wrapping at the bubble's 80% cap, hugging when short — with the textarea stretched
   over it. `data-value` is kept in sync on input; the trailing space stops a trailing newline
   from collapsing. Typography and padding here MUST match the textarea's exactly, or the
   visible layer and the sizing layer drift apart. */
.art-comment-grow {
  display: grid;
}
.art-comment-grow::after,
.art-comment-grow > .art-comment-content-input {
  grid-area: 1 / 1 / 2 / 2;
  padding: 4px 6px;
  font-family: inherit;
  font-size: 14px;
  line-height: 1.4;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.art-comment-grow::after {
  content: attr(data-value) " ";
  visibility: hidden;
}
.art-comment-bubble .art-comment-content-input {
  overflow: hidden;
  resize: none;
  min-height: 0;
}

/* --- Status as 4 buttons (art modal only, Huy 2026-08-15) ------------------------------ */
.art-status-toggle {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.art-status-toggle-btn {
  padding: 5px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
  cursor: pointer;
}
/* :not(.active) matters: the active button carries its own .art-status-* class, which is ALSO
   a single class, so with equal specificity whichever rule sits LOWER in the file wins — and
   this one does (~line 3570 vs ~1550). Painting the idle look unconditionally therefore erased
   the status colour it was supposed to show (Huy 2026-08-15: "không thấy màu cho nút đang
   chọn"). Excluding .active leaves the status class as the only source of its colours. */
.art-status-toggle-btn:not(.active) {
  background: var(--color-surface);
  color: var(--color-text-muted);
}
.art-status-toggle-btn:hover:not(.active):not(:disabled) {
  color: var(--color-text);
  border-color: var(--color-text-muted);
}
/* .active also carries its own .art-status-* class, which supplies background + color — so
   this only needs to remove the idle border and stop the hover rule above from repainting it. */
.art-status-toggle-btn.active {
  border-color: transparent;
}
.art-status-toggle-btn:disabled {
  cursor: default;
  opacity: 0.6;
}

/* --- "Chuyển status" modal (folder page, Huy 2026-10-02) ----------------------------------
   Reuses .art-status-toggle for both status groups, so they look exactly like the art
   detail's buttons. Wider than the 360px default modal so the four buttons sit on one row,
   and capped at the viewport with its own scroll — the art list alone can be 220px tall. */
.modal-bulk-status {
  max-width: 440px;
  max-height: 90vh;
  overflow-y: auto;
}
.bulk-status-section + .bulk-status-section {
  margin-top: 20px;
}
.bulk-status-section .section-title {
  margin-bottom: 12px;
}
/* Captions for the two non-input fields, matching `form label`'s weight and size. Not real
   <label>s on purpose: a label forwards a click on its text to the first labelable element
   inside it, and buttons are labelable — clicking "Status thuộc" would toggle "Empty". */
.bulk-status-caption {
  font-weight: 600;
  font-size: 13px;
  margin-bottom: 6px;
}
.bulk-status-section .art-status-toggle {
  margin-bottom: 14px;
}
.bulk-status-list {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  max-height: 220px;
  overflow-y: auto;
  padding: 4px 0;
}
.bulk-status-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 11px;
  font-size: 13px;
}
.bulk-status-row-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bulk-status-list > .text-muted {
  margin: 6px 11px;
  font-size: 13px;
}
.bulk-status-summary {
  font-size: 13px;
  margin: 16px 0 12px;
}
/* A frame marked "Flip ngang" in the editor: its strip thumbnail mirrors too, so which frames
   are flipped is visible without clicking through them (Huy 2026-08-26). The canvas itself is
   mirrored by the transform renderFramePosition() builds, not by this. */
.file-editor-frame-item img.is-flipped {
  transform: scaleX(-1);
}
.file-editor-frame-tools .icon-btn.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

/* --- Cây tiến hoá tab (Huy 2026-10-10) ------------------------------------------------ */

/* Full-screen mode while the tab is open (project.js toggles body.evo-mode): no project
   header, the tab bar sits centred in the breadcrumb row, and everything below the
   breadcrumbs (56px topbar + 52px breadcrumbs) is the canvas. The page itself never
   scrolls — only the canvas does. */
body.evo-mode {
  overflow: hidden;
}
body.evo-mode .project-header {
  display: none;
}
body.evo-mode #project-tabs {
  position: fixed;
  top: 56px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 21; /* above the sticky breadcrumbs (19) */
  height: 52px;
  margin: 0;
  align-items: flex-end;
  border-bottom: none;
}
#tab-pane-tree {
  position: fixed;
  top: 108px;
  left: 0;
  right: 0;
  bottom: 0;
}
.evo-viewport {
  position: relative;
  overflow: auto;
  height: 100%;
  border-top: 1px solid var(--color-border);
  background-color: var(--color-surface);
  background-image: radial-gradient(var(--color-border) 1px, transparent 1px);
  background-size: 20px 20px;
}
.evo-sizer {
  position: relative; /* takes the canvas' scaled size, so the viewport scrolls the right amount */
}
.evo-canvas {
  position: absolute; /* left/top = the scroll margin, set by JS */
  transform-origin: 0 0;
}

/* Left icon panel, stuck to the screen edge ("nhô ra từ mép màn hình"). */
.evo-toolbar {
  position: fixed;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 10px 8px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: none;
  border-radius: 0 12px 12px 0;
  box-shadow: var(--shadow-lg);
}
.evo-tool {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: none;
  border-radius: 8px;
  background: none;
  color: var(--color-text);
  cursor: pointer;
}
.evo-tool:hover:not(:disabled) {
  background: var(--color-bg);
}
.evo-tool:disabled {
  color: var(--color-border);
  cursor: default;
}
.evo-tool-danger:not(:disabled) {
  color: var(--color-danger);
}
.evo-tool-save.is-dirty {
  background: var(--color-primary);
  color: #fff;
}
.evo-tool-save.is-dirty:hover {
  background: var(--color-primary-hover);
}
.evo-tool-help {
  color: var(--color-text-muted);
  cursor: help;
}
.evo-zoom-level {
  width: 40px;
  padding: 2px 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--color-text-muted);
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
}
.evo-zoom-level:hover {
  background: var(--color-bg);
  color: var(--color-text);
}
.evo-tool-sep {
  width: 24px;
  height: 1px;
  background: var(--color-border);
}
.evo-add-wrap {
  position: relative;
}
.evo-menu {
  position: absolute;
  top: 0;
  left: calc(100% + 14px); /* opens to the right of the panel, over the canvas */
  z-index: 30;
  min-width: 220px;
  max-height: 60vh;
  overflow-y: auto;
  padding: 4px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.evo-menu-item {
  display: block;
  width: 100%;
  padding: 6px 10px;
  border: none;
  border-radius: 4px;
  background: none;
  text-align: left;
  font-size: 13px;
  color: var(--color-text);
  cursor: pointer;
  white-space: nowrap;
}
.evo-menu-item:hover {
  background: var(--color-bg);
}
.evo-menu-item .tag-chip,
.evo-menu-item .text-muted {
  margin-left: 4px;
}
.evo-menu-all {
  font-weight: 600;
  color: var(--color-primary);
  border-bottom: 1px solid var(--color-border);
  border-radius: 4px 4px 0 0;
  margin-bottom: 4px;
}
.evo-menu-item:disabled {
  color: var(--color-text-muted);
  cursor: default;
  background: none;
}

/* Dark bar at the bottom of the screen for the selected folder / note / arrow. */
.evo-bottombar {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 16px;
  max-width: calc(100vw - 32px);
  padding: 8px 10px 8px 14px;
  background: #1f2328;
  color: #fff;
  border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
  font-size: 13px;
}
.evo-bottombar[hidden] {
  display: none;
}
.evo-bb-group {
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}
.evo-bb-label {
  color: #9ca3af;
}
.evo-bb-input,
.evo-bb-select {
  height: 32px;
  padding: 0 10px;
  border: 1px solid #3d444d;
  border-radius: 6px;
  background: #2d333b;
  color: #fff;
  font: inherit;
}
.evo-bb-input {
  width: 200px;
}
.evo-bb-input::placeholder {
  color: #7d8590;
}
.evo-swatches {
  display: flex;
  gap: 6px;
}
.evo-swatch {
  width: 20px;
  height: 20px;
  padding: 0;
  border: 2px solid #1f2328;
  border-radius: 50%;
  box-shadow: 0 0 0 1px #57606a;
  cursor: pointer;
}
.evo-swatch.active {
  box-shadow: 0 0 0 2px #fff;
}
.evo-swatch-auto {
  background: #2d333b;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
}
.evo-bb-seg {
  gap: 0;
}
.evo-bb-seg button {
  height: 30px;
  padding: 0 10px;
  border: 1px solid #3d444d;
  background: #2d333b;
  color: #c9d1d9;
  font: inherit;
  cursor: pointer;
}
.evo-bb-seg button:first-child {
  border-radius: 6px 0 0 6px;
}
.evo-bb-seg button:last-child {
  border-radius: 0 6px 6px 0;
  border-left: none;
}
.evo-bb-seg button.active {
  background: #fff;
  color: #1f2328;
}
.evo-bb-delete {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: #ff7b72;
  cursor: pointer;
}
.evo-bb-delete:hover {
  background: #3d1f1f;
}

/* Arrows */
.evo-edges {
  position: absolute;
  inset: 0;
  overflow: visible;
}
.evo-edge-line {
  stroke-width: 1.6;
}
.evo-edge-halo {
  stroke: var(--color-primary);
  stroke-opacity: 0.25;
  stroke-width: 8;
  stroke-linecap: round;
}
.evo-edge-hit {
  stroke: transparent;
  stroke-width: 14;
  pointer-events: stroke;
  cursor: pointer;
}
.evo-edge-label {
  font-size: 13px;
  font-weight: 600;
}
.is-editable .evo-edge-label {
  cursor: pointer;
}
.evo-empty {
  position: absolute;
  top: 24px;
  left: 96px;
  margin: 0;
}

/* Nodes */
.evo-node {
  position: absolute;
  user-select: none;
  touch-action: none;
}
.is-editable .evo-node {
  cursor: grab;
}
.evo-node.is-selected {
  outline: 2px solid var(--color-primary);
  outline-offset: 4px;
  border-radius: 6px;
}
.evo-node-folder {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
/* 100 x 120 frame (Huy 2026-10-10). The picture keeps its own pixel size — never scaled up;
   a wider one widens the frame instead of being squeezed, a taller one is shrunk to fit. */
.evo-node-img {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  min-width: 100px;
  height: 120px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 4px;
  font-size: 11px;
  text-align: center;
}
.evo-node-img img {
  display: block;
  max-height: 100%;
  image-rendering: pixelated;
  pointer-events: none;
}
.evo-node-label {
  font-weight: 700;
  font-size: 13px;
  line-height: 18px;
  white-space: nowrap;
}
.evo-node-note {
  max-width: 320px;
  padding: 2px 4px;
  font-size: 15px;
  line-height: 1.4;
  white-space: pre-wrap;
}
.is-editable .evo-node-note:hover {
  outline: 1px dashed var(--color-border);
}
.evo-note-input {
  display: block;
  width: 240px;
  min-height: 60px;
  font: inherit;
  color: inherit;
}
.evo-handle {
  position: absolute;
  left: 50%;
  bottom: -12px;
  width: 12px;
  height: 12px;
  margin-left: -6px;
  border-radius: 50%;
  background: var(--color-surface);
  border: 2px solid var(--color-primary);
  cursor: crosshair;
  opacity: 0;
}
.evo-node:hover .evo-handle,
.evo-node.is-selected .evo-handle {
  opacity: 1;
}
/* Selection box dragged over empty canvas (Huy 2026-10-10). Border width set by JS so it
   stays 1 screen px at any zoom. */
.evo-marquee {
  position: absolute;
  border: 1px solid var(--color-primary);
  background: rgba(37, 99, 235, 0.08);
  pointer-events: none;
  z-index: 5;
}

/* "Xuất từ cây tiến hoá" in the Sửa project modal (Huy 2026-10-10) */
.tree-image-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: -4px 0 12px;
  font-size: 13px;
}
.tree-image-preview {
  display: block;
  max-width: 100%;
  max-height: 220px;
  margin-bottom: 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}
.tree-image-preview[hidden] {
  display: none;
}

/* Admin: "Quyền trên UI" in the user modal (Huy 2026-10-11) — role presets + permission list */
.user-perm-section {
  margin: 4px 0 12px;
  padding-top: 12px;
  border-top: 1px solid var(--color-border);
}
.user-perm-section h3 {
  margin: 0 0 8px;
  font-size: 15px;
}
.user-role-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.user-role-btn.active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.user-perm-hint {
  margin: 8px 0;
  font-size: 12px;
}
.modal.modal-user {
  max-width: 720px;
}
/* Two columns so the 13 permissions don't make the modal scroll (Huy 2026-10-11); one column
   again on a narrow screen. */
.user-perm-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px 20px;
}
@media (max-width: 640px) {
  .user-perm-list {
    grid-template-columns: 1fr;
  }
}
.user-perm-list .checkbox-label {
  margin: 0;
  font-size: 13px;
  align-items: flex-start; /* two-line labels: box beside the first line, not the middle */
}
.user-perm-list .checkbox-label input {
  margin-top: 2px;
}

/* "Quyền MCP" section of the user modal */
.user-mcp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.user-mcp-head h3 {
  margin: 0;
}
.user-mcp-switch {
  margin: 0;
  font-size: 13px;
}
.user-mcp-section {
  margin-top: 4px;
}
#mcp-body {
  margin-top: 8px;
}
.user-mcp-token-label {
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 4px;
}
.user-mcp-token-row {
  display: flex;
  gap: 6px;
}
.user-mcp-token-row input {
  flex: 1;
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
}
#mcp-body code {
  font-size: 12px;
}
