/* ═══════════════════════════════════════════════════════════════════════
   ALVA Design System v2
   Figma: ALVA Design System (v2) — pages Color and Typography.
   Variable names below are CESA's own and are referenced throughout this file;
   each comment names the v2 semantic token it stands for. Components reference
   these, never a hex value — the design system's rule 03, and ours.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  /* ── neutral ── */
  --bg:#FFFFFF;              /* Background/Neutral/primary */
  --surface:#F3F3F3;         /* Background/Neutral/secondary */
  --text:#1F2120;            /* Text/primary — 16.20:1 */
  --text-muted:#6B6C6C;      /* Text/secondary — 5.27:1 */
  --neutral-mid:#9E9F9F;     /* Text/tertiary · Icon/tertiary — 2.65:1, so
                                placeholders and decoration only, never content */

  /* Borders come in two strengths on purpose.
     Containers and dividers use what the v2 DataTable and Forms artwork
     actually draw, measured at #D9D9D9: Border/Neutral/disabled. A divider is
     decoration, so WCAG sets it no contrast floor.
     Control edges do not follow the artwork. The Forms page draws inputs with
     that same light grey, which measures 1.38:1 and fails v2's own rule 04
     (3:1 for UI elements) — an empty field is close to invisible. They take
     Border/Neutral/primary instead, the token v2 names for inputs. */
  --border-control:#898A8D;  /* Border/Neutral/primary — 3.45:1 */
  --border:#DBDBDB;          /* Border/Neutral/disabled — dividers, containers */

  /* ── brand ── */
  --accent:#33FFC9;          /* Background/Brand/primary — edges, active states;
                                v2 rule 02 caps Electric Green at 10–40% */
  --accent-tint:#EBFFFA;     /* Background/Success/secondary */
  --success:#02CF99;         /* Border/Success/primary */
  --success-ink:#09765A;     /* Text/brand-secondary — 5.60:1 */

  /* ── status ── every tint is paired with an ink measured against it ── */
  --danger:#C9303B;          /* Text/error = Background/Error/primary — 5.29:1
                                as text on white and as white on it */
  --danger-strong:#8B2931;   /* Icon/error-primary — danger hover, 8.54:1 */
  --danger-tint:#FFF0F1;     /* Background/Error/secondary */
  --danger-ink:#C9303B;      /* Text/error — 4.78:1 on the tint */
  --warn:#FFD748;            /* Yellow (brand) — dots and fills, never text */
  --warn-tint:#FFEFB6;       /* Background/Warning/secondary */
  --warn-ink:#79641D;        /* Text/warning — 5.00:1 on the tint */
  --info:#607DFF;            /* Border/Success/info — v2's name, an info border */
  --info-tint:#EFF2FF;       /* Background/Info/secondary */
  --info-ink:#435CCE;        /* Text/info — 5.14:1 on the tint */
  --neutral-tint:#EDEDED;    /* Background/Neutral/tertiary */
  /* Not v2's Text/secondary: on this tint it measures 4.50:1, on the line
     rather than over it, and badge text is 12px. */
  --neutral-ink:#505151;     /* 6.80:1 */

  /* Links. v2 defines Text/link #8097FF and Text/link-visited #607DFF, but
     they measure 2.70:1 and 3.57:1 on white and fail v2's own rule 04 (AA,
     4.5:1). Text/info is the same blue family and does pass. */
  --link:#435CCE;            /* Text/info — 5.74:1 */

  /* Not in v2's semantic set. Machine-type badges need a hue distinct from
     info badges and v2 has no second blue or a purple; kept until the Tag
     page says otherwise. */
  --alt-tint:#DCEDF9;
  --alt-ink:#1F6898;         /* 5.02:1 on the tint */

  /* Primary. v2 draws it as a gradient from #016248 to #06BD8C under a white
     label, but white on the light end measures 2.42:1 — v2's rule 04 again.
     The gradient is cut where it still clears AA (4.63:1 at #038563): the
     button keeps v2's direction and hue, and every letter of a long label
     stays legible. Hover and press darken by 8% and 15%, as the page says. */
  --btn-grad:linear-gradient(90deg,#016248 0%,#038563 100%);
  --btn-bg:#016248;          /* the darkest stop; solid fallback */
  --btn-fg:#FFFFFF;          /* Text/inverse */
  --btn-disabled-bg:#EDEDED; /* Button: disabled fill */
  --btn-disabled-fg:#9E9F9F; /* Button: disabled text */
  --focus-ring:0 0 0 3px #33FFC9;  /* Button: Electric Green ring, 3px spread */
  --focus-border:#06C08F;    /* Border/Brand/primary */

  /* Chip fills (Tag page). Stronger than the tints above, which stay for
     banners and field backgrounds. */
  --success-fill:#D6FFF4;    /* Background/Success/primary */
  --info-fill:#DFE5FF;       /* Background/Info/primary */
  --danger-chip-ink:#B42A33; /* the Tag and Forms artwork's red — 5.72:1 */
  --disabled-bg:#E7E7E7;     /* Background/Neutral/disabled */

  --white:#FFFFFF;           /* retained: existing rules reference it */

  /* Radius (Grid & Spacing). v2 reads rounder than the v1 this file started
     from: pill buttons and chips, 12px panels, 16px modals. Inputs are the
     exception — the Forms and DataTable artwork draw them square, and that is
     followed over the scale's "md 8px for inputs", because the artwork is what
     other ALVA products ship. */
  --radius:4px;              /* radius/sm */
  --radius-sm:2px;           /* radius/xs */
  --radius-md:8px;           /* radius/md — dropdowns, toasts, inline messages */
  --radius-lg:12px;          /* radius/lg — cards, panels, tooltips, banners */
  --radius-xl:16px;          /* radius/xl — modals */
  --radius-full:999px;       /* radius/full — buttons, chips, page numbers */
  /* Elevation: the v2 Effect Styles, exact. Read from the Figma effect data
     (dgtl-alv/alva-design-system, tokens/elevation.json), replacing values
     that had been estimated from the rendered artwork. Every level is two
     layers, as in Figma: a 1px ambient ring plus the key shadow, both in
     navy #000A37 rather than black. */
  --elev-100:0 0 1px rgba(0,10,55,.31),0 1px 1px rgba(0,10,55,.25);    /* Shadow/100 -- chips, avatar rings */
  --elev-200:0 0 1px rgba(0,10,55,.31),0 3px 5px rgba(0,10,55,.2);     /* Shadow/200 -- cards, stat cards */
  --elev-300:0 0 1px rgba(0,10,55,.31),0 8px 12px rgba(0,10,55,.15);   /* Shadow/300 -- dropdowns, popovers, toasts */
  --elev-400:0 0 1px rgba(0,10,55,.31),0 10px 18px rgba(0,10,55,.15);  /* Shadow/400 -- drawers, sticky headers */
  --elev-500:0 0 1px rgba(0,10,55,.31),0 18px 28px rgba(0,10,55,.15);  /* Shadow/500 -- modals */

  /* Inline Message's optional border. */
  --inline-border:#B2EFE0;

  /* ── MOS type scale ──
     10 Footnote/Fn1 · 12 Body/M4 · 14 Body/M3 · 16 Body/M2 · 18 Body/M1
     24 Heading/H5 · 28 Heading/H4 · 32 Heading/H3. Nothing off this scale is
     used for text; the larger sizes that remain in the file are emoji and
     glyph icons, not type. */
  --fs-xs:12px; --fs-sm:14px; --fs-base:16px;
  --fs-lg:18px; --fs-xl:24px; --fs-2xl:32px;
}
*{box-sizing:border-box;margin:0;padding:0;}
/* Made Outer Sans is v2's primary face for all UI text; Readex Pro is only
   for long-form reading, which CESA has none of. MOS is a licensed face and
   is not bundled, so this still falls through to Inter/Space Grotesk/
   system-ui. Drop the woff2 into web/static/fonts and add @font-face to get
   the real thing; the CSP forbids loading it from a CDN. */
body{
  font-family:"MADE Outer Sans",Inter,"Space Grotesk",system-ui,sans-serif;
  font-weight:400;line-height:1.5;
  background:var(--surface);color:var(--text);font-size:var(--fs-sm);
  -webkit-font-smoothing:antialiased;
}
[hidden]{display:none !important;}
h1,h2,h3{font-weight:700;letter-spacing:.5px;}   /* H1–H3 */
a{color:var(--link);}

/* ── NAV ── */
/* The 4px accent underline is the design system's signature edge treatment. */
.topnav{background:var(--bg);border-bottom:4px solid var(--accent);padding:0 24px;display:flex;align-items:center;gap:12px;height:60px;position:sticky;top:0;z-index:200;}
.logo{display:flex;align-items:center;gap:8px;font-weight:700;font-size:var(--fs-base);color:var(--text);text-decoration:none;letter-spacing:0;}
.logo span{color:var(--text-muted);font-weight:400;font-size:14px;letter-spacing:0;}
/* Sized to the cap height of the wordmark beside it, not to the 60px bar. */
.logo-mark{width:22px;height:22px;display:block;flex-shrink:0;}
.badge-internal{background:var(--warn-tint);color:var(--warn-ink);padding:3px 8px;border-radius:var(--radius);font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:.06em;}
.spacer{flex:1;}
.env-tag{font-size:var(--fs-xs);color:var(--text-muted);background:var(--surface);padding:4px 10px;border-radius:var(--radius);border:1px solid var(--border);}
/* Sits beside the environment tag and reads as secondary to it: which build
   is running matters less often than which environment you are in. */
.ver-tag{font-size:12px;color:var(--text-muted);font-family:ui-monospace,Consolas,monospace;
  padding:4px 8px;border-radius:var(--radius);border:1px solid var(--border);
  background:var(--bg);letter-spacing:.02em;}
.ver-tag:empty{display:none;}

/* ── USER MENU ── */
.user-menu-wrap{position:relative;}
.user-btn{display:flex;align-items:center;gap:8px;padding:5px 10px 5px 5px;border:1px solid var(--border-control);border-radius:var(--radius);background:var(--bg);cursor:pointer;font-size:14px;color:var(--text);transition:border-color .15s;font-family:inherit;}
.user-btn:hover{border-color:var(--text);}
/* A circle here is a shape, not a radius token. */
.user-avatar{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:var(--text);flex-shrink:0;background:var(--accent);}
.user-info{text-align:left;}
.user-info .uname{font-weight:500;font-size:14px;line-height:1.2;}
.user-info .urole{font-size:12px;color:var(--text-muted);}
.user-dropdown{position:absolute;right:0;top:calc(100% + 6px);background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--elev-300);min-width:220px;z-index:300;display:none;overflow:hidden;}
.user-menu-wrap.open .user-dropdown{display:block;}
.ud-header{padding:14px 16px;border-bottom:1px solid var(--border);background:var(--surface);}
.ud-header .udname{font-weight:700;font-size:var(--fs-sm);}
.ud-header .udemail{font-size:12px;color:var(--text-muted);margin-top:2px;word-break:break-all;}
.ud-header .udrole{display:inline-flex;align-items:center;margin-top:6px;}
.ud-item{display:flex;align-items:center;gap:10px;padding:10px 16px;font-size:14px;cursor:pointer;color:var(--text);transition:background .15s;border:none;background:none;width:100%;text-align:left;font-family:inherit;}
.ud-item:hover{background:var(--surface);}
.ud-item.danger{color:var(--danger);}
.ud-divider{height:1px;background:var(--border);}

/* ── LOGIN ── */
/* Dark ground with a single electric-green edge: ALVA's core contrast. */
.login-screen{position:fixed;inset:0;background:linear-gradient(160deg,#1F2120 0%,#171717 55%,#0D0D0D 100%);display:flex;align-items:center;justify-content:center;padding:24px;overflow-y:auto;}
.login-card{background:var(--bg);border-radius:var(--radius);border-top:4px solid var(--accent);padding:40px 36px;width:100%;max-width:420px;box-shadow:0 24px 64px rgba(0,0,0,.45);}
.login-logo{display:flex;align-items:center;gap:10px;margin-bottom:28px;}
/* The mark is a self-contained logo, so it stands on its own. The tinted tile
   that used to sit behind the emoji would read as an icon chip around it, and
   pale mint under dark green muddies both. */
.login-logo .ico{width:44px;height:44px;display:block;flex-shrink:0;}
.login-logo .ltext{font-size:var(--fs-lg);font-weight:700;color:var(--text);letter-spacing:0;}
.login-logo .lsub{font-size:var(--fs-xs);color:var(--text-muted);}
.login-title{font-size:18px;font-weight:700;margin-bottom:4px;}
.login-sub{font-size:14px;color:var(--text-muted);margin-bottom:24px;}
.login-field{margin-bottom:16px;}
.login-field label{display:block;font-size:var(--fs-sm);font-weight:700;color:var(--text);margin-bottom:6px;}
.login-field input{width:100%;padding:10px 12px;border:1px solid var(--border-control);border-radius:0;min-height:40px;font-size:var(--fs-sm);outline:none;transition:border-color .15s,box-shadow .15s;font-family:inherit;font-weight:400;}
.login-field input:focus{border-color:var(--focus-border);box-shadow:0 0 0 3px var(--accent-tint);}
.login-err{background:var(--danger-tint);color:var(--danger-ink);border:1px solid var(--danger);border-radius:var(--radius);padding:8px 12px;font-size:14px;margin-bottom:14px;}
.login-btn{width:100%;min-height:46px;padding:0 20px;background:var(--btn-bg);background-image:var(--btn-grad);color:var(--btn-fg);border:none;border-radius:var(--radius-full);font-size:var(--fs-base);font-weight:700;cursor:pointer;transition:filter .15s;font-family:inherit;}
.login-btn:hover:not(:disabled){filter:brightness(.92);}
.login-btn:focus-visible{outline:none;box-shadow:var(--focus-ring);}
.login-btn:disabled{background:var(--btn-disabled-bg);color:var(--btn-disabled-fg);cursor:default;}
.login-btn.ms{display:flex;align-items:center;justify-content:center;gap:10px;}
.login-note{margin-top:20px;background:var(--accent-tint);border:1px solid var(--inline-border);border-radius:var(--radius-md);padding:12px 16px;font-size:var(--fs-xs);color:var(--success-ink);line-height:1.5;}   /* Inline Message, 5.39:1 */
.env-pill{display:inline-block;margin-top:14px;font-size:12px;color:var(--text-muted);background:var(--surface);border:1px solid var(--border);padding:3px 10px;border-radius:var(--radius);}

/* ── LAYOUT ── */
.main{max-width:1300px;margin:0 auto;padding:24px 24px 60px;}
.page-header{margin-bottom:20px;}
/* The accent rule on headings mirrors the design system's own h2 treatment. */
.page-header h1{font-size:var(--fs-xl);font-weight:700;letter-spacing:1px;border-left:4px solid var(--accent);padding-left:12px;}
.page-header p{color:var(--text-muted);margin-top:6px;font-size:14px;padding-left:16px;}

/* ── GLOBAL SCOPE BAR ── */
.scope-bar{display:flex;align-items:center;gap:10px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--elev-200);padding:10px 14px;margin-bottom:16px;flex-wrap:wrap;}
.scope-bar label{font-size:var(--fs-xs);font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;}
.scope-note{font-size:var(--fs-xs);color:var(--text-muted);}
.scope-note .counting{color:var(--text);font-weight:500;}

/* ── STATS ── */
.stats-row{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:24px;}
.stat-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--elev-200);padding:16px 20px;display:flex;align-items:center;gap:16px;}
.stat-icon{width:56px;height:56px;border-radius:var(--radius);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0;}
.stat-icon.loc{background:var(--info-tint);}
.stat-icon.mac{background:var(--accent-tint);}
.stat-icon.con{background:var(--alt-tint);}
.stat-info .num{font-size:var(--fs-2xl);font-weight:700;letter-spacing:.5px;line-height:1.1;}
.stat-info .lbl{font-size:var(--fs-xs);color:var(--text-muted);margin-top:4px;text-transform:uppercase;letter-spacing:.06em;}
.stat-info .sub{font-size:12px;color:var(--text-muted);margin-top:3px;
  display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;}
/* Status counts. Colour is doing real work here, so each also keeps its label
   in words — colour alone would exclude anyone who cannot separate red from
   green. */

.statc{display:inline-flex;align-items:center;gap:3px;white-space:nowrap;}
.statc b{font-weight:700;}
.sc-active{color:var(--success-ink);}
.sc-maint{color:var(--danger-ink);}   /* Text/error, 5.29:1 */
.sc-draft{color:var(--text);}
.sc-decom{color:var(--text-muted);text-decoration:line-through;}
.sc-note{color:var(--text-muted);font-style:italic;}

/* ── TABS ── */
.tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:20px;}
/* The same tab strip inside a modal. Its own class on purpose: the page's
   handler binds every .tab-btn once at startup, so a modal tab wearing that
   class would switch the tables behind the modal. */
.htabs{display:flex;border-bottom:1px solid var(--border);margin:0 0 14px;}
.tab-btn,.htab{padding:10px 22px;font-size:var(--fs-sm);font-weight:400;color:var(--text-muted);background:none;border:none;border-bottom:4px solid transparent;margin-bottom:-1px;cursor:pointer;display:flex;align-items:center;gap:6px;transition:color .15s,border-color .15s;font-family:inherit;}
.tab-btn:hover,.htab:hover{color:var(--text);}
.tab-btn.active,.htab.active{color:var(--text);background:var(--accent-tint);border-bottom-color:var(--success);font-weight:700;}
.tab-count{background:var(--surface);color:var(--text-muted);padding:1px 7px;border-radius:var(--radius-full);font-size:12px;border:1px solid var(--border);}
.tab-btn.active .tab-count{background:var(--accent-tint);color:var(--text);border-color:var(--success);}
.tab-panel,.hpanel{display:none;}.tab-panel.active,.hpanel.active{display:block;}

/* ── TOOLBAR ── */
.toolbar{display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap;}
.toolbar-cta{flex:1 1 100%;display:flex;justify-content:center;}
.search-wrap{position:relative;flex:1;min-width:200px;max-width:360px;}
.search-wrap input{width:100%;padding:8px 12px 8px 34px;border:1px solid var(--border-control);border-radius:0;min-height:36px;font-size:14px;background:var(--bg);color:var(--text);outline:none;transition:border-color .15s,box-shadow .15s;font-family:inherit;font-weight:400;}
.search-wrap input:focus{border-color:var(--focus-border);box-shadow:0 0 0 3px var(--accent-tint);}
.search-wrap .ico-s{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text-muted);pointer-events:none;}
.filter-select{padding:8px 12px;border:1px solid var(--border-control);border-radius:0;min-height:36px;font-size:14px;background:var(--bg);color:var(--text);outline:none;cursor:pointer;font-family:inherit;font-weight:400;}
.filter-select:focus{border-color:var(--focus-border);box-shadow:0 0 0 3px var(--accent-tint);}
.btn{min-height:32px;padding:0 14px;border-radius:var(--radius-full);font-size:14px;font-weight:700;cursor:pointer;border:1px solid transparent;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:filter .15s,background .15s,border-color .15s,color .15s;white-space:nowrap;font-family:inherit;line-height:1.15;}
.btn:disabled,.btn:disabled:hover{background:var(--btn-disabled-bg);color:var(--btn-disabled-fg);border-color:var(--btn-disabled-bg);cursor:not-allowed;filter:none;}
.btn-primary{background:var(--btn-bg);background-image:var(--btn-grad);color:var(--btn-fg);}
.btn-primary:hover:not(:disabled),.btn-danger:hover:not(:disabled){filter:brightness(.92);}
.btn-primary:active:not(:disabled),.btn-danger:active:not(:disabled){filter:brightness(.85);}
.btn:focus-visible{outline:none;box-shadow:var(--focus-ring);}
.btn-secondary{background:var(--bg);color:var(--text);border-color:var(--text);}
.btn-secondary:hover:not(:disabled),.btn-ghost:hover:not(:disabled){background:var(--surface);}
.btn-secondary:active:not(:disabled),.btn-ghost:active:not(:disabled){background:var(--neutral-tint);}
.btn-danger{background:var(--danger);color:#fff;}
/* Ghost: tertiary or inline action, transparent over a light hairline. */
.btn-ghost{background:transparent;color:var(--text);border-color:var(--border);}
.btn-sm{min-height:24px;padding:0 12px;font-size:var(--fs-xs);gap:4px;}   /* XS */
.ml-auto{margin-left:auto;}

/* ── TABLE ── */
/* overflow was hidden, which did not hide the overflow so much as amputate it:
   the Actions column was clipped off the right edge with no way to reach it.
   Scroll as a safety net, and pin Actions so it is visible without scrolling. */
.table-wrap{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-lg);overflow-x:auto;}
.table-wrap table td:has(> .actions-cell),
.table-wrap table td:has(> [data-act="join-detail"]),
.table-wrap table thead th:last-child{position:sticky;right:0;background:var(--bg);z-index:1;}
.table-wrap table thead th:last-child{background:var(--surface);z-index:2;}
/* The pinned cell is found by what it holds, not by being a row's last cell.
   Join and Data PLN are built with rowspan, so a continuation row's last cell
   is a connector's status or serial -- and pinning :last-child pinned those
   too, stacking them over the real action column the moment the table
   scrolled (on a phone, and on Data PLN at laptop widths). */
/* A hairline so the pinned column reads as pinned rather than overlapping. */
.table-wrap table td:has(> .actions-cell)::before,
.table-wrap table td:has(> [data-act="join-detail"])::before,
.table-wrap table thead th:last-child::before{content:'';position:absolute;left:0;top:0;bottom:0;
  width:1px;background:var(--border);}

/* The Join table carries fifteen columns and must not scroll sideways, so it
   trades padding and type size for width rather than reachability.
   
   table-layout:fixed is what makes that guarantee hold: with auto layout a
   single long location name or display name widens its column and pushes the
   table past the viewport. Fixed widths mean content wraps instead. */
.join-table{font-size:12px;table-layout:fixed;width:100%;}
.join-table th,.join-table td{padding:6px 6px;}
/* A serial number is one token. Breaking it across lines makes it unreadable
   and easy to mis-copy, so the two SN columns are sized to hold the longest
   value instead: machine serials run to 24 characters, connector serials to
   16, which is why they do not share a width. */
.join-table .sn-tag{font-size:10px;padding:1px 4px;white-space:nowrap;}
/* A status badge is one word; breaking "Maintenance" across two lines makes
   it unreadable, so the column is sized to fit it instead. */
.join-table .badge{font-size:10px;padding:1px 6px;min-height:0;white-space:nowrap;}
/* Headers wrap onto the two lines the markup asks for; the global th rule
   sets nowrap, which would keep "Status Konektor" one wide line. */
.join-table th{white-space:normal;line-height:1.25;padding-top:8px;padding-bottom:8px;font-size:12px;}
/* Long values wrap rather than being cut off — nothing here is reachable by
   scrolling sideways, so an ellipsis would simply hide the value. */
.join-table td{word-wrap:break-word;overflow-wrap:anywhere;}
.jt-wrap{white-space:normal;}
/* Display Name is capped at three lines so one verbose name cannot make every
   row in its location tall. The full text stays in the title attribute.
   
   This must sit on a div inside the cell, never on the td itself: -webkit-box
   replaces display:table-cell, which drops the cell out of table layout and
   makes its rowspan stop reserving a column. */
.jt-clamp3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;
  overflow:hidden;line-height:1.3;}
.jt-chips{line-height:1.5;}
.chip-xs{display:inline-block;font-size:10px;padding:1px 4px;margin:1px 2px 1px 0;
  border:1px solid var(--border);border-radius:var(--radius-sm);
  color:var(--text-muted);white-space:nowrap;}

/* Percentages, so the fifteen columns divide whatever width the viewport
   gives them instead of demanding a minimum the screen may not have. */
.join-table .jt-loc   {width:8%;}
.join-table .jt-stat  {width:7.5%;}
.join-table .jt-priv  {width:4.5%;}
.join-table .jt-bike  {width:7%;}
.join-table .jt-pln   {width:6.5%;}
.join-table .jt-kva   {width:4.5%;}
.join-table .jt-snmac {width:12.5%;}
.join-table .jt-type  {width:6.5%;}
.join-table .jt-dname {width:6.5%;}
.join-table .jt-sncon {width:8.5%;}
.join-table .jt-slot  {width:3%;}
.join-table .jt-watt  {width:4.5%;}
.join-table .jt-act   {width:5.5%;}   /* holds the edit button at 1214px; 4% did not */

/* The Join tab is the widest table in the app -- fifteen columns against every
   other tab's six or seven -- and .main caps every tab at 1300px. Rather than
   squeeze fifteen columns into a bound set for six, this tab breaks out of it
   on screens with the room to spare. Below 1400px nothing changes and the
   columns above are sized to fit that narrower case, which is the binding one.

   Location (ten columns) and Data PLN (thirteen) break out the same way, so the
   three wide tabs share one width and none of them scrolls sideways where the
   screen has room. */
@media (min-width:1400px){
  #panel-join,#panel-loc,#panel-pln{
    --jt-w:min(1680px, calc(100vw - 48px));
    width:var(--jt-w);
    margin-left:calc((1252px - var(--jt-w)) / 2);
  }
}
/* What set these two tables' width was their narrowest possible layout, not
   their content: every header held to one line (the global th is nowrap, and
   "Charger Machine Type" at 14px bold is wide), and a location's UUID, which
   has no place to break. Letting both wrap lets the columns shrink to the
   panel instead of pushing past it. A wrapped UUID still copies whole. */
#panel-loc th,#panel-pln th{white-space:normal;line-height:1.25;}

/* Data PLN carries thirteen columns, four of them long identifiers, and with
   real rows it needed 1467px -- the earlier "fits" was measured on an empty
   table. Most of that was padding (28px a column) and 12px monospace IDs, so
   it takes the Join table's density: 12px text, 6px cell padding, 10px IDs. */
#panel-pln table{font-size:12px;}
#panel-pln th,#panel-pln td{padding:8px 6px;}
#panel-pln th{font-size:12px;}
#panel-pln .sn-tag{font-size:10px;padding:1px 4px;}
#panel-pln .cell-trunc{max-width:140px;}
#panel-pln .badge{font-size:11px;min-height:20px;padding:0 8px;}
#panel-loc .id-tag{overflow-wrap:anywhere;}

/* Wrapping headers was enough for Data PLN but not for Location, whose floor
   was set by its cells: two text columns truncated at a fixed 200px, a cluster
   badge ("Greater Yogyakarta Area") that could not wrap, and a row of action
   buttons -- 1442px of minimum that no screen under 1536px could hold. So it
   takes the Join table's approach: fixed layout, percentage columns, and the
   content fits the column instead of the column fitting the content.
   Actions gets the room its buttons need at 1280px so they stay on one row. */
#panel-loc table{table-layout:fixed;min-width:1214px;}   /* below it, scroll: on a phone these ten columns had been crushed into 364px */
/* Sized at the narrowest panel (1214px) with 10px cell padding: Status holds
   "Maintenance" (101px) and Bayar holds "Berbayar" (68px) on one line, since
   a one-word badge broken mid-word is unreadable. */
#panel-loc th,#panel-loc td{padding-left:10px;padding-right:10px;}
#panel-loc th:nth-child(1){width:17%;}   /* Location Name */
#panel-loc th:nth-child(2){width:10%;}   /* City / Province */
#panel-loc th:nth-child(3){width:10%;}   /* Cluster */
#panel-loc th:nth-child(4){width:9%;}    /* Jam Operasional */
#panel-loc th:nth-child(5){width:5%;}    /* Provider */
#panel-loc th:nth-child(6){width:8%;}    /* Bayar */
#panel-loc th:nth-child(7){width:8%;}    /* Parkir */
#panel-loc th:nth-child(8){width:11%;}   /* Status */
#panel-loc th:nth-child(9){width:8%;}    /* Go Live */
/* Three icon buttons abreast are 144px; 14% of the narrowest panel (1214px)
   less the cell's 20px of padding is 150px. */
#panel-loc th:nth-child(10){width:14%;}  /* Actions */
#panel-loc .cell-trunc{max-width:100%;}
#panel-loc .badge{white-space:normal;line-height:1.3;padding:3px 10px;}
#panel-loc .actions-cell{flex-wrap:wrap;}

table{width:100%;border-collapse:collapse;}
thead{background:var(--surface);}
th{padding:11px 14px;font-size:var(--fs-sm);font-weight:700;color:var(--text);text-align:left;border-bottom:1px solid var(--success);white-space:nowrap;}
td{padding:11px 14px;border-bottom:1px solid var(--border);vertical-align:middle;}
tbody tr:last-child td{border-bottom:none;}
tbody tr:hover{background:var(--surface);}
.cell-trunc{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;display:block;}
.sn-tag{font-family:ui-monospace,Consolas,monospace;font-size:12px;background:var(--surface);padding:2px 6px;border-radius:var(--radius-sm);}
.id-tag{font-family:ui-monospace,Consolas,monospace;font-size:10px;color:var(--text-muted);}
.actions-cell{white-space:nowrap;display:flex;gap:6px;}

/* ── BADGES ──
   Every tint pairs with dark or deep ink rather than white. The bright end of
   the ALVA ramps cannot carry white text at an accessible contrast, and the
   status dot does the colour-signalling job instead. */
.badge{display:inline-flex;align-items:center;gap:5px;min-height:24px;padding:0 10px;border-radius:var(--radius-full);font-size:12px;font-weight:500;white-space:nowrap;}
.badge-green{background:var(--success-fill);color:var(--success-ink);}      /* 5.19:1 */
.badge-red{background:var(--danger-tint);color:var(--danger-chip-ink);}     /* 5.72:1 */
.badge-yellow{background:var(--warn-tint);color:var(--warn-ink);}
.badge-blue{background:var(--info-fill);color:var(--info-ink);}             /* 4.59:1 */
.badge-gray{background:var(--disabled-bg);color:var(--neutral-ink);}       /* 6.44:1 */
/* No purple exists in the ALVA palette — machine types use blueSecondary. */
.badge-purple{background:var(--alt-tint);color:var(--alt-ink);}
.dot{width:6px;height:6px;border-radius:50%;display:inline-block;flex-shrink:0;}
.dot-green{background:var(--success);}
.dot-yellow{background:var(--warn);}
.dot-gray{background:var(--neutral-mid);}
.dot-red{background:var(--danger);}

/* ── PAGINATION ── */
.pagination{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg);border-top:1px solid var(--border);flex-wrap:wrap;gap:8px;}
.pagination .info{font-size:var(--fs-xs);color:var(--text-muted);}
.pg-btns{display:flex;gap:4px;}
.pg-btn{min-width:32px;height:32px;padding:0 8px;border:none;border-radius:var(--radius-full);font-size:var(--fs-sm);font-weight:700;background:transparent;cursor:pointer;color:var(--text);font-family:inherit;}
.pg-btn:hover:not(:disabled):not(.active){background:var(--surface);}
.pg-btn:focus-visible{outline:none;box-shadow:var(--focus-ring);}
.pg-btn:disabled{opacity:.4;cursor:default;}
.pg-btn.active{background:var(--btn-bg);background-image:var(--btn-grad);color:var(--btn-fg);}

/* ── STATES ── */
.empty-state{text-align:center;padding:48px 24px;color:var(--text-muted);}
.empty-state .eico{font-size:36px;margin-bottom:12px;}
.loading-state{text-align:center;padding:48px 24px;color:var(--text-muted);font-size:14px;}
.spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--success);border-radius:50%;animation:spin .7s linear infinite;vertical-align:middle;margin-right:8px;}
@keyframes spin{to{transform:rotate(360deg);}}
.error-banner{background:var(--danger-tint);border:1px solid var(--danger);color:var(--danger-ink);border-radius:var(--radius-md);padding:12px 16px;margin-bottom:16px;font-size:14px;display:flex;gap:10px;align-items:flex-start;}
.error-banner button{margin-left:auto;background:none;border:none;color:var(--danger-ink);cursor:pointer;font-size:16px;line-height:1;}

/* ── TOAST ── */
.toast-wrap{position:fixed;bottom:24px;right:24px;display:flex;flex-direction:column;gap:8px;z-index:2000;}
/* v2 Toast: 344px, 12px Medium, background and ink from one palette per
   state. Every pair is the Toast page's own and measures over 5:1. */
.toast{display:flex;align-items:center;gap:10px;width:344px;max-width:calc(100vw - 48px);min-height:48px;padding:12px 16px;border-radius:var(--radius-md);font-size:var(--fs-xs);font-weight:500;line-height:1.35;box-shadow:var(--elev-300);animation:slide-in .2s ease-out;background:var(--info-tint);color:var(--info-ink);}
.toast.ok{background:var(--success-fill);color:var(--success-ink);}      /* 5.19:1 */
.toast.err{background:var(--danger-tint);color:var(--danger-chip-ink);}  /* 5.72:1 */
/* The state is also spelled out as a mark, so it does not rest on hue. */
.toast::before{flex-shrink:0;width:20px;height:20px;border-radius:50%;border:1.5px solid currentColor;display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;line-height:1;content:'i';}
.toast.ok::before{content:'\2713';}
.toast.err::before{content:'!';}
@keyframes slide-in{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}

/* ── MODAL ── */
.overlay{display:none;position:fixed;inset:0;background:rgba(31,33,32,.55);z-index:1000;align-items:flex-start;justify-content:center;padding:24px;overflow-y:auto;}
.overlay.open{display:flex;}
.modal{background:var(--bg);border-radius:var(--radius-xl);width:100%;max-width:720px;display:flex;flex-direction:column;box-shadow:var(--elev-500);margin:auto;overflow:hidden;}
.modal-header{padding:20px 24px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;}
.modal-header h2{font-size:var(--fs-base);font-weight:700;letter-spacing:0;}
.modal-header .sub{font-size:12px;color:var(--text-muted);margin-top:2px;font-family:ui-monospace,Consolas,monospace;}
.modal-close{background:none;border:none;font-size:18px;cursor:pointer;color:var(--text-muted);padding:4px;line-height:1;}
.modal-body{padding:20px 24px;overflow-y:auto;flex:1;max-height:72vh;}
.modal-footer{padding:14px 24px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px;flex-shrink:0;align-items:center;flex-wrap:wrap;}
.modal-footer .err{margin-right:auto;color:var(--danger);font-size:12px;}

/* The guided-create button carries a parenthetical that is genuinely useful —
   it is what distinguishes this from plain "Add Location" — but it must not
   read as loudly as the action itself. */
.btn-sub{font-weight:400;opacity:.85;}

/* With the guided flow open the footer holds four buttons and wraps to two
   rows, which left Batal sitting shoulder to shoulder with a save button —
   an easy misclick when one of them discards the form. Pushing it to the far
   left separates destructive from constructive.

   Scoped with :has so the other modals, which have room for one row, keep
   their existing layout. Where :has is unsupported nothing changes, which is
   the pre-existing arrangement rather than a broken one. */
.modal-footer:has(.wiz-only:not([hidden])) [data-close]{margin-right:auto;}
.del-modal .modal{max-width:440px;}
.del-icon{width:52px;height:52px;border-radius:50%;background:var(--danger-tint);color:var(--danger);display:flex;align-items:center;justify-content:center;font-size:22px;margin:0 auto 16px;}
.del-body{text-align:center;padding:24px;}
.del-body h3{font-size:16px;font-weight:700;margin-bottom:8px;}
.del-body p{color:var(--text-muted);font-size:14px;line-height:1.5;}
.del-footer{display:flex;justify-content:center;gap:10px;padding:0 24px 24px;}

/* ── FORM ── */
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 20px;}
.form-grid .full{grid-column:1/-1;}
.form-group{display:flex;flex-direction:column;gap:5px;}
label.flabel{font-size:var(--fs-sm);font-weight:700;color:var(--text);}
label.flabel .req{color:var(--danger);margin-left:2px;}
label.flabel .hint{font-weight:300;font-style:italic;margin-left:6px;font-size:12px;color:var(--neutral-mid);text-transform:none;letter-spacing:0;}
.form-group input,.form-group select,.form-group textarea{padding:8px 10px;border:1px solid var(--border-control);border-radius:0;min-height:40px;font-size:14px;color:var(--text);background:var(--bg);outline:none;transition:border-color .15s,box-shadow .15s;font-family:inherit;font-weight:400;}
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:var(--focus-border);box-shadow:0 0 0 3px var(--accent-tint);}
.form-group textarea{resize:vertical;min-height:64px;}
/* Text/tertiary: 2.65:1, which is why it is only ever a placeholder. */
input::placeholder,textarea::placeholder{color:var(--neutral-mid);opacity:1;}
.form-group input:disabled,.form-group select:disabled,.form-group textarea:disabled{background:var(--disabled-bg);color:var(--text-muted);border-color:var(--border);}
/* Read-only rather than disabled, so a serial number can still be selected and
   copied — which is most of what anyone opens a locked one to do. */
.form-group input[readonly]{background:var(--disabled-bg);color:var(--text-muted);border-color:var(--border);
  cursor:default;}
.fsec{font-size:12px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin:4px 0 2px;padding:0 0 6px 10px;border-left:3px solid var(--accent);border-bottom:1px solid var(--border);grid-column:1/-1;}
.field-note{font-size:12px;color:var(--text-muted);}
.field-note.err{color:var(--danger);}
/* Not every unhappy answer is a failure. --warn itself is yellow.50 #FFD748,
   which measures 1.6:1 on white and cannot carry text; --warn-ink is the same
   hue at yellow.90 and measures 5.75:1, so it passes AA at this 11px size. */
.field-note.warn{color:var(--warn-ink);}

/* ── TOGGLE ── */
.toggle-row{display:flex;align-items:center;gap:10px;}
/* v2 Toggle, Medium (52×32): "form settings, preference toggles".
   Off is #EDEDED as drawn, but that track measures 1.18:1 against the page
   and a white thumb on it less still, so the control would not be findable.
   An inset hairline in Border/Neutral/primary (3.45:1) outlines it without
   changing the drawn fill. */
.toggle{width:52px;height:32px;border-radius:var(--radius-full);background:var(--neutral-tint);box-shadow:inset 0 0 0 1px var(--border-control);border:none;cursor:pointer;position:relative;transition:background .2s,box-shadow .2s;flex-shrink:0;padding:0;}
.toggle::after{content:'';position:absolute;width:24px;height:24px;border-radius:50%;background:#fff;top:4px;left:4px;transition:left .2s;box-shadow:var(--elev-100);}
.toggle:focus-visible{outline:none;box-shadow:inset 0 0 0 1px var(--border-control),0 0 0 3px var(--info);}
.toggle:disabled{cursor:not-allowed;background:var(--disabled-bg);box-shadow:none;}
.toggle.on{background:var(--accent);box-shadow:none;}
.toggle.on:hover:not(:disabled){background:#00523C;}   /* the Toggle page's hover track */
.toggle.on:disabled{background:var(--success-fill);}
.toggle.on::after{left:24px;}

/* ── CHIP CHECKBOXES ── */
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px;}
.chip{display:flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;font-size:14px;transition:border-color .15s,background .15s;user-select:none;}
.chip:hover{border-color:var(--text);}
.chip.checked{border-color:var(--success);background:var(--accent-tint);color:var(--text);font-weight:500;}
.chip input{display:none;}
/* A machine another meter already feeds. Shown rather than hidden, so the
   operator can see it exists and who holds it, but not selectable — the
   database would refuse the pairing anyway. */
.chip-taken{opacity:.55;cursor:not-allowed;border-style:dashed;}
.chip-taken:hover{border-color:var(--border);}

/* A chip that describes a machine rather than naming a value: serial, display
   name, and the connectors inside it. Stacked, because three facts on one line
   read as a sentence nobody finishes. */
.chip-rich{align-items:flex-start;padding:8px 12px;}
.chip-body{display:flex;flex-direction:column;gap:3px;}
.chip-head strong{font-family:ui-monospace,Consolas,monospace;font-size:12px;}
/* Display name and charger type: what the machine is called and what it is,
   under the serial that identifies it. */
.chip-meta{font-size:12px;color:var(--text-muted);}

/* One table row per machine, so the four machine columns stay level however
   many connectors a machine has. Only the first row of a record carries the
   dividing line; the rest belong to it. */
.sn-tag-xs{font-size:10px;padding:1px 4px;}
#tbody-pln tr:not(.pln-row-first) > td{border-top:none;}
#tbody-pln tr.pln-row-first > td{border-top:1px solid var(--border);}
.chip-meta:empty{display:none;}
.chip-owner{color:var(--danger);font-size:12px;}
.chip-cons{display:flex;flex-wrap:wrap;gap:4px;}
.chip-con{font-size:10px;font-family:ui-monospace,Consolas,monospace;
  background:var(--surface);border:1px solid var(--border);border-radius:2px;
  padding:1px 5px;color:var(--text-muted);}
.chip-con-none{font-family:inherit;font-style:italic;border-style:dashed;}

/* ── OPERATIONAL HOURS ── */
.ops-presets{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;}
.ops-preset-btn{padding:4px 10px;border:1px solid var(--border);border-radius:var(--radius);font-size:var(--fs-xs);background:var(--bg);cursor:pointer;color:var(--text-muted);transition:border-color .15s,background .15s,color .15s;font-family:inherit;}
.ops-preset-btn:hover{border-color:var(--success);color:var(--text);background:var(--accent-tint);}

/* The chip that matches the table below, or Custom when none does.
   #09765A on the #EBFFFA tint measures 5.39:1, so the label carries its own
   weight rather than relying on the fill.

   The tick is not decoration. Colour alone would be the only signal that a
   chip is selected, which is exactly the case a colour-blind operator cannot
   read; the mark says "selected" without it. */
.ops-preset-btn.active,.ops-preset-tag.active{
  border-color:var(--success);background:var(--accent-tint);
  color:var(--success-ink);font-weight:500;}
.ops-preset-btn.active::before,.ops-preset-tag.active::before{content:'✓ ';}

/* Custom is a state readout, not a control: same chip shape so it reads as
   one of the row, but no hover and no pointer, because there is nothing to
   apply — hours become custom by editing the table, not by clicking here. */
/* Maintenance tab: rows are grouped location > machine > connector, so the
   first cell is indented to make that nesting readable. Padding on the cell
   rather than a margin on the text, so the truncation still measures against
   the full column width. */
.maint-depth-1 > td:first-child{padding-left:26px;}
.maint-depth-2 > td:first-child{padding-left:48px;}
/* The label inside is display:block, so an inline ::before would sit on its
   own line above it. Park the arrow in the padding gutter instead. */
.maint-depth-1 > td:first-child,
.maint-depth-2 > td:first-child{position:relative;}
.maint-depth-1 > td:first-child::before,
.maint-depth-2 > td:first-child::before{content:'↳';color:var(--text-muted);
  font-weight:400;position:absolute;top:50%;transform:translateY(-50%);}
.maint-depth-1 > td:first-child::before{left:10px;}
.maint-depth-2 > td:first-child::before{left:32px;}

/* A connector in maintenance whose machine is not: it has no parent row, so
   the indent needs saying out loud or it reads as belonging to the machine
   that happens to sort above it. */
.maint-orphan{color:var(--warn-ink);}

/* Cascade notice: what changed besides the thing the operator clicked. */
.cascade-list{list-style:none;margin:14px 0 0;padding:0;text-align:left;
  max-height:240px;overflow-y:auto;}
.cascade-list li{padding:7px 10px;border:1px solid var(--border);
  border-radius:var(--radius);margin-bottom:6px;font-size:14px;line-height:1.45;}
.cascade-why{color:var(--text-muted);font-size:12px;}

.ops-preset-tag{padding:4px 10px;border:1px solid var(--border);border-radius:var(--radius);
  font-size:var(--fs-xs);background:var(--bg);color:var(--text-muted);cursor:default;
  user-select:none;transition:border-color .15s,background .15s,color .15s;}
.ops-table{width:100%;border-collapse:collapse;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;font-size:14px;}
.ops-table th{padding:7px 10px;background:var(--surface);font-size:12px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;text-align:left;border-bottom:1px solid var(--border);}
.ops-table td{padding:6px 10px;border-bottom:1px solid var(--border);}
.ops-table tbody tr:last-child td{border-bottom:none;}
.ops-table tr.ops-closed td{opacity:.45;}
.ops-time{width:100px;padding:4px 6px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:var(--fs-xs);background:var(--bg);font-family:inherit;}
.ops-time:focus{border-color:var(--text);outline:none;}
.ops-table input[type=checkbox]{cursor:pointer;width:15px;height:15px;accent-color:var(--danger);}

/* ── GMAPS ROW ── */
.gmaps-row{display:flex;gap:8px;align-items:center;}
.gmaps-row input{flex:1;}

/* ── MAP ── */
.map-section{margin-top:24px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;}
.map-header{display:flex;align-items:flex-start;gap:12px;padding:14px 16px;border-bottom:1px solid var(--border);flex-wrap:wrap;}
.map-header h2{font-size:16px;font-weight:700;border-left:4px solid var(--accent);padding-left:10px;}
.map-header p{font-size:var(--fs-xs);color:var(--text-muted);margin-top:4px;padding-left:14px;}
.map-actions{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
#map-canvas{height:460px;width:100%;background:var(--surface);}
#map-canvas .placeholder{display:flex;height:100%;align-items:center;justify-content:center;flex-direction:column;gap:10px;color:var(--text-muted);font-size:14px;text-align:center;padding:24px;}
.map-legend{display:flex;align-items:center;gap:16px;padding:9px 16px;border-top:1px solid var(--border);font-size:var(--fs-xs);color:var(--text-muted);flex-wrap:wrap;}
.map-legend .lg{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:5px;vertical-align:-1px;border:1px solid rgba(31,33,32,.2);}
.lg-active{background:var(--success);}
.lg-draft{background:var(--neutral-mid);}
.lg-maint{background:var(--warn);}
.lg-other{background:var(--danger);}
/* ── ADMIN VIEWS (User Management, Access History) ── */
.admin-head{display:flex;align-items:center;gap:14px;margin-bottom:20px;flex-wrap:wrap;}
.admin-head h2{font-size:var(--fs-xl);font-weight:700;letter-spacing:1px;
  border-left:4px solid var(--accent);padding-left:12px;}
.admin-count{font-size:var(--fs-xs);color:var(--text-muted);background:var(--surface);
  border:1px solid var(--border);border-radius:var(--radius);padding:3px 10px;}
/* Explains what the view is for, before the table dumps rows at you. */
.schema-note{background:var(--info-tint);border:1px solid var(--border);
  border-left:4px solid var(--info);border-radius:var(--radius);
  padding:12px 14px;margin-bottom:16px;font-size:var(--fs-xs);line-height:1.55;color:var(--text);}
/* A note about something being wrong, rather than an explanation of the tab.
   Yellow, not red: the rows are a work list, not an error. */
.schema-note.note-warn{background:var(--warn-tint);border-left-color:var(--warn);}
/* States a limit rather than explaining the tab, and sits directly above the
   filters it constrains. Neutral so it does not read as a problem. */
.schema-note.audit-window{background:var(--surface);border-left-color:var(--neutral-mid);
  margin-top:-8px;}

/* ── Small helpers ──
   The CSP is `style-src 'self'`, which blocks style="" attributes outright —
   a blocked style is silently NOT applied, so anything that used to live
   inline belongs here instead. Do not reintroduce inline styles. */
.u-sm{font-size:12px;}
.u-xs{font-size:10px;}
.u-muted{color:var(--text-muted);}
.u-danger{color:var(--danger);}
.u-dim{opacity:.6;}
.u-center{text-align:center;}
.u-top{vertical-align:top;}
.u-bold{font-weight:700;}
.u-medium{font-weight:500;}
.u-empty{color:var(--text-muted);font-style:italic;font-size:12px;}
.u-caret{font-size:10px;color:var(--text-muted);margin-left:2px;}
.u-stack{display:flex;flex-direction:column;gap:12px;}
.filter-w{width:150px;}
/* The completeness options are full sentences; at the shared width they
   truncate to the point of being indistinguishable. Fixed rather than
   max-width, so the flex row does not stretch it across the leftover space. */
.filter-w-lg{width:300px;flex:0 0 auto;}
.pg-gap{padding:4px 6px;color:var(--text-muted);}
.col-actions{width:150px;}
.col-actions-wide{width:180px;}
.col-day{font-weight:500;width:90px;}
.col-closed{width:70px;}
/* No min-width and nothing to scroll: the fixed percentage columns above fit
   whatever width is available. The overflow rule stays for very narrow phones,
   where fifteen columns genuinely cannot fit. */
.join-scroll{overflow-x:auto;}
/* The percentages above are sized for a 1214px panel (a 1280px screen). Below
   that the Join table scrolls sideways rather than squeezing: squeezed to a
   phone's 900px, sixteen-digit serials and status badges spilled across into
   the neighbouring columns and the edit button hung out of Aksi. */
.join-table{min-width:1214px;}

/* ── CesaMap: our own slippy map (web/static/js/map.js) ── */
.cmap{position:relative;overflow:hidden;background:var(--surface);cursor:grab;touch-action:none;user-select:none;}
.cmap.cmap-dragging{cursor:grabbing;}
.cmap-tiles,.cmap-overlay{position:absolute;inset:0;}
.cmap-overlay{pointer-events:none;}
.cmap-tile{position:absolute;top:0;left:0;width:256px;height:256px;
  /* Hairline gaps appear between tiles at fractional offsets without this. */
  outline:1px solid transparent;pointer-events:none;}

/* Markers and clusters sit in a pointer-events:none overlay, so re-enable
   them individually — that keeps map dragging working everywhere else. */
.cmap-marker,.cmap-cluster{position:absolute;top:0;left:0;pointer-events:auto;
  margin:0;padding:0;border:none;font:inherit;cursor:pointer;}
/* Fixed size, so a negative margin centres it on the projected point. */
.cmap-marker{width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;
  border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.35);}
.cmap-marker:hover{filter:brightness(1.15);}
.cmap-active{background:var(--success);}
.cmap-draft{background:var(--neutral-mid);}
.cmap-maintenance{background:var(--warn);}
.cmap-decommissioned,.cmap-other{background:var(--danger);}

/* Cluster bubbles, tinted by the worst status inside: amber = something in
   maintenance, grey = all draft, green = otherwise. */
/* Size is dynamic, so a fixed negative margin will not do. The `translate`
   property composes with the `transform` JS sets, shifting by half the
   element's own size to land the bubble on the projected point. */
/* Cluster bubbles carry dark ink: the ALVA greens and yellows are too bright
   to sit under white text. */
.cmap-cluster{display:flex;align-items:center;justify-content:center;border-radius:50%;
  font-weight:700;font-size:14px;color:var(--text);border:2px solid #fff;
  box-shadow:0 2px 6px rgba(31,33,32,.3);translate:-50% -50%;}
.cmap-cluster-active{background:var(--success);}
.cmap-cluster-maint{background:var(--warn);}
.cmap-cluster-draft{background:#CFCFCF;} /* neutral.60 */

/* ── ALVA's own sites carry the CESA mark ──────────────────────────────
   PLN and other providers keep the plain dot, which is what makes the
   distinction readable at a glance.

   The mark is /favicon.svg itself rather than a copy, so the two can never
   drift; it is drawn with no background of its own, which is why the white
   field below is set here.

   Status was carried entirely by the fill colour, and the map has a status
   filter — so it moves to the ring rather than disappearing. Two-class
   selectors throughout: .cmap-active and friends are single-class and would
   otherwise win or lose on source order alone. */
.cmap-marker.cmap-marker-cesa{
  width:26px;height:26px;margin:-13px 0 0 -13px;border-radius:50%;
  background:#fff url('/favicon.svg') center/17px 17px no-repeat;
  border-width:3px;box-shadow:0 1px 5px rgba(0,0,0,.4);}
.cmap-marker-cesa.cmap-active{border-color:var(--success);}
.cmap-marker-cesa.cmap-draft{border-color:var(--neutral-mid);}
.cmap-marker-cesa.cmap-maintenance{border-color:var(--warn);}
.cmap-marker-cesa.cmap-decommissioned,
.cmap-marker-cesa.cmap-other{border-color:var(--danger);}

/* Cluster: the mark sits above the count inside one bubble.
   White field, not green: the mark is green with a white inner area, so on
   green it would need recolouring, and flattening it to a white silhouette
   loses the leaf entirely. On white it renders as drawn. The count is set in
   the same green — 10.4:1 on white, legible at the smallest bubble size. */
.cmap-cluster.cmap-cluster-cesa{
  flex-direction:column;gap:1px;background:#fff;color:#00664B;
  border-width:3px;line-height:1;}
.cmap-cluster-cesa::before{
  content:'';display:block;width:38%;height:38%;
  background:url('/favicon.svg') center/contain no-repeat;}
.cmap-cluster-cesa.cmap-cluster-active{border-color:var(--success);}
.cmap-cluster-cesa.cmap-cluster-maint{border-color:var(--warn);}
.cmap-cluster-cesa.cmap-cluster-draft{border-color:#CFCFCF;}

.cmap-zoom{position:absolute;top:10px;left:10px;display:flex;flex-direction:column;gap:2px;z-index:5;}
.cmap-zoom button{width:30px;height:30px;border:1px solid var(--border);background:var(--bg);
  color:var(--text);font-size:17px;line-height:1;cursor:pointer;border-radius:var(--radius);
  box-shadow:var(--elev-100);font-family:inherit;}
.cmap-zoom button:hover{background:var(--accent-tint);border-color:var(--success);}

.cmap-attr{position:absolute;right:0;bottom:0;z-index:5;background:rgba(255,255,255,.85);
  font-size:12px;padding:2px 6px;border-top-left-radius:var(--radius-sm);color:var(--text-muted);}
.cmap-attr a{color:var(--link);}

/* A label on a map, not a form: one line of identity, one of context, and a
   single round action. Sized to the content rather than a fixed column. */
.cmap-popup{position:absolute;z-index:6;min-width:210px;max-width:300px;background:var(--bg);
  border:1px solid var(--border);border-radius:var(--radius);
  box-shadow:var(--elev-300);   /* Shadow/300: popover */
  padding:12px 30px 12px 14px;font-size:14px;line-height:1.4;pointer-events:auto;}

/* The tail points at the marker the popup belongs to. Two stacked triangles
   fake a 1px border on the notch, which a single one cannot do. */
.cmap-popup::after,.cmap-popup::before{content:'';position:absolute;left:50%;
  border:8px solid transparent;width:0;height:0;}
.cmap-popup::before{bottom:-17px;margin-left:-8px;border-top-color:var(--border);}
.cmap-popup::after{bottom:-15px;margin-left:-8px;border-top-color:var(--bg);}

.cmap-card{display:flex;align-items:center;gap:12px;}
.cmap-card-text{min-width:0;flex:1;}
.cmap-card-title{font-weight:700;letter-spacing:0;line-height:1.25;
  overflow-wrap:anywhere;}
.cmap-card-sub{color:var(--text-muted);font-size:var(--fs-xs);margin-top:2px;
  font-family:ui-monospace,Consolas,monospace;overflow-wrap:anywhere;}

/* Round, accent-filled, dark glyph — the ALVA accent is far too bright to
   carry a white icon. */
.cmap-card-btn{flex-shrink:0;width:34px;height:34px;border-radius:50%;border:none;
  background:var(--accent);color:var(--text);font-size:16px;line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center;font-family:inherit;
  transition:filter .15s;}
.cmap-card-btn:hover{filter:brightness(.93);}

/* Inside a cluster list the rows stack, so the tail and per-row card padding
   would fight each other. */
.cmap-card-row{padding:8px 0;border-top:1px solid var(--border);}
.cmap-card-row:first-of-type{border-top:none;}
.cmap-popup strong{display:block;margin-bottom:3px;}
.cmap-popup .muted{display:block;color:var(--text-muted);font-size:12px;}
.cmap-popup-x{position:absolute;top:4px;right:6px;border:none;background:none;cursor:pointer;
  font-size:16px;line-height:1;color:var(--text-muted);padding:2px;}
.cmap-popup-x:hover{color:var(--text);}
.cmap-list-head{font-weight:500;font-size:var(--fs-xs);color:var(--text-muted);
  text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px;}
/* Scrolling belongs on an inner element, never on .cmap-popup itself: the tail
   is drawn by ::before/::after sitting below the box, and any overflow value
   other than visible counts them as scrollable overflow — which both clipped
   the tail away and produced a scrollbar on a popup that had nothing to
   scroll. */
.cmap-popup-body{max-height:320px;overflow-y:auto;}
.cmap-popup-actions{margin-top:10px;padding-top:8px;border-top:1px solid var(--border);}

/* Placeholder call-to-action, so the empty map offers the button rather than
   pointing at one somewhere else. */
.map-cta-icon{font-size:34px;line-height:1;}
#map-canvas .placeholder .btn{margin:2px 0;}

/* Preset row above the bike-type chips. */
.chip-presets{display:flex;gap:6px;flex-wrap:wrap;margin:2px 0 8px;}

/* ── FORM: wajib & amenity bebas ── */
.req-legend{grid-column:1/-1;font-size:12px;color:var(--text-muted);
  background:var(--surface);border-radius:var(--radius);padding:6px 10px;margin-bottom:2px;}
.amen-add{display:flex;gap:8px;margin-top:8px;}
.amen-add input{flex:1;min-width:0;padding:7px 10px;border:1px solid var(--border-control);
  border-radius:0;min-height:36px;font-size:var(--fs-sm);font-family:inherit;font-weight:400;}
.amen-add input:focus{outline:none;border-color:var(--focus-border);box-shadow:0 0 0 3px var(--accent-tint);}

/* ── TUTORIAL ── */
.tut{max-width:900px;}
/* A list, not a wrapped row of chips. Eleven chips of differing widths give no
   reading order and no sense of how long the guide is; one entry per line
   does both, and each row is a click target across its full width. */
.tut-toc{background:var(--white);border:1px solid var(--border);
  border-radius:var(--radius);padding:6px;margin:16px 0 28px;}
.tut-toc-title{font-size:12px;font-weight:500;text-transform:uppercase;
  letter-spacing:.06em;color:var(--text-muted);padding:8px 10px 6px;}
.tut-toc a{display:flex;align-items:center;gap:10px;text-decoration:none;
  font-size:14px;color:var(--text);padding:9px 10px;border-radius:var(--radius);
  transition:background .15s;}
.tut-toc a:hover{background:var(--surface);}
.tut-toc a + a{border-top:1px solid var(--border);}
.tut-toc-n{display:flex;align-items:center;justify-content:center;
  width:22px;height:22px;border-radius:var(--radius);background:var(--accent);
  color:var(--text);font-weight:700;font-size:12px;flex-shrink:0;}

.tut-flow{background:var(--white);border:1px solid var(--border);border-radius:var(--radius);
  border-top:4px solid var(--accent);padding:22px 24px;margin-bottom:22px;
  /* Anchor jumps land under the sticky 60px top bar without this. */
  scroll-margin-top:76px;}
.tut-flow h3{display:flex;align-items:center;gap:10px;font-size:var(--fs-lg);font-weight:700;
  letter-spacing:0;margin-bottom:4px;}
.tut-num{display:flex;align-items:center;justify-content:center;width:26px;height:26px;
  border-radius:var(--radius);background:var(--text);color:var(--white);
  font-size:14px;font-weight:700;flex-shrink:0;}
.tut-sub{color:var(--text-muted);font-size:var(--fs-sm);margin-bottom:12px;}
.tut-why{background:var(--accent-tint);border-left:3px solid var(--accent);
  padding:10px 14px;border-radius:0 var(--radius) var(--radius) 0;
  font-size:var(--fs-sm);line-height:1.55;margin-bottom:18px;}

/* A nested list inside a step. The outer .tut-steps strips its own markers
   and numbers itself with a counter, so an inner list needs its bullets and
   indent putting back. */
.tut-list{list-style:disc;margin:8px 0 0 18px;padding:0;}
.tut-list li{margin-bottom:6px;line-height:1.55;}

.tut-steps{list-style:none;counter-reset:tutstep;margin:0;padding:0;}
.tut-steps > li{counter-increment:tutstep;position:relative;padding-left:34px;
  margin-bottom:22px;}
.tut-steps > li::before{content:counter(tutstep);position:absolute;left:0;top:0;
  width:24px;height:24px;border-radius:50%;background:var(--surface);
  border:1px solid var(--border);display:flex;align-items:center;justify-content:center;
  font-size:12px;font-weight:700;color:var(--text-muted);}
.tut-text{font-size:var(--fs-sm);line-height:1.6;margin-bottom:10px;}
/* A short ordered list nested inside one step. Numbered plainly rather than
   with the circled counters above, so it reads as detail under the step it
   belongs to and not as a competing sequence. */
.tut-substeps{margin:8px 0 0 18px;padding:0;}
.tut-substeps > li{margin-bottom:6px;padding-left:4px;}
.tut-text code{background:var(--surface);border:1px solid var(--border);border-radius:2px;
  padding:1px 5px;font-family:ui-monospace,Consolas,monospace;font-size:12px;}

.tut-fig{margin:0;}
/* Full-size captures are up to 1000px tall; capping the display width keeps the
   page scannable, and the link opens the original for anyone who needs detail. */
/* Capped on both axes. Width alone left the portrait captures — full modals and
   the login card — 800px tall each, which buried the text between them. */
.tut-fig img{display:block;max-width:min(620px,100%);max-height:480px;
  width:auto;height:auto;
  border:1px solid var(--border);border-radius:var(--radius);background:var(--white);}
.tut-fig a{display:inline-block;text-decoration:none;}
.tut-fig a:hover img{border-color:var(--text-muted);}
.tut-fig figcaption{font-size:12px;color:var(--text-muted);margin-top:5px;}

/* ── ICONS ── */
/* An emoji placeholder and its replacement image occupy the same box, so
   swapping one for the other cannot shift the layout around it. */
.app-ico{display:inline-flex;align-items:center;justify-content:center;
  width:1.45em;height:1.45em;line-height:1;vertical-align:-0.35em;flex-shrink:0;}
.app-ico-img{width:100%;height:100%;object-fit:contain;display:block;}
/* The stat cards give their icon a whole 44px tile; filling it makes the
   artwork legible instead of a stamp in the middle of a box. */
.stat-icon .app-ico{width:48px;height:48px;font-size:26px;}
.stat-icon .app-ico-img{object-fit:contain;}

/* The machine is a floor-standing unit: its artwork is roughly three times
   taller than it is wide, so a square box leaves it a thin sliver. Letting it
   run taller than the line costs nothing here — both boxes have room above and
   below — and is the difference between recognising the product and not. */
.app-ico[data-ico="machine"]{width:1.1em;height:2.1em;vertical-align:-0.8em;}
.stat-icon .app-ico[data-ico="machine"]{width:20px;height:56px;}
.tabs .app-ico[data-ico="machine"]{height:1.9em;vertical-align:-0.7em;}

/* ── JOIN DETAIL PANEL ── */
.col-actions-sm{width:56px;}
.jd-loc{border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;margin-bottom:16px;}
.jd-loc-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;}
.jd-label{font-size:12px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;}
.jd-locname{font-size:var(--fs-lg);font-weight:700;letter-spacing:0;margin-top:2px;
  overflow-wrap:anywhere;}
.jd-loc-act{text-align:right;flex-shrink:0;}
.jd-inline{margin:4px 0;}
/* Status and the three actions sit as a 2×2 block -- status | edit over
   maintenance | delete -- on every width; four abreast ran off a phone's edge.
   Each item is placed by what it is rather than by order, because the
   maintenance button is absent for draft and decommissioned records, and
   delete must stay under edit either way. */
.jd-inline,.jd-mac-act,.jd-con-act{display:grid;grid-template-columns:1fr 1fr;gap:6px;align-items:center;justify-content:end;flex-shrink:0;}
.jd-inline > .badge,.jd-mac-act > .badge,.jd-con-act > .badge{grid-area:1 / 1;justify-self:center;}
.jd-inline > [data-act^="jd-edit"],.jd-mac-act > [data-act^="jd-edit"],.jd-con-act > [data-act^="jd-edit"]{grid-area:1 / 2;}
.jd-inline > [data-act^="jd-maint"],.jd-mac-act > [data-act^="jd-maint"],.jd-con-act > [data-act^="jd-maint"]{grid-area:2 / 1;}
.jd-inline > [data-act^="jd-del"],.jd-mac-act > [data-act^="jd-del"],.jd-con-act > [data-act^="jd-del"]{grid-area:2 / 2;}
.jd-gmaps{display:inline-flex;margin-top:10px;}
.jd-address{background:var(--info-tint);border:1px solid var(--info);border-radius:var(--radius-md);
  padding:8px 12px;margin-top:10px;font-size:var(--fs-sm);}
.jd-meta{margin-top:10px;font-size:var(--fs-sm);display:flex;flex-wrap:wrap;gap:4px 24px;}
.jd-pln{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:10px;
  padding-top:10px;border-top:1px solid var(--border);font-size:var(--fs-sm);}

.jd-section{font-weight:700;margin:18px 0 10px;letter-spacing:0;}
.jd-machine{border:1px solid var(--text);border-radius:var(--radius);padding:12px 14px;
  margin-bottom:12px;}
/* Same size as the connector line below it: the machine block used three
   different sizes at once — 16px for the serial, 12px for the rest — against
   the connector's 14px, so nothing lined up. */
.jd-mac-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;
  margin-bottom:10px;font-size:var(--fs-sm);}
.jd-mac-head > div:first-child > div{margin-bottom:2px;}
/* The text column must be allowed to shrink, or a long display name or serial
   pushes the 2×2 action block past the card's edge on a phone. */
.jd-mac-head > div:first-child,.jd-loc-head > div:first-child{min-width:0;overflow-wrap:anywhere;}
.jd-con{display:flex;justify-content:space-between;align-items:center;gap:12px;
  border:1px solid var(--border);border-radius:var(--radius);padding:7px 10px;margin-bottom:6px;
  font-size:var(--fs-sm);}
.jd-con-text{min-width:0;overflow-wrap:anywhere;}
/* Full-width so "add" reads as the end of the list it belongs to. */
.jd-wide{width:100%;justify-content:center;margin-top:4px;}

/* ── SERIAL NUMBER HISTORY ── */
.snhist-subject{font-weight:700;letter-spacing:0;margin-bottom:4px;overflow-wrap:anywhere;}
/* A timestamp broken across three lines is unreadable; let the table scroll
   instead, which .table-wrap already handles. */
.snhist-when{white-space:nowrap;}
/* The superseded serial stays legible but reads as past tense. */
.sn-tag.sn-old{color:var(--text-muted);text-decoration:line-through;
  text-decoration-thickness:1px;}

/* ── JOIN TABLE ── */

@media (max-width:820px){
  .stats-row{grid-template-columns:1fr;}
  /* minmax(0,1fr), not 1fr: a bare 1fr track never shrinks below its widest
     unbreakable content, and the serial pattern in a field note
     (TIPE_TANGGAL_INISIALTEMPAT_URUTAN) pushed every field past the screen. */
  .form-grid{grid-template-columns:minmax(0,1fr);}
  .form-grid .field-note{overflow-wrap:anywhere;}
  .main{padding:16px 12px 48px;}
}

/* Maintenance History: the dash means the episode is still running, which is
   the most consequential cell in the row. Left as plain text it reads like a
   missing value. */
.mh-open{font-weight:700;color:var(--warn-ink);}

/* Date bounds in a toolbar. The label sits inline with its input so the two
   read as one control and the toolbar keeps its single row of items. */
.date-filter{display:inline-flex;align-items:center;gap:6px;font-size:14px;
  font-weight:400;color:var(--text-muted);white-space:nowrap;}
.date-filter input{padding:7px 10px;border:1px solid var(--border-control);
  border-radius:0;min-height:36px;font-size:14px;background:var(--bg);color:var(--text);
  outline:none;font-family:inherit;font-weight:400;}
.date-filter input:focus{border-color:var(--focus-border);box-shadow:0 0 0 3px var(--accent-tint);}
/* States the limit beside the two inputs it applies to, quietly — the form
   already enforces it, so this only explains why the picker narrows. */
.filter-hint{font-size:var(--fs-xs);color:var(--text-muted);white-space:nowrap;}

/* What a delete takes with it. Listed rather than counted alone: "3 konektor"
   does not tell the operator whether it is the ones they meant. */
.del-cascade{margin-top:12px;padding:10px 12px;background:var(--warn-tint);
  border-radius:var(--radius);text-align:left;font-size:var(--fs-xs);line-height:1.5;}
.del-list{margin:6px 0 8px;padding-left:18px;max-height:150px;overflow-y:auto;}
.del-list li{margin:2px 0;}
.del-warn{display:block;margin-top:10px;color:var(--danger);font-weight:500;}

/* One choice per row, for the dialog that asks which connector keeps a slot.
   The whole row is the label, so the answer is a big target rather than a
   6px circle — these are read on a phone in the field. */
.radio-row{display:flex;align-items:flex-start;gap:10px;margin-top:8px;padding:10px 12px;
  border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;
  font-size:var(--fs-sm);line-height:1.45;}
.radio-row:hover{background:var(--neutral-tint);}
.radio-row input{margin:2px 0 0;accent-color:var(--accent);flex-shrink:0;}
.radio-row:has(input:checked){border-color:var(--focus-border);background:var(--success-fill);}

/* Sits under the locked Serial Number field, so it reads as the way out of
   that lock rather than as another form control. */
.sn-replace{margin-top:8px;}
/* What is about to be replaced, stated before the fields that replace it. */
.rep-target{padding:10px 12px;background:var(--surface);border:1px solid var(--border);
  border-radius:var(--radius);font-size:var(--fs-sm);line-height:1.6;}
.rep-target strong{font-family:ui-monospace,Consolas,monospace;}

/* The tutorial shortcut sits in the top bar. Hidden on narrow screens, where
   the bar is already tight and the profile menu still has the entry. */
.topnav-tut{margin-right:4px;}
@media (max-width:820px){.topnav-tut{display:none;}}

/* ── MOBILE ─────────────────────────────────────────────────────────────
   Field and maintenance teams open CESA on their phones. Three things broke
   there: the header pushed the account menu off-screen, the six tabs formed a
   946px row that widened the whole page (so the phone zoomed out to fit it),
   and because the page was 946px wide, every modal centred itself in that
   width instead of the screen and opened half off to the right. */
@media (max-width:820px){
  /* Header: the mark, the environment and the account. The subtitle, the
     Internal Tool badge and the build number do not earn a phone's width;
     the environment tag stays, because UAT and production must never be
     confused in the field. The tutorial stays reachable from the profile menu. */
  .topnav{padding:0 12px;gap:8px;}
  .logo span,.badge-internal,.ver-tag{display:none;}
  .user-info,.u-caret{display:none;}
  .user-btn{padding:3px;}

  /* Tabs: one strip that scrolls sideways. The partly cut tab at the edge is
     what tells a thumb there is more. The rule under the strip moves from a
     border to an inset shadow: with overflow set, the tabs' -1px overlap onto
     that border would become a vertical scroll of its own. */
  .tabs{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;border-bottom:none;box-shadow:inset 0 -1px 0 var(--border);}
  .tabs::-webkit-scrollbar{display:none;}
  .tab-btn{flex-shrink:0;white-space:nowrap;padding:10px 14px;margin-bottom:0;}

  /* Form modals become full-screen sheets: header and footer stay in place
     and the body scrolls between them, so Simpan is always within reach.
     Confirmation dialogs (no .modal-body) stay centred cards. Where :has is
     unsupported the form modals fall back to a card with 16px margins, which
     fits the screen now that the page no longer overflows. */
  .overlay{padding:16px;}
  .overlay:has(> .modal > .modal-body){padding:0;}
  .modal:has(> .modal-body){max-width:none;height:100vh;height:100dvh;margin:0;border-radius:0;}
  .modal:has(> .modal-body) > .modal-header{padding:14px 16px 10px;}
  .modal:has(> .modal-body) > .modal-body{max-height:none;flex:1;min-height:0;padding:16px;}
  .modal:has(> .modal-body) > .modal-footer{padding:10px 16px calc(10px + env(safe-area-inset-bottom));}

  /* 16px, or iOS Safari zooms the page in every time a field takes focus
     and leaves it zoomed after. !important because text controls are sized by
     a dozen class rules (the operating-hours times are 12px); listing them
     would miss the next one added, and one zooming field is enough to break
     the form for everyone on an iPhone. */
  input:not([type=checkbox]):not([type=radio]):not([type=range]),select,textarea{font-size:16px !important;}

  /* Actions: on a phone the buttons stack instead of lining up, so the pinned
     column is a narrow strip rather than two thirds of the screen (Machine and
     Connector had four buttons abreast: 251px of a 390px screen). */
  .actions-cell{flex-direction:column;align-items:stretch;gap:6px;}
  .table-wrap table td:has(> .actions-cell),.table-wrap table thead th:last-child{padding-left:8px;padding-right:8px;}
  /* Percentages, not 132px: under table-layout:fixed a pixel width that leaves
     the columns short of 100% gets the remainder shared back into it, and
     Actions grew straight back to 206px. 5.5% of the 1214px floor is 67px,
     the same strip Machine and Connector get for their stacked icons; the
     8.5% it gives up goes to Location Name. */
  #panel-loc th:nth-child(10){width:5.5%;}
  #panel-loc th:nth-child(1){width:25.5%;}

  /* Operating hours: four columns in the Location form. At 360px the fixed
     100px time fields and uppercase, letter-spaced headers ran 29px past the
     sheet; the times now share the width they are given. */
  .ops-table th{padding:6px;letter-spacing:0;}
  .ops-table td{padding:5px 6px;}
  .ops-time{width:100%;min-width:0;}
}

/* Pushed to the right of the tutorial heading. */
.tut-share{margin-left:auto;}
/* Off-screen rather than display:none — a hidden textarea cannot be selected,
   and selection is how the clipboard fallback works. */
.offscreen-copy{position:fixed;left:-9999px;top:0;opacity:0;}

/* ─────────── CSMS (OCPP proof of concept) — static/js/csms.js ─────────── */
.cs-tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:16px;flex-wrap:wrap;}
.cs-tab{padding:10px 20px;font-size:var(--fs-sm);color:var(--text-muted);background:none;border:none;border-bottom:4px solid transparent;margin-bottom:-1px;cursor:pointer;display:flex;align-items:center;gap:6px;font-family:inherit;}
.cs-tab:hover{color:var(--text);}
.cs-tab.active{color:var(--text);background:var(--accent-tint);border-bottom-color:var(--success);font-weight:700;}
.cs-panel{display:none;}.cs-panel.active{display:block;}
.cs-live{font-size:12px;color:var(--success-ink);}
.cs-live.cs-live-err{color:var(--danger);}
.cs-kv{display:grid;gap:6px;margin:10px 0;}
.cs-kv>div{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.cs-k{font-size:12px;color:var(--text-muted);min-width:170px;}
.cs-kv code,.cs-payload code{font-family:ui-monospace,Consolas,monospace;font-size:12px;background:var(--surface);padding:2px 6px;border-radius:4px;word-break:break-all;}
.cs-conns .badge{margin:1px 0;}
.cs-detail-modal{max-width:880px;}
.cs-grid{display:grid;grid-template-columns:170px 1fr;gap:6px 12px;font-size:var(--fs-sm);margin-bottom:8px;}
.cs-h3{font-size:var(--fs-sm);font-weight:700;margin:18px 0 8px;}
.cs-actions{display:grid;gap:8px;}
.cs-action{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
.cs-action .flabel{margin:0;min-width:110px;}
.cs-action select,.cs-action input{width:auto;min-width:140px;}
.cs-result{margin-top:12px;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--surface);font-size:var(--fs-sm);}
.cs-result.cs-result-err{background:var(--danger-tint);color:var(--danger-ink);}
.cs-result pre,.cs-payload pre{font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;margin:6px 0 0;max-height:320px;overflow:auto;}
.cs-config{max-height:320px;overflow:auto;margin-top:6px;}
.cs-config tr[data-cs-key]{cursor:pointer;}
.cs-config .cs-val{word-break:break-all;}
.cs-log td{vertical-align:top;}
.cs-payload{max-width:560px;}
.cs-payload summary{cursor:pointer;font-family:ui-monospace,Consolas,monospace;font-size:12px;word-break:break-all;}
.cs-log-bad td{background:var(--danger-tint);}
.cs-log-event td{background:var(--info-tint);}
.cs-log-bad.cs-log-event td{background:var(--danger-tint);}
@media (max-width:640px){
  .cs-grid{grid-template-columns:1fr;}
  .cs-k{min-width:0;}
}
.cs-action select,.cs-action input{padding:6px 8px;border:1px solid var(--border-control);min-height:34px;font-size:13px;color:var(--text);background:var(--bg);font-family:inherit;}
.cs-sim-form{display:flex;flex-wrap:wrap;gap:10px 14px;align-items:flex-end;margin-bottom:16px;}
.cs-sim-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--text-muted);}
.cs-sim-form select,.cs-sim-form input[type=text],.cs-sim-form input[type=number]{padding:6px 8px;border:1px solid var(--border-control);min-height:34px;font-size:13px;color:var(--text);background:var(--bg);font-family:inherit;width:150px;}
.cs-sim-form .cs-sim-check{flex-direction:row;align-items:center;min-height:34px;color:var(--text);}
.cs-sim-card{border:1px solid var(--border);border-radius:8px;padding:14px;margin-bottom:12px;display:grid;gap:10px;}
.cs-sim-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
.cs-sim-metrics{display:flex;gap:22px;flex-wrap:wrap;}
.cs-sim-metrics>div{display:flex;flex-direction:column;gap:2px;}
.cs-sim-metrics .cs-k{min-width:0;}
.cs-sim-table td{vertical-align:middle;}
.cs-sim-btns{display:flex;gap:6px;flex-wrap:nowrap;justify-content:flex-end;}
.cs-sim-table td.actions-cell{min-width:340px;}
.cs-sim-log summary{cursor:pointer;font-size:12px;color:var(--text-muted);}
.cs-sim-log pre{font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap;max-height:220px;overflow:auto;background:var(--surface);padding:8px;margin:6px 0 0;}

/* ─────────── CSMS QR ─────────── */
.cs-qr-modal{max-width:420px;}
.cs-qr-body{display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center;}
.cs-qr-body img{width:290px;height:290px;image-rendering:pixelated;border:1px solid var(--border);}
.cs-qr-connector{font-weight:700;}
.cs-qr-body code{word-break:break-all;}

@media (max-width:640px){
  .topnav .topnav-tut{padding:6px 8px;font-size:12px;}
}
