/* ═══════════════════════════════════════════════════════════════════════════
   ORIX Design System — component library v1 (docs/DESIGN_SYSTEM.md)
   ─────────────────────────────────────────────────────────────────────────
   Namespaced ds-* classes; token-driven only (no raw hex, no raw px except
   0/1px hairlines). Legacy classes in app.css stay untouched — modules
   migrate to ds-* per page (strangler pattern).
   ═══════════════════════════════════════════════════════════════════════ */

/* ─── Icons ─────────────────────────────────────────────────────────────── */
.ds-icon{display:inline-flex;flex-shrink:0;vertical-align:middle;}
.ds-icon svg{display:block;width:100%;height:100%;}
.ds-icon-16{width:16px;height:16px;}
.ds-icon-20{width:20px;height:20px;}
.ds-icon-24{width:24px;height:24px;}

/* ─── Screen-reader-only text ───────────────────────────────────────────── */
.ds-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}

/* ─── Focus contract ────────────────────────────────────────────────────── */
.ds-btn:focus-visible,.ds-btn-icon:focus-visible,.ds-tile:focus-visible,
.ds-card-link:focus-visible,.ds-stat-link:focus-visible,
.ds-item:focus-visible,.row-menu button:focus-visible{
  outline:none;box-shadow:var(--shadow-focus);
  border-color:var(--color-primary);
}

/* ─── Buttons ───────────────────────────────────────────────────────────── */
.ds-btn{
  display:inline-flex;align-items:center;justify-content:center;
  gap:var(--space-2);
  min-height:var(--control-height);padding:0 var(--space-4);
  font-family:inherit;font-size:var(--font-size-body);
  font-weight:var(--weight-semibold);line-height:1;
  border-radius:var(--radius-md);
  border:var(--border-width-strong) solid transparent;
  cursor:pointer;white-space:nowrap;text-decoration:none;
  transition:background var(--duration-fast) var(--ease-out),
             border-color var(--duration-fast) var(--ease-out),
             color var(--duration-fast) var(--ease-out);
  -webkit-tap-highlight-color:transparent;touch-action:manipulation;
}
.ds-btn:hover{text-decoration:none;}
.ds-btn:disabled,.ds-btn[aria-disabled="true"]{
  opacity:var(--opacity-disabled);cursor:not-allowed;pointer-events:none;}
.ds-btn-primary{background:var(--color-primary);color:var(--color-surface);}
.ds-btn-primary:hover:not(:disabled){background:var(--color-primary-hover);}
.ds-btn-primary:active:not(:disabled){background:var(--color-primary-pressed);}
.ds-btn-secondary{background:var(--color-surface);color:var(--color-text);
  border-color:var(--color-border);}
.ds-btn-secondary:hover:not(:disabled){background:var(--color-surface-hover);}
.ds-btn-secondary:active:not(:disabled){background:var(--color-border-subtle);}
.ds-btn-ghost{background:transparent;color:var(--color-primary);
  border-color:var(--color-primary);}
.ds-btn-ghost:hover:not(:disabled){background:var(--color-primary-muted);}
.ds-btn-text{background:transparent;color:var(--color-primary);border-color:transparent;}
.ds-btn-text:hover:not(:disabled){background:var(--color-primary-muted);}
.ds-btn-danger{background:var(--color-danger);color:var(--color-surface);}
.ds-btn-danger:hover:not(:disabled){background:var(--color-danger-hover);}
.ds-btn-sm{min-height:var(--control-height-sm);padding:0 var(--space-3);
  font-size:var(--font-size-small);}
.ds-btn-lg{min-height:var(--control-height-lg);padding:0 var(--space-5);
  font-size:var(--font-size-body-lg);}

/* Segmented toggle group (Gate C's PROJECT/FLM work-type choice, Gate E's
   Calendar/Overview switch) — a row of ds-btn-secondary buttons that read as
   ONE control, the currently-chosen one visually distinct via .is-active.
   This was already relied on (assignment-drawer.js) with no CSS backing it;
   filled in here rather than duplicated per caller. */
.ds-btn-group{display:inline-flex;}
.ds-btn-group .ds-btn{border-radius:0;margin-left:-1px;}
.ds-btn-group .ds-btn:first-child{border-top-left-radius:var(--btn-radius);
  border-bottom-left-radius:var(--btn-radius);margin-left:0;}
.ds-btn-group .ds-btn:last-child{border-top-right-radius:var(--btn-radius);
  border-bottom-right-radius:var(--btn-radius);}
.ds-btn-group .ds-btn.is-active{position:relative;z-index:1;
  background:var(--color-primary);color:var(--color-surface);border-color:var(--color-primary);}
.ds-btn-group .ds-btn.is-active:hover{background:var(--color-primary-hover);}
.ds-btn.is-loading{position:relative;color:transparent;pointer-events:none;}
.ds-btn.is-loading::after{
  content:"";position:absolute;width:16px;height:16px;
  border:2px solid var(--color-border);border-top-color:var(--color-primary);
  border-radius:var(--radius-pill);animation:ds-spin 0.7s linear infinite;}

/* Icon button: square, icon-only, requires aria-label */
.ds-btn-icon{
  display:inline-flex;align-items:center;justify-content:center;
  width:var(--control-height);height:var(--control-height);
  min-width:var(--touch-target);min-height:var(--touch-target);
  padding:0;background:transparent;border:var(--border-width) solid transparent;
  border-radius:var(--radius-md);color:var(--color-text-muted);cursor:pointer;
  transition:background var(--duration-fast) var(--ease-out),
             color var(--duration-fast) var(--ease-out);
  -webkit-tap-highlight-color:transparent;touch-action:manipulation;
}
.ds-btn-icon:hover{background:var(--color-surface-hover);color:var(--color-text);}
.ds-btn-icon-danger:hover{background:var(--color-error-bg);color:var(--color-danger);}

/* ─── Page header ───────────────────────────────────────────────────────── */
.ds-page-header{
  display:flex;align-items:flex-start;justify-content:space-between;
  gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-5);
}
.ds-page-title{
  font-size:var(--font-size-h1);font-weight:var(--weight-bold);
  line-height:var(--leading-tight);color:var(--color-heading);margin:0;
  display:flex;align-items:center;gap:var(--space-2);
}
.ds-page-subtitle{
  font-size:var(--font-size-body);color:var(--color-text-muted);
  margin-top:var(--space-1);
}
.ds-page-actions{display:flex;gap:var(--space-2);align-items:center;
  flex-wrap:wrap;}

/* ─── Section header ────────────────────────────────────────────────────── */
.ds-section-header{
  display:flex;align-items:center;justify-content:space-between;
  gap:var(--space-3);margin:var(--space-6) 0 var(--space-3);
}
.ds-section-title{
  font-size:var(--font-size-h2);font-weight:var(--weight-bold);
  line-height:var(--leading-tight);color:var(--color-heading);margin:0;
}

/* ─── Cards ─────────────────────────────────────────────────────────────── */
/* Elevation-first (DS v2): the card floats on shadow; the hairline border is
   kept only to hold the edge on low-contrast surfaces. Driven entirely by the
   layer-3 --card-* tokens, so restyling a card never means editing this rule. */
.ds-card{
  background:var(--card-bg);
  border:var(--border-width) solid var(--card-border);
  border-radius:var(--card-radius);
  box-shadow:var(--card-shadow);
  padding:var(--card-padding-compact);margin-bottom:var(--space-4);
}
@media(min-width:768px){.ds-card{padding:var(--card-padding);}}
/* Wraps rather than squeezes: in a half-width card a long title and a long
   sub would otherwise each break mid-phrase. The sub drops to its own line. */
.ds-card-header{
  display:flex;align-items:center;justify-content:space-between;
  gap:var(--space-2) var(--space-3);margin-bottom:var(--space-4);flex-wrap:wrap;
}
.ds-card-header .ds-card-title{min-width:0;}
.ds-card-title{
  font-size:var(--font-size-h3);font-weight:var(--weight-semibold);
  line-height:var(--leading-normal);color:var(--color-heading);margin:0;
}
.ds-card-sub{font-size:var(--font-size-small);color:var(--color-text-muted);}
a.ds-card-link{display:block;color:inherit;text-decoration:none;cursor:pointer;
  transition:border-color var(--duration-fast) var(--ease-out),
             box-shadow var(--duration-fast) var(--ease-out);}
a.ds-card-link:hover{border-color:var(--color-primary);
  box-shadow:var(--shadow-xs);text-decoration:none;}

/* ─── Key-value rows (info card) ────────────────────────────────────────── */
.ds-kv{display:flex;justify-content:space-between;gap:var(--space-3);
  padding:var(--space-3) 0;
  border-bottom:var(--border-width) solid var(--color-border-subtle);}
.ds-kv:last-child{border-bottom:none;}
.ds-kv-label{font-size:var(--font-size-small);color:var(--color-text-muted);
  flex-shrink:0;}
.ds-kv-value{font-size:var(--font-size-body);font-weight:var(--weight-medium);
  color:var(--color-heading);text-align:right;word-break:break-word;}
.ds-kv-total{border-top:var(--border-width-accent) solid var(--color-border);
  border-bottom:none;font-weight:var(--weight-bold);}

/* ─── List rows (data items) ────────────────────────────────────────────── */
.ds-list{display:flex;flex-direction:column;gap:var(--space-2);}
.ds-item{
  display:flex;align-items:center;gap:var(--space-3);
  background:var(--color-surface);
  border:var(--border-width) solid var(--color-border);
  border-radius:var(--radius-lg);
  padding:var(--space-3) var(--space-4);min-height:var(--touch-target);
  color:inherit;text-decoration:none;
  transition:border-color var(--duration-fast) var(--ease-out),
             box-shadow var(--duration-fast) var(--ease-out);
}
/* The pointer and the hover lift are promises that the whole row is clickable,
   so they belong to the rows that actually are: a row rendered as a plain
   element (its action sits in a button at the end) must not claim otherwise. */
a.ds-item,button.ds-item{cursor:pointer;}
a.ds-item:hover,button.ds-item:hover{
  border-color:var(--color-primary);box-shadow:var(--shadow-xs);
  text-decoration:none;}
.ds-item-icon{
  display:flex;align-items:center;justify-content:center;flex-shrink:0;
  width:36px;height:36px;border-radius:var(--radius-md);
  background:var(--color-primary-muted);color:var(--color-primary);
}
.ds-item-main{flex:1;min-width:0;}
.ds-item-title{font-size:var(--font-size-body);font-weight:var(--weight-semibold);
  color:var(--color-heading);}
.ds-item-sub{font-size:var(--font-size-small);color:var(--color-text-muted);
  margin-top:2px;}
.ds-item-end{flex-shrink:0;display:flex;align-items:center;gap:var(--space-2);}

/* ─── Flush list (rows inside a card) — ORIX DS v2 ───────────────────────────
   A card that contains a list should hold ROWS, not a stack of smaller cards:
   DS v2 never nests a surface inside a surface. This opt-in variant strips the
   row's own border, radius and background and separates rows with a hairline,
   so the card is the only surface. Plain .ds-list elsewhere is untouched. */
.ds-list-flush{gap:0;}
.ds-list-flush .ds-item{
  background:transparent;border:none;border-radius:var(--radius-sm);
  box-shadow:none;padding:var(--space-3);position:relative;
}
.ds-list-flush .ds-item + .ds-item::before{
  content:"";position:absolute;top:0;left:var(--space-3);right:var(--space-3);
  border-top:var(--border-width) solid var(--color-border-subtle);
}
.ds-list-flush a.ds-item:hover,.ds-list-flush button.ds-item:hover{
  background:var(--color-surface-hover);box-shadow:none;border-color:transparent;
}
/* Title and sub are spans, so they flow inline and merge when the title wraps
   — the customer name and the time run together on a narrow screen. Making
   them blocks separates the two lines (and lets the sub's margin apply). */
.ds-list-flush .ds-item-title,
.ds-list-flush .ds-item-sub{display:block;}

/* The figure that matters sits right, above its status. */
.ds-list-flush .ds-item-end{
  flex-direction:column;align-items:flex-end;gap:var(--space-1);
}

/* A row whose end carries an ACTION, not just a status or an amount. A button
   needs real width, and on a phone keeping it beside the text squeezes the
   subtitle into a five-line column. Below the tablet breakpoint the end moves
   to its own line and lays out horizontally; from 768px up the row is exactly
   the row above. One component that answers to width — not a mobile variant. */
@media(max-width:767px){
  .ds-list-flush .ds-item--action{flex-wrap:wrap;}
  /* Leaves room for the icon and the gap, so only the end wraps. */
  .ds-list-flush .ds-item--action .ds-item-main{
    min-width:calc(100% - 36px - var(--space-3));
  }
  .ds-list-flush .ds-item--action .ds-item-end{
    flex-direction:row;align-items:center;justify-content:flex-end;
    width:100%;gap:var(--space-2);
  }
  /* A compact button is fine beside a mouse; here it is a thumb target, so the
     platform's own --touch-target applies. Scoped to this row so dense desktop
     toolbars keep their small buttons. */
  .ds-list-flush .ds-item--action .ds-item-end .ds-btn{
    min-height:var(--touch-target);
  }
}
.ds-item-amount{
  font-size:var(--font-size-body);font-weight:var(--weight-bold);
  color:var(--color-heading);font-variant-numeric:tabular-nums;white-space:nowrap;
}
/* A day's total carries real weight, not the caption styling of a subtitle. */
.ds-card-total{
  font-size:var(--font-size-h3);font-weight:var(--weight-bold);
  color:var(--color-heading);font-variant-numeric:tabular-nums;white-space:nowrap;
}

/* A week day with no hours recedes rather than shouting an em dash. */

/* ─── Stat / metric cards ───────────────────────────────────────────────── */
.ds-stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
  gap:var(--gap-grid);}
.ds-stat{
  background:var(--color-surface);
  border:var(--border-width) solid var(--color-border);
  border-radius:var(--radius-lg);padding:var(--space-4);
  display:block;color:inherit;text-decoration:none;
}
a.ds-stat-link{cursor:pointer;
  transition:border-color var(--duration-fast) var(--ease-out),
             box-shadow var(--duration-fast) var(--ease-out);}
a.ds-stat-link:hover{border-color:var(--color-primary);
  box-shadow:var(--shadow-xs);text-decoration:none;}
.ds-stat-value{
  font-size:var(--font-size-h1);font-weight:var(--weight-extrabold);
  line-height:var(--leading-tight);color:var(--color-heading);
  font-variant-numeric:tabular-nums;
}
.ds-stat-label{font-size:var(--font-size-caption);
  font-weight:var(--weight-medium);color:var(--color-text-muted);
  margin-top:var(--space-1);}

/* ─── Quick action tiles ────────────────────────────────────────────────── */
.ds-tile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
  gap:var(--gap-grid);}
.ds-tile{
  display:flex;align-items:center;gap:var(--space-3);
  background:var(--color-surface);
  border:var(--border-width) solid var(--color-border);
  border-radius:var(--radius-lg);padding:var(--space-4);
  min-height:var(--touch-target);
  color:inherit;text-decoration:none;cursor:pointer;
  transition:border-color var(--duration-fast) var(--ease-out),
             box-shadow var(--duration-fast) var(--ease-out);
}
.ds-tile:hover{border-color:var(--color-primary);box-shadow:var(--shadow-xs);
  text-decoration:none;}
.ds-tile-icon{
  display:flex;align-items:center;justify-content:center;flex-shrink:0;
  width:var(--touch-target);height:var(--touch-target);
  border-radius:var(--radius-md);
  background:var(--color-primary-muted);color:var(--color-primary);
}
/* Both are spans so they sit in one flex child; blocks put the description on
   its own line instead of letting it run on from the title. */
.ds-tile-title{display:block;font-size:var(--font-size-body);
  font-weight:var(--weight-semibold);color:var(--color-heading);}
.ds-tile-desc{display:block;font-size:var(--font-size-small);
  color:var(--color-text-muted);margin-top:2px;}

/* ─── Badges & status chips ─────────────────────────────────────────────── */
.ds-badge{
  display:inline-flex;align-items:center;gap:var(--space-1);
  font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  line-height:1;padding:var(--space-1) var(--space-2);
  border-radius:var(--radius-pill);white-space:nowrap;
  background:var(--color-surface-sunken);color:var(--color-text-muted);
}
.ds-chip-draft{background:var(--status-draft-bg);color:var(--status-draft-text);}
.ds-chip-submitted{background:var(--status-submitted-bg);color:var(--status-submitted-text);}
.ds-chip-approved{background:var(--status-approved-bg);color:var(--status-approved-text);}
.ds-chip-rejected{background:var(--status-rejected-bg);color:var(--status-rejected-text);}
.ds-chip-changes_requested{background:var(--status-changes-bg);color:var(--status-changes-text);}

/* ─── Avatar ────────────────────────────────────────────────────────────── */
.ds-avatar{
  display:inline-flex;align-items:center;justify-content:center;
  width:var(--control-height);height:var(--control-height);
  border-radius:var(--radius-pill);
  background:var(--color-primary);color:var(--color-surface);
  font-size:var(--font-size-body);font-weight:var(--weight-bold);
}
.ds-avatar-sm{width:28px;height:28px;font-size:var(--font-size-caption);}
.ds-avatar-lg{width:64px;height:64px;font-size:var(--font-size-h1);}

/* ─── Filter bar ────────────────────────────────────────────────────────── */
.ds-filter-bar{
  display:flex;gap:var(--space-3);align-items:flex-end;flex-wrap:wrap;
}
.ds-filter-bar .ds-field{margin-bottom:0;flex:1;min-width:150px;}
/* D4.1A — "toon geannuleerd" filter toggle; mirrors bp-weekday's checkbox
   pattern (same sizing/accent tokens), scoped to filter-bar checkboxes.
 *
 * Corrective fix (week/day-view overflow): this control is a <label>, and
 * two PRE-EXISTING, more specific rules were silently winning over it —
 * ".ds-form label{display:block}" (any label inside a .ds-form, which this
 * always is) turned the label back into a block box, breaking the flex row
 * that centres the checkbox against its text; ".ds-filter-bar input
 * {width:100%}" (any input directly inside .ds-filter-bar) stretched the
 * 20x20 checkbox itself to the row's full width. Combined with
 * white-space:nowrap, the label text had nowhere left to go but out past
 * the card and off the right edge of the viewport. Both rules are scoped
 * to ".ds-filter-bar …" here (two classes) specifically so they can never
 * again lose that specificity fight, regardless of source order.
 * flex-shrink:0 keeps the whole control from ever being compressed below
 * its own content — .ds-field's min-width:150px is what makes room for it
 * by wrapping to a new line first when the row is genuinely too narrow,
 * exactly like every other filter-bar item already does. */
.ds-filter-bar .ds-checkbox-field{
  display:flex;align-items:center;gap:var(--space-2);
  min-height:var(--touch-target);cursor:pointer;
  font-size:var(--font-size-body);white-space:nowrap;
  flex-shrink:0;
}
.ds-filter-bar .ds-checkbox-field input{width:20px;height:20px;flex-shrink:0;accent-color:var(--color-primary);}

/* ─── Forms ─────────────────────────────────────────────────────────────── */
.ds-form .ds-field,.ds-field{margin-bottom:var(--space-4);}
.ds-field label,.ds-form label{
  display:block;font-size:var(--font-size-small);
  font-weight:var(--weight-semibold);color:var(--color-heading);
  margin-bottom:var(--space-1);
}
.ds-form input[type=text],.ds-form input[type=email],.ds-form input[type=number],
.ds-form input[type=tel],.ds-form input[type=search],.ds-form input[type=password],
.ds-form input[type=date],.ds-form input[type=time],
.ds-form input[type=datetime-local],.ds-form select,.ds-form textarea,
.ds-filter-bar input,.ds-filter-bar select{
  width:100%;min-height:var(--touch-target);
  padding:var(--space-2) var(--space-3);
  font-size:var(--font-size-body-lg);
  border:var(--border-width-strong) solid var(--color-border);
  border-radius:var(--radius-md);
  background:var(--color-surface);color:var(--color-text);
  transition:border-color var(--duration-base) var(--ease-out),
             box-shadow var(--duration-base) var(--ease-out);
}
.ds-form textarea{min-height:calc(var(--touch-target) * 2);resize:vertical;}
.ds-form .ds-hint{font-size:var(--font-size-small);color:var(--color-text-muted);
  margin-top:var(--space-1);}
.ds-form .ds-field-error{font-size:var(--font-size-small);color:var(--color-error);
  font-weight:var(--weight-semibold);margin-top:var(--space-1);}
.ds-form-row{display:grid;gap:var(--space-3);}
.ds-form-row.cols-2{grid-template-columns:1fr 1fr;}
.ds-form-row.cols-3{grid-template-columns:1fr 1fr 1fr;}
@media(max-width:600px){
  .ds-form-row.cols-2,.ds-form-row.cols-3{grid-template-columns:1fr;}
}
.ds-form-actions{
  display:flex;gap:var(--space-2);flex-wrap:wrap;align-items:center;
  padding-top:var(--space-4);margin-top:var(--space-4);
  border-top:var(--border-width) solid var(--color-border-subtle);
}
.ds-form-actions-right{margin-left:auto;display:flex;gap:var(--space-2);}
.ds-form fieldset{border:0;padding:0;margin:0;min-width:0;}
.ds-input-group{display:flex;gap:var(--space-2);align-items:stretch;}
.ds-input-group > :first-child{flex:1;min-width:0;}
.ds-radio-group{display:flex;gap:var(--space-4);flex-wrap:wrap;}
.ds-radio-group label{display:inline-flex;align-items:center;gap:var(--space-2);
  font-weight:var(--weight-regular);font-size:var(--font-size-body);
  margin-bottom:0;min-height:var(--touch-target);cursor:pointer;}
.ds-radio-group input[type=radio]{width:20px;height:20px;accent-color:var(--color-primary);}

/* ─── Bulk planning drawer (D4C) ────────────────────────────────────────────
   Multi-engineer / multi-day scheduling form. Built entirely from existing
   tokens — no new colour or spacing scale, matching every other form. */
.bp-checklist{
  display:flex;flex-direction:column;gap:var(--space-1);
  max-height:220px;overflow-y:auto;
  border:var(--border-width) solid var(--color-border-subtle);
  border-radius:var(--radius-md);padding:var(--space-2);
}
.bp-checklist-inline{flex-direction:row;flex-wrap:wrap;max-height:none;
  border:0;padding:0;gap:var(--space-3);}
.bp-engineer-row,.bp-weekday{
  display:flex;align-items:center;gap:var(--space-2);
  min-height:var(--touch-target);cursor:pointer;
  font-size:var(--font-size-body);
}
.bp-engineer-row input,.bp-weekday input{
  width:20px;height:20px;flex-shrink:0;accent-color:var(--color-primary);
}
.bp-not-on-roster{margin:0;color:var(--color-warning);}
.bp-exception-add{display:flex;gap:var(--space-2);align-items:stretch;}
.bp-exception-add input{flex:1;min-width:0;}
.bp-exception-list{
  list-style:none;margin:var(--space-2) 0 0;padding:0;
  display:flex;flex-direction:column;gap:var(--space-1);
}
.bp-exception-list li{
  display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);
  font-size:var(--font-size-small);
}
.bp-roster-warning{
  display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;
  gap:var(--space-2);padding:var(--space-2) 0;
  border-top:var(--border-width) solid var(--color-warning-border);
}
.bp-roster-warning:first-of-type{border-top:0;}
.bp-roster-warning-actions{display:flex;gap:var(--space-2);flex-wrap:wrap;}
.bp-preview-rows{display:flex;flex-direction:column;gap:var(--space-3);}
.bp-preview-engineer-name{font-weight:var(--weight-semibold);color:var(--color-heading);
  margin-bottom:var(--space-1);}
.bp-preview-dates{list-style:none;margin:0;padding:0;
  display:flex;flex-direction:column;gap:var(--space-1);}
.bp-preview-dates li{
  display:flex;align-items:center;gap:var(--space-2);
  font-size:var(--font-size-small);padding:var(--space-1) var(--space-2);
  border-radius:var(--radius-sm);
}
.bp-row-duplicate{color:var(--color-text-muted);}
.bp-row-conflict{background:var(--color-warning-bg);color:var(--color-warning);}

/* ─── Table ─────────────────────────────────────────────────────────────── */
.ds-table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;
  border-radius:var(--radius-md);}
.ds-table-wrap:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
.ds-table{width:100%;border-collapse:collapse;font-size:var(--font-size-body);}
.ds-table th{
  text-align:left;padding:var(--space-2);
  font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em;
  border-bottom:var(--border-width-accent) solid var(--color-border-subtle);
  white-space:nowrap;
}
.ds-table td{padding:var(--space-3) var(--space-2);
  border-bottom:var(--border-width) solid var(--color-border-subtle);}
.ds-table tbody tr{transition:background var(--duration-fast) var(--ease-out);}
.ds-table tbody tr:hover{background:var(--color-surface-hover);}
.ds-num{font-variant-numeric:tabular-nums;text-align:right;}

/* ─── Row action menu (U1.2C) ────────────────────────────────────────────
   One compact `[⋮]` overflow trigger per table row, replacing permanent
   inline action buttons. `.row-menu` is `position:fixed` — see
   ui.js's positionFloatingMenu() for why (a `.ds-table-wrap` row menu would
   otherwise risk clipping by the wrap's own overflow-x:auto). Visual shape
   and item styling deliberately mirror .account-dropdown/.notif-panel
   (app.css) — the same anchored-popover pattern, not a second menu system;
   open/close motion is wired via wireMenuSurface() + motion.css's existing
   M3 ds-menu-in/out-below keyframes, reused as-is. */
.row-menu-wrap{position:relative;display:inline-flex;}
.row-menu{position:fixed;z-index:150;min-width:180px;
  background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);
  box-shadow:0 8px 28px var(--color-shadow);padding:6px;}
.row-menu button{display:block;width:100%;text-align:left;
  padding:9px 12px;font-size:13px;color:var(--color-text);background:none;border:none;
  border-radius:var(--radius-sm);cursor:pointer;}
.row-menu button:hover{background:var(--color-surface-hover);}
.row-menu-divider{border-top:var(--border-width) solid var(--color-border-subtle);margin:6px 4px;}
/* Archive is destructive-of-current-access (not permanent deletion) — the
   divider above already separates it structurally; color is a second,
   non-exclusive cue, never the only one (label text + confirmation wording
   carry the meaning on their own).
   U1.2C visual-verification fix: `.row-menu button` (one class + one
   element = 0,1,1) was silently outweighing `.row-menu-item-danger` alone
   (one class = 0,1,0), so Archive rendered in the same color as every other
   item despite carrying the modifier class — caught by inspecting
   getComputedStyle() against a real rendered menu. Qualifying the selector
   with `.row-menu button` again restores equal specificity, so source
   order (declared after the base rule) decides, as intended. */
.row-menu button.row-menu-item-danger{color:var(--color-danger);}
.row-menu button.row-menu-item-danger:hover{background:var(--color-error-bg);color:var(--color-danger);}

/* ─── Alerts (ds variant) ───────────────────────────────────────────────── */
.ds-alert{
  display:flex;gap:var(--space-2);align-items:flex-start;
  padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);
  font-size:var(--font-size-body);line-height:var(--leading-normal);
  margin-bottom:var(--space-4);
  border:var(--border-width) solid transparent;
}
.ds-alert-success{background:var(--color-success-bg);color:var(--color-success);
  border-color:var(--color-success-border);}
.ds-alert-error{background:var(--color-error-bg);color:var(--color-danger);
  border-color:var(--color-error);}
.ds-alert-info{background:var(--color-info-bg);color:var(--color-info);
  border-color:var(--color-info-border);}
.ds-alert-warning{background:var(--color-warning-bg);color:var(--color-warning);
  border-color:var(--color-warning-border);}

/* ─── Toast ─────────────────────────────────────────────────────────────── */
.ds-toast{
  position:fixed;bottom:var(--space-4);right:var(--space-4);
  z-index:var(--z-toast);max-width:320px;
  padding:var(--space-3) var(--space-4);
  border-radius:var(--radius-md);box-shadow:var(--shadow-md);
  font-size:var(--font-size-body);font-weight:var(--weight-semibold);
  color:var(--color-surface);
  opacity:1;transform:translateY(0);
  transition:opacity var(--duration-slow) var(--ease-in),
             transform var(--duration-slow) var(--ease-in);
}
.ds-toast-success{background:var(--color-success);}
.ds-toast-error{background:var(--color-danger);}
.ds-toast-warning{background:var(--color-warning);}
.ds-toast.is-leaving{opacity:0;transform:translateY(var(--space-2));}
@media(max-width:767px){
  /* Above the 52px bottom navigation + safe area */
  .ds-toast{left:var(--space-4);right:var(--space-4);max-width:none;
    bottom:calc(76px + env(safe-area-inset-bottom));}
}

/* ─── Skeleton ──────────────────────────────────────────────────────────── */
.ds-skeleton{
  position:relative;overflow:hidden;
  background:var(--color-border-subtle);border-radius:var(--radius-sm);
}
.ds-skeleton::after{
  content:"";position:absolute;inset:0;transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,
    var(--color-surface-hover),transparent);
  animation:ds-shimmer 1.4s var(--ease-out) infinite;
}
.ds-skeleton-text{height:14px;margin-bottom:var(--space-2);}
.ds-skeleton-title{height:20px;width:40%;margin-bottom:var(--space-3);}
.ds-skeleton-row{height:var(--touch-target);border-radius:var(--radius-lg);
  margin-bottom:var(--space-2);}
.ds-skeleton-card{height:96px;border-radius:var(--radius-lg);
  margin-bottom:var(--space-4);}
@keyframes ds-shimmer{100%{transform:translateX(100%);}}
@keyframes ds-spin{to{transform:rotate(360deg);}}

/* ─── Empty state ───────────────────────────────────────────────────────── */
.ds-empty{
  display:flex;flex-direction:column;align-items:center;text-align:center;
  padding:var(--space-7) var(--space-5);color:var(--color-text-muted);
}
.ds-empty-icon{
  display:flex;align-items:center;justify-content:center;
  width:56px;height:56px;border-radius:var(--radius-pill);
  background:var(--color-surface-sunken);color:var(--color-text-subtle);
  margin-bottom:var(--space-3);
}
.ds-empty-title{
  font-size:var(--font-size-h3);font-weight:var(--weight-semibold);
  color:var(--color-heading);margin:0 0 var(--space-1);
}
.ds-empty-body{font-size:var(--font-size-body);max-width:36em;margin:0;}
.ds-empty .ds-btn{margin-top:var(--space-4);}
.ds-empty-icon--error{background:var(--color-error-bg);color:var(--color-danger);}
.ds-alert svg{flex-shrink:0;}
.ds-alert-list{margin:var(--space-1) 0 0 var(--space-5);}

/* ═══ Admin operations (Sprint A) ═══════════════════════════════════════════ */

/* Layout grids */
.ds-grid-2{display:grid;grid-template-columns:1fr;gap:var(--space-4);}
@media(min-width:900px){
  .ds-grid-2{grid-template-columns:1fr 1fr;}
  .ds-grid-2 .ds-span-2{grid-column:1 / -1;}
}

/* KPI cards — colourful accents, calm surfaces */
.ds-kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));
  gap:var(--gap-grid);margin-bottom:var(--space-4);}
.ds-kpi{
  display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-2);
  border:var(--border-width) solid var(--color-border);border-radius:var(--radius-lg);
  padding:var(--space-4);background:var(--color-surface);color:inherit;text-decoration:none;
  border-left:3px solid var(--color-border);
  transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out);
}
a.ds-kpi:hover{box-shadow:var(--shadow-xs);text-decoration:none;}
a.ds-kpi:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
.ds-kpi-value{font-size:var(--font-size-h1);font-weight:var(--weight-extrabold);
  line-height:var(--leading-tight);color:var(--color-heading);font-variant-numeric:tabular-nums;}
.ds-kpi-label{font-size:var(--font-size-caption);font-weight:var(--weight-medium);
  color:var(--color-text-muted);margin-top:var(--space-1);}
.ds-kpi-icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;
  width:34px;height:34px;border-radius:var(--radius-md);}
.ds-kpi--time{background:var(--module-time-bg);border-left-color:var(--module-time);}
.ds-kpi--time .ds-kpi-icon{color:var(--module-time);background:var(--color-surface);}
.ds-kpi--flm{background:var(--module-flm-bg);border-left-color:var(--module-flm);}
.ds-kpi--flm .ds-kpi-icon{color:var(--module-flm);background:var(--color-surface);}
.ds-kpi--projects{background:var(--module-projects-bg);border-left-color:var(--module-projects);}
.ds-kpi--projects .ds-kpi-icon{color:var(--module-projects);background:var(--color-surface);}
.ds-kpi--review{background:var(--module-review-bg);border-left-color:var(--module-review);}
.ds-kpi--review .ds-kpi-icon{color:var(--module-review);background:var(--color-surface);}
.ds-kpi--warn{background:var(--module-warn-bg);border-left-color:var(--module-warn);}
.ds-kpi--warn .ds-kpi-icon{color:var(--module-warn);background:var(--color-surface);}
/* ORIX Field Engineer Dashboard Foundation (D2): the approved handoff's own
   rule — "KPI-kaarten zijn neutraal; kleur is uitsluitend statusdrager" — no
   module-coloured background or left accent bar. The base .ds-kpi rule
   above already IS this treatment (surface background, neutral border);
   this variant exists only to give the icon a calm, non-accented container
   instead of inheriting no colour at all. Used exclusively by the engineer
   KPI cards (engKpi() in dashboard-cards.js) via kpi({neutral:true}) — admin
   KPI tiles are untouched and keep their existing --module-* accents. */
.ds-kpi--neutral .ds-kpi-icon{color:var(--color-text-muted);background:var(--color-surface-sunken);}

/* Charts — the table IS the chart; SVG bars are aria-hidden enhancement */
.ds-chart-card .ds-card-header{margin-bottom:var(--space-3);}
.ds-chart{width:100%;border-collapse:collapse;font-size:var(--font-size-small);}
.ds-chart th,.ds-chart td{padding:var(--space-1) 0;vertical-align:middle;}
.ds-chart-label{text-align:left;font-weight:var(--weight-medium);color:var(--color-text);
  white-space:nowrap;padding-right:var(--space-3);max-width:42%;overflow:hidden;text-overflow:ellipsis;}
.ds-chart-sub{color:var(--color-text-subtle);font-weight:var(--weight-regular);margin-left:var(--space-2);
  font-size:var(--font-size-caption);}
.ds-chart-bar-cell{width:100%;padding:0 var(--space-3);}
.ds-chart-bar{display:block;width:100%;height:10px;}
.ds-chart-value{text-align:right;white-space:nowrap;color:var(--color-heading);
  font-weight:var(--weight-semibold);}
.ds-chart-compact th{padding-right:var(--space-3);}
.ds-chart-empty{padding:var(--space-5);text-align:center;color:var(--color-text-muted);
  font-size:var(--font-size-body);}
.ds-bar{fill:var(--module-time);}
.ds-bar--time{fill:var(--module-time);}
.ds-bar--flm{fill:var(--module-flm);}
.ds-bar--projects{fill:var(--module-projects);}
.ds-bar--planning{fill:var(--module-planning);}
.ds-bar--status-draft{fill:var(--status-draft-text);}
.ds-bar--status-submitted{fill:var(--status-submitted-text);}
.ds-bar--status-changes_requested{fill:var(--status-changes-text);}
.ds-bar--status-approved{fill:var(--status-approved-text);}
.ds-dot{display:inline-block;width:8px;height:8px;border-radius:var(--radius-pill);
  margin-right:var(--space-2);vertical-align:middle;}
.ds-dot--draft{background:var(--status-draft-text);}
.ds-dot--submitted{background:var(--status-submitted-text);}
.ds-dot--changes_requested{background:var(--status-changes-text);}
.ds-dot--approved{background:var(--status-approved-text);}
.ds-columns{margin-bottom:var(--space-3);}
.ds-columns-svg{display:block;width:100%;height:120px;}
.ds-columns-axis{display:flex;margin-top:var(--space-1);}
.ds-col-label{flex:1;text-align:center;font-size:var(--font-size-caption);color:var(--color-text-muted);}

/* Filter bar / period / tabs / totals */
.ds-filter-bar{margin-bottom:var(--space-4);}
.ds-filter-actions{display:flex;gap:var(--space-2);align-items:flex-end;flex-wrap:wrap;}
.ds-tabs{display:inline-flex;gap:var(--space-1);padding:var(--space-1);
  background:var(--color-surface-sunken);border-radius:var(--radius-md);margin-bottom:var(--space-4);}
.ds-tab{appearance:none;border:none;background:none;cursor:pointer;
  padding:var(--space-2) var(--space-4);min-height:var(--control-height-sm);
  font-size:var(--font-size-small);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);border-radius:var(--radius-sm);
  transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);}
.ds-tab:hover{color:var(--color-text);}
.ds-tab[aria-selected="true"]{background:var(--color-surface);color:var(--color-heading);box-shadow:var(--shadow-xs);}
.ds-tab:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
.ds-totals{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));
  gap:var(--gap-grid);margin-bottom:var(--space-4);}
.ds-total{border:var(--border-width) solid var(--color-border);border-radius:var(--radius-md);
  padding:var(--space-3) var(--space-4);background:var(--color-surface);}
.ds-total-value{font-size:var(--font-size-h2);font-weight:var(--weight-bold);
  color:var(--color-heading);font-variant-numeric:tabular-nums;}
.ds-total-label{font-size:var(--font-size-caption);color:var(--color-text-muted);margin-top:2px;}
.ds-total--warn .ds-total-value{color:var(--module-warn);}
.ds-typechips{display:flex;flex-wrap:wrap;gap:var(--space-2);margin:calc(-1 * var(--space-2)) 0 var(--space-4);}
.ds-typechip{font-size:var(--font-size-small);color:var(--color-text-muted);
  background:var(--color-surface-sunken);border-radius:var(--radius-pill);
  padding:var(--space-1) var(--space-3);}
.ds-typechip strong{color:var(--color-heading);}

/* Activity feed + item accents + compact tiles */
.ds-activity-row{display:flex;justify-content:space-between;gap:var(--space-3);
  padding:var(--space-2) 0;border-bottom:var(--border-width) solid var(--color-border-subtle);
  font-size:var(--font-size-small);}
.ds-activity-row:last-child{border-bottom:none;}
.ds-activity-meta{color:var(--color-text-muted);white-space:nowrap;}
.ds-item-icon--warn{background:var(--module-warn-bg);color:var(--module-warn);}
.ds-tile-grid--compact{grid-template-columns:repeat(auto-fit,minmax(200px,1fr));max-width:640px;}

/* ═══ A.5-1 — Platform-wide UI consistency ═════════════════════════════════
   Two parts:
   (a) the app shell (nav chrome) moves off hardcoded hex onto tokens;
   (b) a legacy → Design-System bridge: pages whose markup is not migrated yet
       (job-form, job-detail, profile, site-photos, login) keep their classes
       but render with design-system metrics, so admin and Field Engineer see
       one visual language platform-wide. A.5-2 replaces the markup itself;
       these rules can then be deleted without touching any page.
   components.css loads after app.css, so equal-specificity rules win here. */

/* ─── App shell / navigation ─────────────────────────────────────────────── */
.app-nav-item{font-size:var(--font-size-body);color:var(--color-nav-text);
  transition:color var(--duration-fast) var(--ease-out);}
.app-nav-item:hover,.app-nav-item.active{color:var(--color-nav-text-active);}
.app-nav-item:focus-visible{outline:none;box-shadow:var(--shadow-focus);border-radius:var(--radius-sm);}
.app-nav-org{font-size:var(--font-size-caption);color:var(--color-nav-text);}
.account-trigger{color:var(--color-nav-text);min-height:var(--touch-target);}
.account-trigger:hover{color:var(--color-nav-text-active);}
.account-trigger:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
@media(max-width:767px){
  /* Readable bottom-nav labels (was 10px) */
  .app-bottom-nav a{font-size:var(--font-size-caption);color:var(--color-nav-text);}
  .app-bottom-nav a.active{color:var(--color-nav-text-active);}
  .app-bottom-nav a:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
}

/* ─── Global form-control baseline ──────────────────────────────────────────
   Not a compatibility bridge any more: the legacy page-level classes are gone
   from the codebase entirely (Sprint 1A). What is left here is deliberately
   global — a token-driven baseline for native controls wherever they appear,
   including outside a .ds-form scope — plus the two structural classes the
   FLM job form still shares with app.css (.part-block / .note-block). */

label{font-size:var(--font-size-small);font-weight:var(--weight-semibold);
  color:var(--color-heading);margin-bottom:var(--space-1);}
input[type=text],input[type=email],input[type=datetime-local],
input[type=date],input[type=time],input[type=number],input[type=tel],
input[type=search],input[type=password],select,textarea{
  font-size:var(--font-size-body-lg);min-height:var(--touch-target);
  padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
  border-width:var(--border-width-strong);}
/* keep room for the select chevron after the padding reset above */
select{padding-right:var(--space-6);background-position:right var(--space-3) center;}

/* Structural leftovers still used by migrated markup */
.part-block{background:var(--color-surface-sunken);
  border-color:var(--color-border-subtle);border-radius:var(--radius-md);}
.part-remove,.note-remove{min-width:var(--touch-target);min-height:var(--touch-target);
  border-radius:var(--radius-md);}
.part-remove:focus-visible,.note-remove:focus-visible{outline:none;box-shadow:var(--shadow-focus);}

/* ─── Detail sections (FLM job detail) ───────────────────────────────────── */
.ds-section-empty{text-align:left;max-width:none;color:var(--color-text-muted);}
.ds-subcard{background:var(--color-surface-sunken);
  border:var(--border-width) solid var(--color-border-subtle);
  border-radius:var(--radius-md);padding:var(--space-4);margin-bottom:var(--space-3);}
.ds-subcard:last-child{margin-bottom:0;}
.ds-subcard-title{font-size:var(--font-size-small);font-weight:var(--weight-bold);
  color:var(--color-heading);margin:0 0 var(--space-3);}
.ds-detail-grid{display:grid;grid-template-columns:1fr;gap:var(--space-3);}
@media(min-width:600px){.ds-detail-grid{grid-template-columns:1fr 1fr;}}
.ds-detail-cell{display:flex;flex-direction:column;gap:2px;min-width:0;}
.ds-detail-label{font-size:var(--font-size-caption);color:var(--color-text-muted);}
.ds-detail-value{font-size:var(--font-size-body);color:var(--color-text);
  word-break:break-word;}
.ds-detail-fault{margin-top:var(--space-3);}

/* ─── Collapsible form sections (FLM job form) ───────────────────────────── */
.ds-form-section{padding:0;}
@media(min-width:768px){.ds-form-section{padding:0;}}
.ds-section-toggle{display:flex;width:100%;align-items:center;justify-content:space-between;
  gap:var(--space-3);padding:var(--space-4);background:none;border:none;cursor:pointer;
  text-align:left;min-height:var(--touch-target);border-radius:var(--radius-lg);}
.ds-section-toggle:hover{background:var(--color-surface-hover);}
.ds-section-toggle:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
.ds-section-toggle .ds-card-title{margin:0;padding:0;border-bottom:none;}
.ds-section-chevron{display:inline-flex;color:var(--color-text-muted);
  transition:transform var(--duration-base) var(--ease-out);}
.ds-section-chevron.is-collapsed{transform:rotate(-90deg);}
.ds-form-section .form-section-body{padding:0 var(--space-4) var(--space-4);}

/* ─── Repeater rows (parts / notes / consumables) ────────────────────────── */
.ds-repeater-row{display:flex;flex-direction:column;gap:var(--space-3);
  padding:var(--space-3) 0;border-bottom:var(--border-width) solid var(--color-border-subtle);}
.ds-repeater-row:last-child{border-bottom:none;}
.ds-repeater-row .ds-field{margin-bottom:0;}
@media(min-width:600px){
  .ds-repeater-row{flex-direction:row;align-items:flex-end;}
  .ds-grow-2{flex:2;min-width:0;} .ds-grow-1{flex:1;min-width:0;}
  .note-time-field{width:200px;flex-shrink:0;} .note-comment-field{flex:1;min-width:0;}
}
.ds-repeater-add{margin-top:var(--space-3);}

/* ─── Sticky form actions ────────────────────────────────────────────────── */
.ds-form-actions--sticky{position:sticky;bottom:0;z-index:var(--z-sticky);
  background:var(--color-background);
  padding-bottom:var(--space-4);margin-top:var(--space-5);
  border-top:var(--border-width) solid var(--color-border);}
@media(max-width:767px){
  /* clear the mobile bottom navigation */
  .ds-form-actions--sticky{bottom:calc(64px + env(safe-area-inset-bottom));}
}

/* ─── Site photographs (module) ──────────────────────────────────────────── */
.ds-photo-grid{display:grid;grid-template-columns:1fr;gap:var(--space-3);}
@media(min-width:768px){.ds-photo-grid{grid-template-columns:1fr 1fr;}}
.ds-photo-card{margin-bottom:0;}
.ds-photo-empty{display:flex;flex-direction:column;align-items:center;gap:var(--space-3);
  padding:var(--space-5) var(--space-3);text-align:center;
  border:var(--border-width-strong) dashed var(--color-border);
  border-radius:var(--radius-md);background:var(--color-surface);}
.ds-photo-empty .ds-btn{width:100%;}
.ds-photo-comment{margin-top:var(--space-3);margin-bottom:0;}
.ds-hint--top{margin-top:0;margin-bottom:var(--space-4);}

/* ─── Identity card (profile) ────────────────────────────────────────────── */
.ds-identity{display:flex;align-items:center;gap:var(--space-4);}
.ds-identity-main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
.ds-identity-name{font-size:var(--font-size-h3);font-weight:var(--weight-bold);
  color:var(--color-heading);}
.ds-identity-meta{font-size:var(--font-size-small);color:var(--color-text-muted);
  word-break:break-word;}
.ds-identity-tags{display:flex;gap:var(--space-2);flex-wrap:wrap;margin-top:var(--space-1);}
.ds-hint{font-size:var(--font-size-small);color:var(--color-text-muted);
  margin-top:var(--space-3);}
.ds-security-note{text-align:left;max-width:none;}

/* ─── Auth / login ───────────────────────────────────────────────────────── */
.auth-card{padding:var(--space-5);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);}
.auth-card--centered{text-align:center;}
.auth-title{font-size:var(--font-size-h2);font-weight:var(--weight-bold);
  color:var(--color-heading);margin-bottom:var(--space-1);}
.auth-subtitle{font-size:var(--font-size-body);color:var(--color-text-muted);
  margin-bottom:var(--space-5);}
.auth-verify-alert{margin-top:var(--space-4);text-align:left;}
.ds-btn-block{width:100%;margin-top:var(--space-2);}

/* ─── Modal: one dialog surface platform-wide ────────────────────────────── */
.modal-overlay{z-index:var(--z-overlay);padding:var(--space-4);}
.modal{border-radius:var(--radius-lg);padding:var(--space-5);
  max-width:var(--container-modal);box-shadow:var(--shadow-lg);}
.modal-title{font-size:var(--font-size-h3);font-weight:var(--weight-bold);
  color:var(--color-heading);margin-bottom:var(--space-2);}
.modal-body{font-size:var(--font-size-body);color:var(--color-text-muted);
  margin-bottom:var(--space-4);line-height:var(--leading-normal);}
.modal-actions{gap:var(--space-2);}

/* ─── Global Search palette (GS-1, design system §2.18 "Command Palette") ────
   A DELIBERATE size variant, never a redefinition of the platform-wide
   .modal above: every existing ui.showModal() caller stays at
   --container-modal (440px) untouched. Reuses .modal-overlay's own backdrop
   (same class, applied alongside this one) — only the vertical anchor
   differs (near the top, the near-universal command-palette convention,
   instead of dead-centre) and the surface itself is wider. Built on
   ui.wireModal() — no second focus-trap/dialog implementation. */
.search-palette-overlay{align-items:flex-start;padding-top:min(15vh,120px);}
.search-palette{
  background:var(--modal-bg);border-radius:var(--modal-radius);box-shadow:var(--modal-shadow);
  width:100%;max-width:var(--container-search);padding:0;overflow:hidden;
}
.search-palette-field{
  display:flex;align-items:center;gap:var(--space-3);
  padding:var(--space-4);border-bottom:1px solid var(--color-border-subtle);
}
.search-palette-field-icon{display:flex;flex-shrink:0;color:var(--color-text-muted);}
/* `input.search-palette-input` (not just `.search-palette-input`) deliberately
   matches app.css's own `input[type=text]{background:white;...}` specificity
   (0,1,1) so this rule's later position in the cascade actually wins — a
   bare class selector alone (0,1,0) would silently lose to that pre-existing,
   platform-wide rule and leave the field with a hardcoded white background,
   wrong in dark mode. */
input.search-palette-input{
  width:auto;flex:1;border:none;background:none;outline:none;box-shadow:none;
  font-size:var(--font-size-h3);color:var(--color-heading);
  -webkit-appearance:none;appearance:none;
}
input.search-palette-input:focus{outline:none;border:none;box-shadow:none;}
.search-palette-input::placeholder{color:var(--color-text-muted);}
.search-palette-body{
  font-size:var(--font-size-body);color:var(--color-text-muted);
}
/* GS-2 — hint / loading / no-results / error line (the GS-1 empty-state
   spacing, now on the status line so a result list can sit flush). */
.search-palette-status{padding:var(--space-6) var(--space-4);text-align:center;}
.search-palette-status--error{color:var(--color-error);}
/* GS-2 — grouped results listbox. Bounded height so the palette never
   outgrows a short/mobile viewport; the list scrolls, the field stays put. */
.search-palette-results{
  max-height:min(60vh,420px);overflow-y:auto;padding:var(--space-2) 0;
}
.search-palette-group + .search-palette-group{border-top:1px solid var(--color-border-subtle);}
.search-palette-group-title{
  padding:var(--space-3) var(--space-4) var(--space-1);
  font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em;
}
.search-palette-option{
  display:flex;flex-direction:column;gap:2px;min-width:0;
  padding:var(--space-2) var(--space-4);cursor:pointer;
}
.search-palette-option:hover{background:var(--color-nav-hover-bg);}
.search-palette-option.is-active{background:var(--color-primary-muted);}
.search-palette-option-title{
  color:var(--color-heading);font-weight:var(--weight-semibold);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.search-palette-option-subtitle{
  font-size:var(--font-size-small);color:var(--color-text-muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}

/* ─── Functional panel (ORIX DS v2) ──────────────────────────────────────────
   A tinted surface whose hue encodes the block's FUNCTION, not its brand:
   planning, hours, warnings, activity, reports. Colour carries meaning, so a
   panel always also carries a text title — never colour alone.
   Driven entirely by the layer-3 --panel-* tokens, so retinting a panel means
   changing a token, never this rule. Both themes come from the token remap. */
.ds-panel{
  background:var(--panel-activity-bg);
  border:var(--border-width) solid var(--panel-border);
  border-radius:var(--panel-radius);
  padding:var(--panel-padding);
  box-shadow:var(--panel-shadow);
}
.ds-panel-title{
  font-family:var(--font-display);
  font-size:var(--font-size-h3);font-weight:var(--weight-semibold);
  line-height:var(--leading-normal);color:var(--color-heading);margin:0;
}
.ds-panel-sub{font-size:var(--font-size-small);color:var(--color-text-muted);margin-top:var(--space-1);}
.ds-panel-planning{background:var(--panel-planning-bg);}
.ds-panel-hours   {background:var(--panel-hours-bg);}
.ds-panel-warn    {background:var(--panel-warn-bg);}
.ds-panel-activity{background:var(--panel-activity-bg);}
.ds-panel-reports {background:var(--panel-reports-bg);}
/* Panels sit above the list they summarise. */
.ds-panel + *{margin-top:var(--space-4);}

/* ─── Engineer Today (ORIX DS v2) ────────────────────────────────────────────
   The daily cockpit. Mobile-first: the hero and the primary action sit above
   the fold, and the layout simply widens on larger screens — one UX, no second
   design. All colour comes from tokens. */
.td-header{margin-bottom:var(--space-5);}
.td-hero{padding:var(--space-5);}
.td-eyebrow{
  font-size:var(--font-size-caption);font-weight:var(--weight-bold);
  letter-spacing:var(--tracking-caption);text-transform:uppercase;
  color:var(--module-planning);margin:0 0 var(--space-1);
}
.td-hero-title{font-size:var(--font-size-h1);line-height:var(--leading-tight);}
.td-kv{margin-top:var(--space-4);}
.td-kv .td-strong{font-weight:var(--weight-bold);color:var(--color-heading);
  font-variant-numeric:tabular-nums;}
.td-note{margin-top:var(--space-3);}
.td-actions{display:flex;gap:var(--space-3);flex-wrap:wrap;margin:var(--space-5) 0;}
.td-actions .ds-btn{flex:1 1 auto;justify-content:center;}
.td-actions-center{justify-content:center;}
.td-section{margin-top:var(--space-6);}
.td-notices{display:flex;flex-direction:column;gap:var(--space-2);
  margin-bottom:var(--space-4);}
@media(min-width:768px){
  .td-actions .ds-btn{flex:0 0 auto;}
  .td-hero{padding:var(--space-6);}
}

/* Row of actions at the foot of a card (e.g. edit an assignment). */
.ds-card-actions{
  display:flex;gap:var(--space-2);flex-wrap:wrap;
  margin-top:var(--space-3);
}

/* ─── Drawer (ORIX DS v2) ────────────────────────────────────────────────────
   A side panel for create/edit without losing the list behind it. Shares the
   modal's focus contract (ui.wireModal): focus enters on open, Tab is trapped,
   Escape dismisses unless the caller refuses, and focus returns to the trigger.
   Desktop: right-hand panel. Mobile: bottom sheet, so the primary actions stay
   in thumb reach. Driven by the layer-3 --drawer-* / --overlay-* tokens. */
.ds-drawer-overlay{
  position:fixed;inset:0;z-index:var(--z-modal);
  background:var(--overlay-bg);
  display:flex;justify-content:flex-end;align-items:stretch;
}
.ds-drawer{
  background:var(--drawer-bg);
  width:min(var(--drawer-width),100%);
  display:flex;flex-direction:column;
  box-shadow:var(--drawer-shadow);
  max-height:100%;
  animation:ds-drawer-in var(--duration-drawer) var(--ease-out);
}
@keyframes ds-drawer-in{from{transform:translateX(16px);opacity:0;}to{transform:none;opacity:1;}}
.ds-drawer-header{
  display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3);
  padding:var(--space-5);border-bottom:var(--border-width) solid var(--color-border-subtle);
}
.ds-drawer-title{
  font-family:var(--font-display);font-size:var(--font-size-h2);
  font-weight:var(--weight-bold);color:var(--color-heading);margin:0;
}
.ds-drawer-sub{font-size:var(--font-size-small);color:var(--color-text-muted);margin-top:var(--space-1);}
.ds-drawer-body{padding:var(--space-5);overflow-y:auto;flex:1;}
.ds-drawer-actions{
  display:flex;gap:var(--space-2);justify-content:flex-end;flex-wrap:wrap;
  padding:var(--space-4) var(--space-5);
  border-top:var(--border-width) solid var(--color-border-subtle);
  background:var(--color-surface);
  padding-bottom:calc(var(--space-4) + env(safe-area-inset-bottom,0px));
}
@media(max-width:767px){
  .ds-drawer-overlay{align-items:flex-end;justify-content:stretch;}
  .ds-drawer{
    width:100%;max-height:92vh;
    border-radius:var(--radius-lg) var(--radius-lg) 0 0;
    animation:ds-sheet-in var(--duration-drawer) var(--ease-out);
  }
  @keyframes ds-sheet-in{from{transform:translateY(24px);opacity:0;}to{transform:none;opacity:1;}}
  .ds-drawer-actions .ds-btn{flex:1;}
}
@media (prefers-reduced-motion: reduce){ .ds-drawer{animation:none;} }

/* ─── Reduced motion ────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce){
  .ds-btn,.ds-btn-icon,.ds-card-link,.ds-item,.ds-tile,.ds-stat-link,
  .ds-toast,.ds-table tbody tr{transition:none;}
  .ds-skeleton::after{animation:none;}
  .ds-btn.is-loading::after,.spinner{animation-duration:1.5s;}
}

/* ═══════════════════════════════════════════════════════════════════════════
   DASHBOARD GRID — one system, three widths (Sprint 1B)
   ─────────────────────────────────────────────────────────────────────────
   Twelve columns on desktop, six on tablet, one on a phone. There is no
   "mobile dashboard": the same cards, in the same order, from the same
   registry — only the spans change. A card that is not registered, or whose
   show() returns false, is never in the DOM at all, so the grid closes over
   its absence on its own. That is the whole reason the spans are declared per
   size class instead of per card: turning a module off must not leave a hole,
   and turning one on must not need a layout edit.
   ═══════════════════════════════════════════════════════════════════════ */
.ds-dash{display:flex;flex-direction:column;gap:var(--gap-grid);}

/* The KPI band: as many across as fit, never fewer than a readable width. */
.ds-dash-kpis{display:grid;gap:var(--gap-grid);
  grid-template-columns:repeat(auto-fit,minmax(200px,1fr));}

/* Main column + right rail. The rail is a region, not a leftover: a card lands
   there because it declared size:'rail', never because the row happened to
   have space. When no card claims the rail the region is not rendered at all
   and the main column takes the full width — turning a module off must not
   leave a gutter. */
.ds-dash-cols{display:grid;gap:var(--gap-grid);
  grid-template-columns:2fr 1fr;align-items:start;}
.ds-dash-cols--norail{grid-template-columns:1fr;}
.ds-dash-main,.ds-dash-rail,.ds-dash-full{display:flex;flex-direction:column;
  gap:var(--gap-grid);min-width:0;}

/* Two half-width cards share a row inside the main column when there is room. */
.ds-dash-main{display:grid;gap:var(--gap-grid);grid-template-columns:1fr 1fr;
  align-content:start;}
.ds-dash-card--wide,.ds-dash-card--full{grid-column:1 / -1;}
.ds-dash-card--half{grid-column:span 1;}
.ds-dash-card{min-width:0;}
/* The card's own margin would fight the gap; the layout owns the rhythm. */
.ds-dash-card > .ds-card,.ds-dash-card > .ds-kpi,
.ds-dash-card > .ds-kpi-grid{margin-bottom:0;height:100%;}

/* Dashboard V2 Phase 1: aligned to the shell's own desktop breakpoint
   (app.css's sidebar rule, min-width:1024px) instead of the dashboard's
   previous, independent 1100px ceiling — removing the 1024-1099px hybrid
   state where the sidebar was already in its desktop position but the
   dashboard body still rendered its stacked/tablet layout underneath it. */
@media(max-width:1023px){
  .ds-dash-cols{grid-template-columns:1fr;}     /* rail drops below main */
}
@media(max-width:767px){
  .ds-dash{gap:var(--space-3);}
  .ds-dash-kpis{grid-template-columns:1fr 1fr;gap:var(--space-3);}
  .ds-dash-main{grid-template-columns:1fr;gap:var(--space-3);}
  .ds-dash-cols,.ds-dash-rail,.ds-dash-full{gap:var(--space-3);}
  .ds-dash-card--half{grid-column:1 / -1;}
}

/* ═══════════════════════════════════════════════════════════════════════════
   ORIX Field Engineer Dashboard Foundation (D2) — engineer-only refinements
   ─────────────────────────────────────────────────────────────────────────
   Every selector below is scoped under .ds-dash--engineer, a class
   pages/dashboard.js adds to #dash-grid only for a non-admin user (identical
   role test to isEngineer() in dashboard-cards.js). The admin dashboard
   renders through the exact same .ds-dash-kpis/-cols/-main/-rail/-full
   scaffolding above and never carries that class, so nothing here can ever
   match it — the base rules above this comment are the admin dashboard's
   only styling, unchanged from before D2.
   ═══════════════════════════════════════════════════════════════════════ */

/* Desktop (≥1024px — Dashboard V2 Phase 1: realigned to the same ceiling
   the rail-drop rule above and the shell's own sidebar breakpoint both use,
   was 1100px): handoff gap (20-24px) and column ratio (1.55fr/1fr),
   engineer only. --gap-grid's own value is untouched; .ds-tile-grid/
   .ds-totals elsewhere keep reading it exactly as before. */
@media(min-width:1024px){
  .ds-dash--engineer.ds-dash{gap:var(--space-5);}
  .ds-dash--engineer .ds-dash-kpis{gap:var(--space-5);}
  .ds-dash--engineer .ds-dash-cols{gap:var(--space-5);grid-template-columns:1.55fr 1fr;}
  .ds-dash--engineer .ds-dash-main,
  .ds-dash--engineer .ds-dash-rail,
  .ds-dash--engineer .ds-dash-full{gap:var(--space-5);}
}

/* Tablet (768-1023px, handoff: "KPI 2 kolommen") — the base rule above only
   forces main/rail to one column at this width; the KPI band itself still
   falls back to auto-fit. This tier did not exist before D2, so it only
   ever exists engineer-scoped. */
@media(min-width:768px) and (max-width:1023px){
  .ds-dash--engineer.ds-dash{gap:var(--space-4);}
  .ds-dash--engineer .ds-dash-kpis{grid-template-columns:1fr 1fr;gap:var(--space-4);}
  .ds-dash--engineer .ds-dash-cols,
  .ds-dash--engineer .ds-dash-main,
  .ds-dash--engineer .ds-dash-rail,
  .ds-dash--engineer .ds-dash-full{gap:var(--space-4);}
}

/* Mobile primary CTA — a second rendering of the same 'uren/new' action
   already offered inside Quick Actions, registered as its own card
   (eng-register-cta, engineer-gated) purely so it can be positioned
   independently of the rest of that card. It only ever exists in an
   engineer's DOM, so no role class is needed here — only a breakpoint gate:
   hidden above 767px, where Quick Actions already satisfies the handoff in
   its approved column position. */
.ds-dash-mobile-cta{display:none;}
@media(max-width:767px){
  .ds-dash-mobile-cta{display:flex;}
}

/* Mobile cross-region order (approved): Uren registreren -> KPI's -> Planning
   vandaag -> overige dashboardonderdelen. .ds-dash is already a flex column,
   so reordering is plain `order` — no DOM change needed.

   Both regions this reaches into (.ds-dash-full and .ds-dash-rail) can hold
   more than the one card each we actually mean to move: .ds-dash-full also
   carries the Operations module's own "upcoming week" strip (out of scope
   for D2 — it must stay inside "overige", never jump the queue with the
   CTA), and the rail's other cards must stay put too. So neither region is
   reordered as a whole; each is dissolved (display:contents), promoting its
   individual cards to direct, orderable siblings one level up, and only the
   two specific cards this order actually names are pulled forward by id —
   eng-register-cta (this file's own card) and the Operations module's
   "today" card (id ops-today, referenced here only as a CSS attribute
   selector — this file still has no knowledge of what that card contains,
   the same boundary the platform/module split already relies on elsewhere).
   Every other card keeps its natural (unset -> 0) order and its natural
   document position, so "overige dashboardonderdelen" — including the
   out-of-scope week strip — stays exactly where it already was. */
@media(max-width:767px){
  .ds-dash--engineer .ds-dash-kpis{order:-1;}
  .ds-dash--engineer .ds-dash-full{display:contents;}
  .ds-dash--engineer [data-card-id="eng-register-cta"]{order:-2;}
  .ds-dash--engineer .ds-dash-main,
  .ds-dash--engineer .ds-dash-rail{display:contents;}
  .ds-dash--engineer [data-card-id="ops-today"]{order:-1;}
}

/* ═══════════════════════════════════════════════════════════════════════════
   ORIX Operations Administrator Dashboard Foundation (D3) — admin-only
   refinements
   ─────────────────────────────────────────────────────────────────────────
   Every selector below is scoped under .ds-dash--admin, a class
   pages/dashboard.js adds to #dash-grid only for the admin role (identical
   role test to isAdmin() in dashboard-cards.js), mutually exclusive with
   .ds-dash--engineer above. Mirrors that block's own mechanism exactly, but
   with admin's own values from the approved handoff — most notably a
   different column ratio (1.6fr/1fr, not engineer's 1.55fr/1fr) — never by
   editing the shared base rules or the engineer block above. */

/* Desktop (≥1024px — Dashboard V2 Phase 1: realigned to the same ceiling
   the rail-drop rule above and the shell's own sidebar breakpoint both use,
   was 1100px): handoff gap (20-24px) and the approved admin column ratio
   (1.6fr/1fr). --gap-grid's own value is untouched, same as for engineer. */
@media(min-width:1024px){
  .ds-dash--admin.ds-dash{gap:var(--space-5);}
  .ds-dash--admin .ds-dash-kpis{gap:var(--space-5);}
  .ds-dash--admin .ds-dash-cols{gap:var(--space-5);grid-template-columns:1.6fr 1fr;}
  .ds-dash--admin .ds-dash-main,
  .ds-dash--admin .ds-dash-rail,
  .ds-dash--admin .ds-dash-full{gap:var(--space-5);}
}

/* Tablet (768-1023px) — the base rule above only forces main/rail to one
   column at this width; the KPI band itself still falls back to auto-fit.
   This tier did not exist for admin before D3, so it only ever exists
   admin-scoped. No admin-specific mobile tier: the shared base mobile rule
   (≤767px, above) already forces a controlled 2-column KPI band and a
   single-column body for every role, and no admin-specific mobile frame
   exists in the approved handoff to diverge from that. */
@media(min-width:768px) and (max-width:1023px){
  .ds-dash--admin.ds-dash{gap:var(--space-4);}
  .ds-dash--admin .ds-dash-kpis{grid-template-columns:1fr 1fr;gap:var(--space-4);}
  .ds-dash--admin .ds-dash-cols,
  .ds-dash--admin .ds-dash-main,
  .ds-dash--admin .ds-dash-rail,
  .ds-dash--admin .ds-dash-full{gap:var(--space-4);}
}

/* A card that failed to load says so in its own footprint and leaves the rest
   of the dashboard standing. */
.ds-dash-error{padding:var(--space-4);border-radius:var(--card-radius);
  border:var(--border-width) dashed var(--color-border);
  color:var(--color-text-muted);font-size:var(--font-size-small);}

/* ─── Dashboard hero ────────────────────────────────────────────────────────
   The greeting is the largest thing on the page; the date is its quiet anchor;
   the week number is set apart in a pill because week is the unit a field
   operation actually plans in, and burying it in a sentence hides it. */
.ds-dash-hero{margin-bottom:var(--space-6);}
.ds-dash-greeting{font-size:var(--font-size-display);letter-spacing:var(--tracking-tight);}
.ds-dash-datebar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;}
.ds-dash-week{display:inline-flex;align-items:center;
  padding:2px var(--space-2);border-radius:var(--radius-pill);
  background:var(--color-primary-muted);color:var(--color-primary);
  font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  font-variant-numeric:tabular-nums;}
@media(max-width:767px){
  .ds-dash-greeting{font-size:var(--font-size-h1);}
  .ds-dash-hero{margin-bottom:var(--space-5);}
}

/* ─── KPI anatomy (Sprint 1B finalization) ──────────────────────────────────
   Reference order, top to bottom: what it measures, the figure, then what the
   figure means. The context line is the point — "0 h" alone tells an engineer
   nothing, while "0 h · nothing logged yet" tells them whether that is a
   problem. A trend is stated in words as well as colour, because a green
   number is not information to anyone who cannot see green. */
.ds-kpi-eyebrow{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);letter-spacing:var(--tracking-caption);
  text-transform:uppercase;}
.ds-kpi .ds-kpi-value{margin-top:var(--space-1);}
.ds-kpi-context{font-size:var(--font-size-caption);color:var(--color-text-muted);
  margin-top:var(--space-1);display:flex;align-items:center;gap:var(--space-1);
  flex-wrap:wrap;}
.ds-kpi-trend{display:inline-flex;align-items:center;gap:2px;
  font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;}
.ds-kpi-trend--up  {color:var(--color-success);}
.ds-kpi-trend--down{color:var(--color-danger);}
.ds-kpi-trend--flat{color:var(--color-text-subtle);}

/* ─── In-card empty state ───────────────────────────────────────────────────
   The page-level .ds-empty is built for a whole screen and reads as a hole
   when dropped inside a card. This is the quiet version: one small glyph, one
   sentence, centred in the space the content would have filled. */
.ds-card-empty{display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:var(--space-2);text-align:center;
  padding:var(--space-6) var(--space-4);min-height:120px;}
.ds-card-empty-icon{display:inline-flex;align-items:center;justify-content:center;
  width:36px;height:36px;border-radius:var(--radius-pill);
  background:var(--color-surface-sunken);color:var(--color-text-subtle);}
.ds-card-empty-text{font-size:var(--font-size-small);color:var(--color-text-muted);
  max-width:28em;margin:0;}
/* Charts render their own empty slot; give it the same quiet treatment. */
.ds-chart-empty{display:flex;align-items:center;justify-content:center;
  padding:var(--space-6) var(--space-4);min-height:120px;
  font-size:var(--font-size-small);color:var(--color-text-muted);text-align:center;}

/* ─── On Duty Now (Dashboard V2 Phase 2) ─────────────────────────────────────
   Step 5: charts.ring() draws the number's visual anchor as a native SVG
   donut — geometry (stroke-dasharray/-dashoffset) in SVG attributes, colour
   only via the ds-onduty-accent--* classes below, never an inline style,
   same discipline as every other chart in charts.js.

   Step 6: exactly one entry transition, on .ds-ring itself — opacity/scale
   only, never touching the segment geometry above. --duration-slow is the
   existing DS token this reveal deliberately reuses (no new duration token
   for Step 6). The .ds-ring--enter class is added by charts.ring({enter})
   only when there is a genuine non-zero ring to reveal, and removed by
   operations.js one frame after mount — after that the class never comes
   back, so this can never become a loop or a live/continuous animation.
   No @keyframes anywhere in this block; a plain property transition is
   enough for a single one-shot reveal. */
.ds-onduty-total{position:relative;display:flex;align-items:center;justify-content:center;
  margin:var(--space-4) 0 var(--space-2);}
.ds-ring{display:block;width:88px;height:88px;
  transition:opacity var(--duration-slow) var(--ease-out), transform var(--duration-slow) var(--ease-out);}
.ds-ring--enter{opacity:0;transform:scale(0.92);}
/* Independently final-state safe: even if .ds-ring--enter is present for any
   reason (a stale render, a future caller mistake), reduced motion still
   forces the ring straight to its final visible state — not just "no
   transition", but the actual end values too. */
@media(prefers-reduced-motion:reduce){
  .ds-ring{transition:none;}
  .ds-ring--enter{opacity:1;transform:none;}
}
.ds-ring-track{stroke:var(--color-border);}
.ds-ring-segment{fill:none;stroke:currentColor;}
.ds-onduty-count{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  font-size:var(--font-size-h2);font-weight:var(--weight-semibold);
  color:var(--color-heading);font-variant-numeric:tabular-nums;}
.ds-onduty-hint{margin:0 0 var(--space-3);text-align:center;
  font-size:var(--font-size-small);color:var(--color-text-muted);}
.ds-onduty-legend{list-style:none;margin:0;padding:0;
  display:flex;flex-direction:column;gap:var(--space-1);}
.ds-onduty-legend-item{display:flex;align-items:center;gap:var(--space-2);
  padding:var(--space-1) 0;color:var(--color-text);}
.ds-onduty-legend-icon{display:flex;flex-shrink:0;color:var(--color-text-subtle);}
.ds-onduty-legend-label{flex:1;font-size:var(--font-size-small);}
.ds-onduty-legend-value{font-size:var(--font-size-small);font-weight:var(--weight-semibold);
  font-variant-numeric:tabular-nums;color:var(--color-heading);}
.ds-onduty-note{margin:var(--space-3) 0 0;font-size:var(--font-size-caption);
  color:var(--color-text-subtle);}

/* One shared class per legend category ties the ring segment's `stroke`
   (via currentColor, see .ds-ring-segment above) and the legend icon's own
   `color` to the same existing semantic token — colour is drawn from
   pre-existing module-accent tokens (components.css/tokens.css), never a
   new one, and every one below is already relied on as a foreground colour
   elsewhere in this file (KPI icon/bar-fill/spark), so none of these six
   is a first, untested use. Project/FLM reuse their own platform module's
   existing token (an exact name match); Travel reuses --module-planning
   (a travel assignment is a planning/logistics concept); Standby reuses
   --module-hours (a time-based waiting state); Other reuses
   --module-assignments (the generic "assignment types with no module of
   their own" bucket this legend row exists for) rather than --module-admin,
   whose only defined value (--orix-m-admin, tokens.css) is a near-black
   navy with no dark-mode override — safe as a background tint, not as a
   foreground colour, so it was never a fit for an icon/ring segment.
   Multiple reuses --module-review: an engineer in two current assignments
   at once is exactly the kind of exception that needs admin attention, the
   same "review" meaning this token already carries elsewhere (adm-kpi-review
   etc.). --module-warn was considered first (ops-conflicts already uses it
   for the same "double-booking" concept) but its only defined value is
   literally identical to --module-flm's (both var(--orix-amber-deep) in
   light mode, tokens.css) — indistinguishable in a six-colour ring, so
   --module-review was chosen instead to keep every segment visually
   distinct. Placed after .ds-onduty-legend-icon so these override its
   default muted colour. */
.ds-onduty-accent--project{color:var(--module-projects);}
.ds-onduty-accent--flm{color:var(--module-flm);}
.ds-onduty-accent--travel{color:var(--module-planning);}
.ds-onduty-accent--standby{color:var(--module-hours);}
.ds-onduty-accent--other{color:var(--module-assignments);}
.ds-onduty-accent--multiple{color:var(--module-review);}

/* ─── Tile: chevron affordance ──────────────────────────────────────────────
   A quick action is a destination, so it says so. The chevron sits at the far
   end and slides a little on hover — enough to feel alive, not enough to
   distract someone scanning the list. */
.ds-tile{justify-content:flex-start;}
.ds-tile-main{flex:1;min-width:0;}
.ds-tile-chevron{flex-shrink:0;color:var(--color-text-subtle);display:inline-flex;
  transition:transform var(--duration-fast) var(--ease-out),
             color var(--duration-fast) var(--ease-out);}
.ds-tile:hover .ds-tile-chevron{transform:translateX(2px);color:var(--color-primary);}
@media(prefers-reduced-motion:reduce){
  .ds-tile:hover .ds-tile-chevron{transform:none;}
}

/* ─── Sparkline ─────────────────────────────────────────────────────────────
   Sits beside a KPI number: trend only, no axis, no labels. Colour comes from
   the same module-accent tokens the KPI card already uses, so the line never
   introduces a hue the design system has not already approved. */
.ds-spark{display:block;width:100%;height:28px;overflow:visible;}
.ds-spark-line{fill:none;stroke:currentColor;stroke-width:1.75;
  stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke;}
.ds-spark-area{fill:currentColor;opacity:0.10;stroke:none;}
.ds-spark-dot{fill:currentColor;stroke:var(--color-surface);stroke-width:1.5;
  vector-effect:non-scaling-stroke;}
.ds-spark--time    {color:var(--module-time);}
.ds-spark--flm     {color:var(--module-flm);}
.ds-spark--projects{color:var(--module-projects);}
.ds-spark--review  {color:var(--module-review);}
.ds-spark--warn    {color:var(--module-warn);}
.ds-kpi-spark{margin-top:var(--space-2);}

/* ─── Interactive line chart (M6) ─────────────────────────────────────────
   An analytical trend, not a decorative glance like the sparkline above: the
   Y axis is zero-based (charts.line() computes the geometry) and the SVG
   stays aria-hidden — accessible truth is the native button row beneath it,
   which doubles as the chart's x-axis labels, so there is no separate axis
   element. No entry motion: the full line exists the instant this markup
   mounts. Structure, static appearance, active-state appearance and
   :focus-visible all live here; the `transition:` declarations that animate
   the move between those states, and their reduced-motion override, live in
   motion.css (M6 section) instead — this block only ever sets the resting
   and active VALUES (opacity:0/1, r, colours), never how the change between
   them is timed. */
.ds-line{margin-bottom:var(--space-2);position:relative;}
.ds-line-svg{display:block;width:100%;height:100px;overflow:visible;}
.ds-line-baseline{stroke:var(--color-border);stroke-width:1;vector-effect:non-scaling-stroke;}
.ds-line-guide{stroke:var(--color-border);stroke-width:1;vector-effect:non-scaling-stroke;opacity:0;}
.ds-line-guide.is-active{opacity:1;}
.ds-line-path{fill:none;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;
  vector-effect:non-scaling-stroke;}
.ds-line-path--time    {stroke:var(--module-time);}
.ds-line-path--flm     {stroke:var(--module-flm);}
.ds-line-path--projects{stroke:var(--module-projects);}
.ds-line-path--review  {stroke:var(--module-review);}
.ds-line-path--planning{stroke:var(--module-planning);}
.ds-line-path--warn    {stroke:var(--module-warn);}
.ds-line-hit{fill:transparent;pointer-events:all;}
.ds-line-pt{stroke:var(--color-surface);stroke-width:1.5;vector-effect:non-scaling-stroke;pointer-events:none;}
.ds-line-pt--time    {fill:var(--module-time);}
.ds-line-pt--flm     {fill:var(--module-flm);}
.ds-line-pt--projects{fill:var(--module-projects);}
.ds-line-pt--review  {fill:var(--module-review);}
.ds-line-pt--planning{fill:var(--module-planning);}
.ds-line-pt--warn    {fill:var(--module-warn);}
.ds-line-pt.is-active{r:3.5;}
.ds-line-axis{display:flex;margin-top:var(--space-1);position:relative;height:20px;}
.ds-line-pt-btn{position:absolute;top:0;transform:translateX(-50%);
  border:none;background:none;padding:var(--space-1) var(--space-2);margin:0;
  font:inherit;font-size:var(--font-size-caption);color:var(--color-text-muted);
  cursor:pointer;border-radius:var(--radius-sm);}
.ds-line-pt-btn:hover,
.ds-line-pt-btn.is-active{color:var(--color-heading);background:var(--color-surface-hover);}
.ds-line-pt-btn:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
.ds-line-value{min-height:1.25em;margin-top:var(--space-2);text-align:center;
  font-size:var(--font-size-small);font-weight:var(--weight-medium);color:var(--color-heading);
  opacity:0;transform:translateY(calc(-1 * var(--space-1)));}
.ds-line-value:not(:empty){opacity:1;transform:translateY(0);}

/* ═══════════════════════════════════════════════════════════════════════════
   PLANNING UI LIBRARY — foundations (Sprint 2, increment 1)
   ─────────────────────────────────────────────────────────────────────────
   The swatch classes below are the ONLY thing the colour provider returns.
   Each one does exactly one job: set --pswatch. Components then style
   themselves from that property and never name a swatch, so replacing the
   provider later (a database column, a customer palette) needs no component
   change — and a wider palette needs only more classes here.
   ═══════════════════════════════════════════════════════════════════════ */
.ds-pswatch-a{--pswatch:var(--planning-swatch-a);}
.ds-pswatch-b{--pswatch:var(--planning-swatch-b);}
.ds-pswatch-c{--pswatch:var(--planning-swatch-c);}
.ds-pswatch-d{--pswatch:var(--planning-swatch-d);}
.ds-pswatch-e{--pswatch:var(--planning-swatch-e);}
.ds-pswatch-f{--pswatch:var(--planning-swatch-f);}
.ds-pswatch-g{--pswatch:var(--planning-swatch-g);}
.ds-pswatch-h{--pswatch:var(--planning-swatch-h);}
.ds-pswatch-none{--pswatch:var(--planning-swatch-none);}

/* ─── Engineer swatches (Gate D) ─────────────────────────────────────────
   A second, independent identity channel from the project swatches above —
   same palette, reused for a role that never shares a shape with --pswatch
   (the avatar background here, never a card accent bar or a project dot).
   Sets --eswatch only; --pswatch and --eswatch are never read by the same
   element, so a project and an engineer sharing a letter is never visually
   ambiguous. See js/planning/planning-ui.js colorForEngineer. */
.ds-eswatch-a{--eswatch:var(--planning-swatch-a);}
.ds-eswatch-b{--eswatch:var(--planning-swatch-b);}
.ds-eswatch-c{--eswatch:var(--planning-swatch-c);}
.ds-eswatch-d{--eswatch:var(--planning-swatch-d);}
.ds-eswatch-e{--eswatch:var(--planning-swatch-e);}
.ds-eswatch-f{--eswatch:var(--planning-swatch-f);}
.ds-eswatch-g{--eswatch:var(--planning-swatch-g);}
.ds-eswatch-h{--eswatch:var(--planning-swatch-h);}
.ds-eswatch-none{--eswatch:var(--planning-swatch-none);}

/* An avatar coloured by engineer identity rather than the platform's single
   primary accent — opt-in modifier, .ds-avatar's default (--color-primary)
   is untouched everywhere else it is already used (header, dropdowns). */
.ds-avatar--eng{background:var(--eswatch,var(--color-primary));}

/* ─── Project badge ─────────────────────────────────────────────────────────
   The dot answers one question — which project — and the name answers it
   again in words, so the badge never depends on hue alone. */
.ds-pbadge{display:inline-flex;align-items:center;gap:var(--space-2);
  min-width:0;font-size:var(--font-size-small);color:var(--color-text);}
.ds-pbadge--sm{font-size:var(--font-size-caption);gap:var(--space-1);}
.ds-pbadge-dot{flex-shrink:0;width:10px;height:10px;border-radius:var(--radius-pill);
  background:var(--pswatch,var(--planning-swatch-none));}
.ds-pbadge--sm .ds-pbadge-dot{width:8px;height:8px;}
.ds-pbadge-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ds-pbadge--dot-only{gap:0;}
/* Sits immediately before a name that is already on screen, so it brings its
   own breathing room and stays out of the accessible name entirely. */
.ds-pbadge--decorative{gap:0;margin-right:var(--space-2);vertical-align:middle;}

/* ─── Time block ────────────────────────────────────────────────────────────
   One component, three amounts of room. The accent bar is the project's
   colour and the only place --pswatch is used here; type lives in the icon
   and status in a treatment plus a word, so no hue ever carries two meanings.

   Layers, in the order they are given up as space runs out:
     expanded     time · title · location · icon · status
     comfortable  time · title · location · icon · status  (tighter)
     compact      time · title
   ─────────────────────────────────────────────────────────────────────── */
.ds-pblock{
  display:flex;align-items:stretch;gap:var(--space-2);
  min-width:0;width:100%;text-align:left;
  padding:var(--space-2);border-radius:var(--radius-md);
  background:var(--color-surface-sunken);
  color:inherit;text-decoration:none;
  transition:background var(--duration-fast) var(--ease-out),
             box-shadow var(--duration-fast) var(--ease-out);
}
a.ds-pblock{cursor:pointer;}
a.ds-pblock:hover{background:var(--color-surface-hover);
  box-shadow:var(--shadow-xs);text-decoration:none;}
a.ds-pblock:focus-visible{outline:none;box-shadow:var(--shadow-focus);}

/* The project's colour. A bar rather than a fill: a saturated block behind
   text costs contrast, and the strip has to stay calm at seven columns. */
.ds-pblock-accent{flex-shrink:0;width:3px;border-radius:var(--radius-pill);
  background:var(--pswatch,var(--planning-swatch-none));}

.ds-pblock-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;}
.ds-pblock-time{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);font-variant-numeric:tabular-nums;}
.ds-pblock-title{font-size:var(--font-size-small);font-weight:var(--weight-semibold);
  color:var(--color-heading);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ds-pblock-sub{font-size:var(--font-size-caption);color:var(--color-text-muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ds-pblock-end{flex-shrink:0;display:flex;align-items:center;gap:var(--space-2);}
.ds-pblock-icon{display:inline-flex;color:var(--color-text-subtle);}

.ds-pblock--compact{padding:var(--space-1) var(--space-2);gap:var(--space-1);}
.ds-pblock--compact .ds-pblock-title{font-size:var(--font-size-caption);}
.ds-pblock--expanded{padding:var(--space-3);gap:var(--space-3);}
.ds-pblock--expanded .ds-pblock-accent{width:4px;}
.ds-pblock--expanded .ds-pblock-title{font-size:var(--font-size-body);}
.ds-pblock--expanded .ds-pblock-sub{font-size:var(--font-size-small);}

/* Cancelled work stays on the board — an engineer needs to see it was called
   off rather than wonder where it went — but recedes so it cannot be read as
   live. The strike-through carries it without relying on the dimming alone. */
.ds-pblock--cancelled{opacity:0.6;}
.ds-pblock--cancelled .ds-pblock-title{text-decoration:line-through;}

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

/* ─── Today indicator (shared) ──────────────────────────────────────────────
   THE one way ORIX says "this is today". The day strip uses it now, the week
   strip and the month grid will use this same class — three surfaces that each
   invented their own today would be three slightly different todays.

   A filled disc, not a tint: a disc is a SHAPE, so it survives greyscale, low
   contrast and colour blindness. Colour never carries meaning alone here
   either — whoever renders this also puts the word "today" in the accessible
   name (planningUI.today.word()).

   It sets no font and no size, so it composes onto whatever element already
   carries the date without fighting it. */
.ds-ptoday{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:1.9em;height:1.9em;padding:0 0.3em;
  border-radius:var(--radius-pill);
  background:var(--planning-today-bg);
  color:var(--planning-today-fg);
}

/* ─── Brand mark (superseded, kept inert) ────────────────────────────────────
   This CSS-mask technique existed for a single-colour placeholder mark
   (assets/branding/logo-mark.svg) that inherited surrounding ink. The
   delivered ORIX mark (UI Harmonisation Sprint 1) is a multi-tone, faceted
   PNG — exactly the case this comment used to warn about ("should the
   delivered mark be multi-colour, this rule becomes an <img> pair"). That
   change has been made, and corrected again in the UI-2 branding fix: the
   header (.wm-wordmark-glyph) renders the compact logo-{light,dark}-mark.png
   on its own (no adjacent text — the mark already carries "ORIX"), while the
   sidebar (.app-sidebar-logo) and login card (.auth-logo-lockup) — both with
   room to spare — render the single-image logo-{light,dark}-horizontal.png
   lockup instead. None of the three pair the image with a second, separate
   "ORIX" text node any more; that pairing was a double wordmark, not a
   fallback. `.ds-brandmark` is no longer referenced by any page; left
   defined (not deleted) as it costs nothing and remains a documented
   single-colour fallback technique should a future icon-sized context need
   one. */
.ds-brandmark{
  display:inline-block;flex-shrink:0;
  width:1em;height:1em;
  background-color:currentColor;
  -webkit-mask:url('/assets/branding/logo-mark.svg') center/contain no-repeat;
          mask:url('/assets/branding/logo-mark.svg') center/contain no-repeat;
}

/* ─── Day heading atoms (shared) ────────────────────────────────────────────
   A weekday and a date, styled once. The day strip and the week strip both
   draw a Monday; two components that each style their own would drift the
   moment one of them is touched. The host scopes the size — a chip and a
   column want different weights of the same figure.

   Sora on the date: a calendar day number is a display figure, not body text.
   It is the design system's one standing exception and it does not widen. */
.ds-pdow{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);text-transform:uppercase;
  letter-spacing:var(--tracking-caption);}
.ds-pdate{font-family:var(--font-display);font-size:var(--font-size-h2);
  font-weight:var(--weight-bold);line-height:1.9;
  font-variant-numeric:tabular-nums;}

/* ─── Day strip ─────────────────────────────────────────────────────────────
   The navigation rail: which day, and how busy — never what is on it.

   Today and selected are deliberately opposite shapes. Today is a disc INSIDE
   the chip (see .ds-ptoday above); selection is a ring AROUND it. They occur
   together constantly, so they must never be able to be mistaken for one
   another, and neither may lean on hue to be told apart. */
.ds-pdaystrip{display:flex;gap:var(--space-2);overflow-x:auto;
  padding-bottom:var(--space-2);
  scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;}
.ds-pdaystrip::-webkit-scrollbar{height:6px;}
.ds-pdaystrip::-webkit-scrollbar-thumb{background:var(--color-border);
  border-radius:var(--radius-pill);}

.ds-pday{flex:0 0 auto;min-width:68px;min-height:var(--touch-target);
  scroll-snap-align:start;
  display:flex;flex-direction:column;align-items:center;gap:var(--space-1);
  padding:var(--space-2);
  border-radius:var(--radius-lg);
  background:var(--color-surface);
  border:var(--border-width) solid var(--color-border-subtle);
  color:var(--color-heading);
  font:inherit;text-align:center;text-decoration:none;cursor:pointer;
  transition:border-color var(--duration-fast) var(--ease-out),
             box-shadow var(--duration-fast) var(--ease-out),
             background var(--duration-fast) var(--ease-out);}
.ds-pday:hover{border-color:var(--color-primary);background:var(--color-surface-hover);
  text-decoration:none;}
.ds-pday:focus-visible{outline:none;box-shadow:var(--shadow-focus);}

/* The ring. Two pixels of border would shift the chip by a pixel and make the
   whole strip jump on selection, so the second ring is a shadow. */
.ds-pday--selected{border-color:var(--color-primary);
  box-shadow:0 0 0 1px var(--color-primary) inset;}


/* How busy, not what. One dot per item up to the cap, each in its project's
   colour; the counter carries whatever did not fit. */
.ds-pday-load{display:flex;align-items:center;justify-content:center;
  gap:3px;min-height:12px;margin-top:auto;}
.ds-pday-dot{width:6px;height:6px;border-radius:var(--radius-pill);
  background:var(--pswatch,var(--planning-swatch-none));}
.ds-pday-more{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);font-variant-numeric:tabular-nums;line-height:1;}
.ds-pday-load--free{font-size:var(--font-size-caption);color:var(--color-text-subtle);}

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

/* ─── Week strip ────────────────────────────────────────────────────────────
   Seven days and what is on them. NOT a spreadsheet: no gridlines, no cell
   borders, no header row. A column is a stack of air with a date on top; the
   blocks inside are the only thing carrying weight, and the eye reads work
   rather than a table.

   The old calendar strip gave every day a bordered box and coloured its
   blocks by TYPE. Both are gone: the box because seven bordered boxes look
   like cells, the type colour because colour means project now. */
.ds-pweek{display:flex;gap:var(--space-1);overflow-x:auto;
  padding-bottom:var(--space-2);
  scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;}
.ds-pweek::-webkit-scrollbar{height:6px;}
.ds-pweek::-webkit-scrollbar-thumb{background:var(--color-border);
  border-radius:var(--radius-pill);}

.ds-pweek-day{flex:1 1 0;min-width:132px;scroll-snap-align:start;
  display:flex;flex-direction:column;gap:var(--space-2);
  padding:var(--space-2);border-radius:var(--radius-lg);
  /* No border and no fill. A day earns chrome only when it is selected. */
  transition:background var(--duration-fast) var(--ease-out),
             box-shadow var(--duration-fast) var(--ease-out);}
/* Selection is a ring, exactly as in the day strip — today stays a disc on
   the date, so the two are never the same signal. */
.ds-pweek-day--selected{box-shadow:0 0 0 var(--border-width-strong) var(--color-primary) inset;}

.ds-pweek-head{display:flex;flex-direction:column;align-items:center;
  gap:var(--space-1);padding:var(--space-1);border-radius:var(--radius-md);
  color:var(--color-heading);text-decoration:none;}
a.ds-pweek-head{min-height:var(--touch-target);justify-content:center;}
a.ds-pweek-head:hover{background:var(--color-surface-hover);text-decoration:none;}
a.ds-pweek-head:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
/* A column's date is a heading, not a hero — the day strip's figure is the
   one you scan, this one only labels the column beneath it. */
.ds-pweek .ds-pdate{font-size:var(--font-size-h3);line-height:1.7;}

.ds-pweek-body{display:flex;flex-direction:column;gap:var(--space-1);}
.ds-pweek-more{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);padding:0 var(--space-2);
  font-variant-numeric:tabular-nums;}
/* An empty day is not an error: one quiet word, no dashed box, no icon. */
.ds-pweek-empty{font-size:var(--font-size-caption);color:var(--color-text-subtle);
  padding:var(--space-2);}

/* Below tablet the strip scrolls, so a column keeps its full width and drops
   the supporting layers instead of shrinking them into nothing. Time and title
   survive at every width — and everything hidden here is still in the block's
   accessible name, so the screen reader loses nothing to the viewport. */
@media(max-width:767px){
  .ds-pweek-day{min-width:150px;}
  .ds-pweek .ds-pblock-sub,
  .ds-pweek .ds-pblock-end{display:none;}
}

@media(prefers-reduced-motion:reduce){
  .ds-pweek-day{transition:none;}
}

/* ─── Planning card ─────────────────────────────────────────────────────────
   One piece of scheduled work in a LIST, where nothing is known from context:
   who is going, for whom, where, what kind, what state, and what you can do.

   It is a row, not a surface. ORIX cards are the only surface and a card never
   nests inside a card, so this carries no background and no border of its own
   and separates from its neighbour with the same hairline .ds-list-flush uses.
   The card it lives in stays the only surface on the screen.

   Density is room, never importance — the accessible name carries the full
   context at every density, including the fields this width dropped. */
.ds-pcard{
  display:flex;align-items:center;gap:var(--space-3);
  width:100%;min-width:0;min-height:var(--touch-target);
  padding:var(--space-3);border-radius:var(--radius-md);
  color:inherit;text-align:left;text-decoration:none;
  position:relative;
  transition:background var(--duration-fast) var(--ease-out);
}
.ds-pcard + .ds-pcard::before{
  content:"";position:absolute;top:0;left:var(--space-3);right:var(--space-3);
  border-top:var(--border-width) solid var(--color-border-subtle);
}
a.ds-pcard:hover{background:var(--color-surface-hover);text-decoration:none;}
a.ds-pcard:focus-visible{outline:none;box-shadow:var(--shadow-focus);}

/* The project's colour, and the only rule in this component that reads it. */
.ds-pcard-accent{flex-shrink:0;align-self:stretch;width:3px;min-height:2.2em;
  border-radius:var(--radius-pill);
  background:var(--pswatch,var(--planning-swatch-none));}

.ds-pcard-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}
.ds-pcard-time{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);font-variant-numeric:tabular-nums;}
.ds-pcard-title{font-size:var(--font-size-body);font-weight:var(--weight-semibold);
  color:var(--color-heading);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* The supporting facts read as one scannable line, not as labelled fields. */
.ds-pcard-meta{font-size:var(--font-size-small);color:var(--color-text-muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ds-pcard-icon{flex-shrink:0;display:inline-flex;color:var(--color-text-subtle);}
.ds-pcard-end{flex-shrink:0;display:flex;align-items:center;gap:var(--space-2);}

.ds-pcard--compact{padding:var(--space-2) var(--space-3);gap:var(--space-2);}
.ds-pcard--compact .ds-pcard-title{font-size:var(--font-size-small);}
.ds-pcard--expanded{padding:var(--space-4) var(--space-3);gap:var(--space-3);}
.ds-pcard--expanded .ds-pcard-accent{width:4px;}

/* Cancelled work stays on the list — an engineer needs to see it was called
   off rather than wonder where it went — but recedes so it cannot be read as
   live. The strike-through carries it without relying on the dimming alone. */
.ds-pcard--cancelled{opacity:0.6;}
.ds-pcard--cancelled .ds-pcard-title{text-decoration:line-through;}

/* On a phone an action button beside the text squeezes the time onto two lines
   and the project name into an ellipsis. The end moves to its own line and the
   row is otherwise exactly the row above — one component answering to width,
   not a mobile variant.

   Keyed on the ACTION, not on the density: a comfortable row with an Edit
   button has the same problem an expanded one does, and hanging this on
   --expanded left the whole planning board crushed. */
@media(max-width:767px){
  .ds-pcard--action{flex-wrap:wrap;}
  .ds-pcard--action .ds-pcard-main{min-width:calc(100% - 36px - var(--space-3));}
  .ds-pcard--action .ds-pcard-end{width:100%;justify-content:flex-end;}
  .ds-pcard--action .ds-pcard-end .ds-btn{min-height:var(--touch-target);}
}

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

/* ─── Planning Overview card (Gate D) ────────────────────────────────────
   A purpose-built card for the admin/Planner and Field Engineer Planning
   Overview — richer than .ds-pcard's list-row density (engineer identity,
   a work-type badge, site/end-customer, ticket/project reference) needs,
   built from the same atomic primitives (ds-avatar, ds-chip, icons) rather
   than stretching the shared list-row component past what it was designed
   for. .ds-pcard/timeBlock stay exactly as they are for the dashboard, week
   strip and day strip — this is additive, not a replacement.

   The whole card is the primary click target (role="button"); its own
   action buttons (Edit/Cancel/Reactivate) sit in ds-oacard-end and must
   never also trigger the parent card's click — the click/keydown handlers
   in operations.js guard for that, not this stylesheet. */
.ds-oacard{
  display:flex;align-items:flex-start;gap:var(--space-3);
  padding:var(--space-3);border-bottom:var(--border-width) solid var(--color-border);
  cursor:pointer;
}
.ds-oacard:last-child{border-bottom:none;}
.ds-oacard:hover{background:var(--color-surface-sunken);}
.ds-oacard:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
.ds-oacard-avatar{flex-shrink:0;}
.ds-oacard-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}
.ds-oacard-top{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
.ds-oacard-engineer{font-size:var(--font-size-body);font-weight:var(--weight-semibold);
  color:var(--color-heading);}
.ds-oacard-worktype{display:inline-flex;align-items:center;gap:var(--space-1);
  font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  padding:var(--space-1) var(--space-2);border-radius:var(--radius-pill);
  background:var(--color-surface-sunken);color:var(--color-text-muted);
  letter-spacing:0.02em;}
.ds-oacard-line{font-size:var(--font-size-small);color:var(--color-text-muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ds-oacard-line--title{font-size:var(--font-size-body);font-weight:var(--weight-medium);
  color:var(--color-text);}
.ds-oacard-time{font-variant-numeric:tabular-nums;}
.ds-oacard-end{flex-shrink:0;display:flex;align-items:center;gap:var(--space-2);
  flex-wrap:wrap;justify-content:flex-end;}

/* Cancelled work stays visible — same rationale as .ds-pcard--cancelled — but
   recedes so it cannot be read as live, and detail/click must still work. */
.ds-oacard--cancelled{opacity:0.6;}
.ds-oacard--cancelled .ds-oacard-line--title{text-decoration:line-through;}

@media(max-width:767px){
  .ds-oacard{flex-wrap:wrap;}
  .ds-oacard-end{width:100%;justify-content:flex-end;}
  .ds-oacard-end .ds-btn{min-height:var(--touch-target);}
}

/* ─── Week navigator ────────────────────────────────────────────────────────
   One week back, one week on, and the way home. Three links, because a week
   is a place with an address — the browser's back button, sharing and
   keyboard support then come from the platform instead of from handlers.

   "This week" is never disabled: a dead control is a trap for anyone who
   tabbed to it. On the current week it carries aria-current="page" instead. */
.ds-pweeknav{display:flex;align-items:center;gap:var(--space-2);
  flex-wrap:wrap;margin-bottom:var(--space-4);}
.ds-pweeknav-step{display:inline-flex;align-items:center;justify-content:center;
  width:var(--touch-target);height:var(--touch-target);flex-shrink:0;
  border-radius:var(--radius-md);
  border:var(--border-width) solid var(--color-border-subtle);
  background:var(--color-surface);color:var(--color-text);text-decoration:none;
  transition:background var(--duration-fast) var(--ease-out),
             border-color var(--duration-fast) var(--ease-out);}
.ds-pweeknav-step:hover{background:var(--color-surface-hover);
  border-color:var(--color-primary);text-decoration:none;}
.ds-pweeknav-step:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
.ds-pweeknav-label{display:flex;flex-direction:column;min-width:0;
  padding:0 var(--space-2);}
.ds-pweeknav-range{font-size:var(--font-size-body);font-weight:var(--weight-semibold);
  color:var(--color-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.ds-pweeknav-week{font-size:var(--font-size-caption);color:var(--color-text-muted);
  text-transform:uppercase;letter-spacing:var(--tracking-caption);}
/* Pushed to the far end on a roomy row; on a phone it drops to its own line
   below, full width, where a thumb can actually reach it. */
/* Matched to the arrows beside it: a row where one control is 44px and its
   neighbour 32px reads as two different kinds of control. */
.ds-pweeknav-today{margin-left:auto;min-height:var(--touch-target);}
.ds-pweeknav-today[aria-current="page"]{border-color:var(--color-primary);
  color:var(--color-primary);}
@media(max-width:520px){
  .ds-pweeknav-today{margin-left:0;width:100%;}
}
@media(prefers-reduced-motion:reduce){
  .ds-pweeknav-step{transition:none;}
}

/* ─── Week Calendar time grid (Gate E) ───────────────────────────────────
   An Outlook-like targeted week grid — the --cal-* tokens below were laid
   down for exactly this and had no consumer until now. Always spans the
   full 00:00-24:00 day (js/planning/planning-week-grid.js never clips or
   hides an hour); only the initial scroll landing position varies.

   Horizontal scroll (the OUTER .ds-calgrid) keeps the header row, the
   all-day row and the time-grid body moving together as one unit on a
   narrow viewport, via a shared min-width on .ds-calgrid-inner. Vertical
   scroll is scoped to just .ds-calgrid-scroll, so the day headers and any
   all-day items stay visible while the hours scroll underneath them. */
.ds-calgrid{overflow-x:auto;border:var(--border-width) solid var(--cal-cell-border);
  border-radius:var(--card-radius);background:var(--cal-cell-bg);}
.ds-calgrid-inner{min-width:640px;display:flex;flex-direction:column;}
.ds-calgrid-headrow,.ds-calgrid-allday-row{display:flex;
  border-bottom:var(--border-width) solid var(--cal-cell-border);}
.ds-calgrid-axis-spacer{flex:0 0 56px;padding:var(--space-2);
  font-size:var(--font-size-caption);color:var(--color-text-muted);
  display:flex;align-items:center;}
.ds-calgrid-daycol-head{flex:1;min-width:0;padding:var(--space-2);text-align:center;
  border-left:var(--border-width) solid var(--cal-cell-border);
  display:flex;flex-direction:column;align-items:center;gap:2px;}
.ds-calgrid-allday-col{flex:1;min-width:0;padding:var(--space-1) var(--space-2);
  border-left:var(--border-width) solid var(--cal-cell-border);
  display:flex;flex-direction:column;gap:var(--space-1);}
.ds-calgrid-scroll{max-height:min(720px,70vh);overflow-y:auto;}
.ds-calgrid-body{display:flex;position:relative;}
.ds-calgrid-axis{flex:0 0 56px;position:relative;}
.ds-calgrid-hour{position:absolute;left:0;right:0;height:0;}
.ds-calgrid-hour-label{position:absolute;top:-7px;left:var(--space-1);right:var(--space-1);
  font-size:var(--font-size-caption);color:var(--color-text-muted);
  background:var(--cal-cell-bg);}
.ds-calgrid-daycol{flex:1;min-width:0;position:relative;
  border-left:var(--border-width) solid var(--cal-cell-border);
  background-repeat:repeat-y;
  background-image:linear-gradient(to bottom,
    var(--cal-cell-border) 0, var(--cal-cell-border) 1px, transparent 1px, transparent 100%);}
.ds-calgrid-event-slot{position:absolute;overflow:hidden;border-radius:var(--cal-event-radius);}

/* One clickable event, built by the caller's renderEvent — this file only
   owns the slot's geometry above; the visual language of the event itself
   (engineer colour, badge, text) is Operations' own, same "vocabulary
   belongs to the caller" contract as planningCard/timeBlock. */
.ds-calevent{display:flex;flex-direction:column;gap:1px;height:100%;width:100%;
  padding:var(--space-1) var(--space-2);border-radius:var(--cal-event-radius);
  background:var(--eswatch,var(--planning-swatch-none));color:var(--color-surface);
  cursor:pointer;text-align:left;overflow:hidden;box-shadow:0 0 0 1px rgba(0,0,0,0.08);}
.ds-calevent:hover{filter:brightness(1.05);}
.ds-calevent:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-surface),
  0 0 0 4px var(--cal-today-ring);}
.ds-calevent-top{display:flex;align-items:center;gap:var(--space-1);min-width:0;}
.ds-calevent-time{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  font-variant-numeric:tabular-nums;flex-shrink:0;}
.ds-calevent-worktype{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  opacity:0.85;flex-shrink:0;white-space:nowrap;}
.ds-calevent-engineer{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ds-calevent-line{font-size:var(--font-size-caption);opacity:0.92;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ds-calevent--cancelled{opacity:0.55;}
.ds-calevent--cancelled .ds-calevent-line,
.ds-calevent--cancelled .ds-calevent-engineer{text-decoration:line-through;}

@media(max-width:900px){
  .ds-calgrid-inner{min-width:560px;}
}

/* ─── Hours: week overview ──────────────────────────────────────────────────
   A day is a card with rows in it, filled or not, so an empty Wednesday reads
   as a day with nothing on it rather than as a day that failed to load. */
.tt-week-strip{margin-bottom:var(--space-4);}
.tt-day{scroll-margin-top:var(--space-4);}
.tt-day:focus{outline:none;}
/* Jumping to a day must be visible to whoever is driving by keyboard. */
.tt-day:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
.tt-day-today{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-primary);text-transform:uppercase;
  letter-spacing:var(--tracking-caption);margin-left:var(--space-2);}
.tt-day-empty{font-size:var(--font-size-small);color:var(--color-text-subtle);
  margin:0;padding:var(--space-3) 0 0;}
/* Status counts sit under the week's type breakdown: how many entries are in
   each state, using the same chip the rest of the platform uses. */
.tt-week-statuses{display:flex;flex-wrap:wrap;gap:var(--space-3);
  padding-top:var(--space-3);margin-top:var(--space-3);
  border-top:var(--border-width) solid var(--color-border-subtle);}
.tt-week-status{display:inline-flex;align-items:center;gap:var(--space-1);}
.tt-week-status-n{font-size:var(--font-size-small);font-weight:var(--weight-semibold);
  color:var(--color-text-muted);font-variant-numeric:tabular-nums;}

/* ─── Operations: day-grouped planning lists ────────────────────────────────
   Planning and My planning are day cards with planning-card rows in them —
   the same shape My hours and the hours week already use. The date figure is
   the shared .ds-pdate atom, so a Monday looks like a Monday everywhere. */
.ops-day-title{display:flex;align-items:baseline;gap:var(--space-2);min-width:0;}
.ops-day-title .ds-pdate{font-size:var(--font-size-h3);line-height:1.4;}
.ops-day-today{font-size:var(--font-size-caption);font-weight:var(--weight-semibold);
  color:var(--color-primary);text-transform:uppercase;
  letter-spacing:var(--tracking-caption);}

/* The day strip's note: a caller's own one-line summary in place of the dots.
   Hours puts the day's worked time here. */
.ds-pday-load--note{font-size:var(--font-size-caption);
  font-weight:var(--weight-semibold);color:var(--color-text-muted);
  font-variant-numeric:tabular-nums;white-space:nowrap;}

/* ─── Intake — expandable raw source email body (Gate I6C) ──────────────────
   Read-only, plain text only (never rendered as HTML — see intake-detail.js's
   own header comment on why). Bounded height with its own scroll, same
   focusable-scroll-region treatment .ds-table-wrap already uses elsewhere. */
.intake-source-body{
  margin:var(--space-2) 0 0;padding:var(--space-3);
  max-height:320px;overflow-y:auto;
  white-space:pre-wrap;word-break:break-word;
  font-family:inherit;font-size:var(--font-size-small);color:var(--color-text);
  background:var(--color-surface-sunken);border:var(--border-width) solid var(--color-border);
  border-radius:var(--radius-sm);
}
.intake-source-body:focus-visible{outline:none;box-shadow:var(--shadow-focus);}
