@charset "UTF-8";
/* ==========================================================================
   MTS Project Hub — theme.css
   --------------------------------------------------------------------------
   The whole design system in one stylesheet: no build step, no Sass, no utility
   framework. Every colour, radius, shadow and metric is a custom property, so
   the dark-scheme block at §02 remaps VARIABLES ONLY and the entire interface
   flips without a single duplicated rule. If you find yourself writing a colour
   literal below §02, add a token instead.

   Flat design throughout: there are deliberately no gradients in this file.
   Depth comes from a 1px border plus a very small shadow, nothing else.

   TABLE OF CONTENTS
     01. Design tokens (:root)
     02. Dark scheme — variables only
     03. Reset and base elements
     04. Typography
     05. Accessibility primitives (skip link, focus rings, sr-only)
     06. App shell — sidebar drawer, topbar, content, container
     07. Surfaces — .ph-card
     08. Buttons
     09. Badges, chips, money
     10. Forms
     11. Tables — including the mobile stacked-card pattern
     12. Grid and stats
     13. Tabs and panels
     14. Key/value lists
     15. Alerts
     16. Empty state and progress
     17. Toolbar, filters, pagination
     18. Page header
     19. Row states and priority dots
     20. Timeline and activity feed
     21. Dropdown menus
     22. Modal
     23. Avatars, code, muted text, icons
     24. Responsive helpers
     25. Reduced motion
   ========================================================================== */


/* ==========================================================================
   01. DESIGN TOKENS
   ========================================================================== */

:root {
  /* Let the OS render form controls and scrollbars in the matching scheme. */
  color-scheme: light dark;

  /* ── Brand ───────────────────────────────────────────────────────────────
     --ph-primary is the darkened brand orange used wherever WHITE TEXT sits
     on the fill. Contrast rationale, measured with the WCAG relative-luminance
     formula:

       #ffffff on #c96e02 → 3.67:1

     That clears the 3:1 floor required for large text and for non-text UI
     boundaries (so buttons, focus rings and badges are legible), but it does
     NOT reach the 4.5:1 that WCAG AA asks of normal-size body copy. Two
     consequences, both deliberate:
       • Primary buttons carry a SEMIBOLD label and are the only place white
         text sits on the primary fill — never long-form copy.
       • If AA for button labels is ever a hard requirement, this is the one
         token to change: --ph-primary: #a85b01 measures 5.05:1 and every
         component below picks the new value up automatically, because nothing
         hard-codes the hex. It is kept as --ph-primary-strong for hover/active
         states, where the darker fill is welcome anyway.

     --ph-accent is the undarkened brand orange. It is an ACCENT only — focus
     rings, the active nav bar, tab underlines — and never carries white text,
     because white on #f08503 is only 2.4:1. */
  --ph-primary: #c96e02;
  --ph-primary-strong: #a85b01;
  --ph-primary-tint: #fdf2e3;
  --ph-accent: #f08503;

  /* ── Structure ───────────────────────────────────────────────────────── */
  --ph-sidebar-bg: #080808;
  --ph-sidebar-hover: #1c1c1c;
  --ph-page: #ffffff;
  --ph-surface: #f7f7f8;
  --ph-card: #ffffff;
  --ph-border: #e4e4e7;
  --ph-border-strong: #d0d0d6;

  /* ── Text ────────────────────────────────────────────────────────────── */
  --ph-text: #080808;
  --ph-text-muted: #5f5f66;
  --ph-text-invert: #ffffff;
  --ph-sidebar-text: rgba(255, 255, 255, 0.72);
  --ph-sidebar-text-strong: #ffffff;

  /* ── Status ──────────────────────────────────────────────────────────── */
  --ph-danger: #b3261e;
  --ph-success: #2f8f4e;
  --ph-warning: #8a5a00;
  --ph-info: #1f6feb;

  /* Status tints. Flat solid colours, used as the background behind the
     matching status text. Each pairing is >= 4.5:1 in light scheme. */
  --ph-danger-tint: #fbeceb;
  --ph-success-tint: #e9f5ed;
  --ph-warning-tint: #fbf1e0;
  --ph-info-tint: #eaf1fe;
  --ph-neutral-tint: #f4f4f5;

  /* Translucent scrim for the modal backdrop — an rgba() value, not a
     gradient, so the flat-design rule still holds. */
  --ph-scrim: rgba(8, 8, 8, 0.55);

  /* ── Shape and depth ─────────────────────────────────────────────────── */
  --ph-radius-sm: 4px;
  --ph-radius: 8px;
  --ph-radius-lg: 12px;
  --ph-radius-pill: 999px;
  --ph-shadow-sm: 0 1px 2px rgba(8, 8, 8, 0.06);
  --ph-shadow: 0 2px 8px rgba(8, 8, 8, 0.08);
  --ph-shadow-pop: 0 10px 30px rgba(8, 8, 8, 0.18);

  /* ── Metrics ─────────────────────────────────────────────────────────────
     --ph-tap is the single source of truth for the 44px minimum touch target.
     It is applied to every button, form control, nav link and dropdown item.
     Do not shrink it: this application is used on phones on site, one-handed,
     in sunlight and often with gloves on. */
  --ph-tap: 44px;
  --ph-sidebar-w: 248px;
  --ph-topbar-min-h: 60px;
  --ph-gap: 1rem;

  /* ── Type ────────────────────────────────────────────────────────────── */
  --ph-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --ph-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}


/* ==========================================================================
   02. DARK SCHEME — VARIABLES ONLY
   --------------------------------------------------------------------------
   prefers-color-scheme is honoured automatically. There is no theme toggle and
   no .dark class, so this block must remain a pure variable remap: the moment a
   rule other than a custom property appears in here, the "one definition per
   component" property of this file is lost and the two schemes start to drift.

   Deliberately NOT remapped:
     • --ph-primary / --ph-accent — brand identity stays the same in both
       schemes, and white on #c96e02 still measures 3.67:1.
     • the sidebar colours — the sidebar is #080808 in light scheme already, so
       in dark scheme it simply merges into the page and the border separates
       them.
   ========================================================================== */

@media (prefers-color-scheme: dark) {
  :root {
    --ph-page: #0f0f11;
    --ph-surface: #17171a;
    --ph-card: #1a1a1e;
    --ph-border: #2c2c33;
    --ph-border-strong: #3a3a42;

    --ph-text: #f2f2f4;
    --ph-text-muted: #a3a3ad;

    --ph-danger-tint: #3a1917;
    --ph-success-tint: #14301f;
    --ph-warning-tint: #33260f;
    --ph-info-tint: #16233d;
    --ph-neutral-tint: #24242a;
    --ph-primary-tint: #33240e;

    --ph-scrim: rgba(0, 0, 0, 0.7);

    /* Shadows read as smudges on dark surfaces unless they are deeper. */
    --ph-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
    --ph-shadow: 0 2px 8px rgba(0, 0, 0, 0.55);
    --ph-shadow-pop: 0 10px 30px rgba(0, 0, 0, 0.7);
  }
}


/* ==========================================================================
   03. RESET AND BASE ELEMENTS
   ========================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  padding: 0;
  background: var(--ph-page);
  color: var(--ph-text);
  font-family: var(--ph-font);
  font-size: 1rem;
  line-height: 1.55;
  /* Long unbroken strings (a job-card reference pasted from a supplier email)
     must wrap instead of widening the layout on a 375px phone. */
  overflow-wrap: break-word;
}

img,
svg,
video {
  max-width: 100%;
}

/* Icons sit inline with text in buttons, nav links and table cells. Without
   this they are flex items that happily shrink to nothing when a label is long,
   which is what makes an icon look "squashed" instead of square. */
.ph-icon {
  flex: 0 0 auto;
  vertical-align: middle;
}

hr {
  border: 0;
  border-top: 1px solid var(--ph-border);
  margin: 1.25rem 0;
}

[hidden] {
  display: none !important;
}


/* ==========================================================================
   04. TYPOGRAPHY
   ========================================================================== */

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 0.5rem;
  line-height: 1.25;
  font-weight: 700;
  color: var(--ph-text);
}

h1 { font-size: 1.45rem; }
h2 { font-size: 1.2rem; }
h3 { font-size: 1.05rem; }
h4 { font-size: 0.95rem; }

p { margin: 0 0 0.85rem; }
p:last-child { margin-bottom: 0; }

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

a:hover,
a:focus-visible {
  text-decoration: underline;
}

small { font-size: 0.82rem; }

strong, b { font-weight: 700; }

ul, ol { margin: 0 0 0.85rem; padding-left: 1.25rem; }

/* Content headings that follow a block of text need breathing room. */
.ph-content h2:not(:first-child),
.ph-content h3:not(:first-child) {
  margin-top: 1.5rem;
}


/* ==========================================================================
   05. ACCESSIBILITY PRIMITIVES
   ========================================================================== */

/* Focus ring, one definition for every interactive element. Uses the UNDARKENED
   brand orange because a focus ring is a non-text boundary: it needs 3:1 against
   its surroundings, not 4.5:1, and the brighter orange is far easier to spot on
   both the white page and the near-black sidebar. :focus-visible means a mouse
   click does not leave a ring behind, but a keyboard user always sees one. */
a:focus-visible,
area:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
details:focus-visible,
[tabindex]:focus-visible,
[contenteditable]:focus-visible,
.ph-btn:focus-visible,
.ph-tab:focus-visible,
.ph-chip:focus-visible,
.ph-sidebar__link:focus-visible,
.ph-dropdown__item:focus-visible,
.ph-dropdown__toggle:focus-visible,
.ph-sidebar__toggle:focus-visible,
.ph-pagination a:focus-visible {
  outline: 2px solid var(--ph-accent);
  outline-offset: 2px;
}

/* Browsers without :focus-visible (anything before ~2020) would otherwise show
   no ring at all on a link or a button, since this file sets border: 0 on
   several controls. Give those browsers the plain :focus ring instead. */
@supports not selector(:focus-visible) {
  a:focus,
  button:focus,
  input:focus,
  select:focus,
  textarea:focus,
  [tabindex]:focus {
    outline: 2px solid var(--ph-accent);
    outline-offset: 2px;
  }
}

.ph-visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* First focusable thing in the document. Off-screen until focused, then it
   drops over the topbar so a keyboard user can jump the 13-item sidebar. */
.ph-skip-link {
  position: absolute;
  top: -100px;
  left: 0.5rem;
  z-index: 200;
  display: inline-flex;
  align-items: center;
  min-height: var(--ph-tap);
  padding: 0.5rem 1rem;
  background: var(--ph-primary);
  color: var(--ph-text-invert);
  font-weight: 600;
  border-radius: 0 0 var(--ph-radius) var(--ph-radius);
  transition: top 0.15s ease;
}

.ph-skip-link:focus,
.ph-skip-link:focus-visible {
  top: 0;
  text-decoration: none;
}


/* ==========================================================================
   06. APP SHELL — SIDEBAR DRAWER, TOPBAR, CONTENT, CONTAINER
   ========================================================================== */

.ph-shell {
  min-height: 100vh;
  /* NOTE: .ph-shell must never get a transform/filter/perspective. Any of those
     makes it the containing block for position:fixed descendants, which pins
     the mobile nav button to the shell instead of the viewport and breaks the
     drawer handle entirely. */
}

.ph-shell__main {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  /* min-width: 0 lets this column shrink below its content width; without it a
     wide table deep inside pushes the whole page sideways. */
  min-width: 0;
  margin-left: var(--ph-sidebar-w);
}

/* ── Sidebar ─────────────────────────────────────────────────────────────── */

.ph-sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  width: var(--ph-sidebar-w);
  background: var(--ph-sidebar-bg);
  color: var(--ph-sidebar-text);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.ph-sidebar__brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 1rem 1rem 0.9rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  color: var(--ph-sidebar-text-strong);
}

.ph-sidebar__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  border-radius: var(--ph-radius);
  background: var(--ph-primary);
  color: var(--ph-text-invert);
}

.ph-sidebar__name {
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  line-height: 1.2;
}

/* The "Project Hub" part of the wordmark: same words, quieter weight, so the
   short initialism carries the recognition and the descriptor stays readable. */
.ph-sidebar__name-tail {
  display: block;
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ph-accent);
}

.ph-sidebar__nav {
  flex: 1 1 auto;
  padding: 0.75rem 0 1.25rem;
}

.ph-sidebar__group {
  margin: 1rem 0.9rem 0.4rem;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.42);
}

.ph-sidebar__link {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  /* 44px touch target — see --ph-tap. */
  min-height: var(--ph-tap);
  margin: 0 0.5rem 0.15rem;
  padding: 0.5rem 0.85rem;
  border-left: 3px solid transparent;
  border-radius: var(--ph-radius);
  color: var(--ph-sidebar-text);
  font-size: 0.93rem;
  text-decoration: none;
}

.ph-sidebar__link:hover {
  background: var(--ph-sidebar-hover);
  color: var(--ph-sidebar-text-strong);
  text-decoration: none;
}

/* The active marker is the accent orange border, not a filled background: two
   filled states side by side (hover and active) are indistinguishable at a
   glance on a phone in daylight. */
.ph-sidebar__link--active {
  background: var(--ph-sidebar-hover);
  border-left-color: var(--ph-accent);
  color: var(--ph-sidebar-text-strong);
  font-weight: 600;
}

.ph-sidebar__foot {
  padding: 0.75rem 1rem 1rem;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  font-size: 0.76rem;
  color: rgba(255, 255, 255, 0.45);
}

/* The drawer handle.
   WHY IT IS NOT INSIDE .ph-sidebar: under 768px the sidebar is translated
   off-canvas as a whole, and a transform makes that element the containing
   block for its position:fixed descendants — a button inside it would slide
   off-screen with the panel and become unreachable exactly when it is needed.
   sidebar.php therefore emits the button as a SIBLING, before the drawer, and
   this rule pins it to the viewport. .ph-hide-md removes it again from 768px up
   (which is why the rule above can set display without a media query). */
.ph-sidebar__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--ph-tap);
  min-height: var(--ph-tap);
  padding: 0;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius);
  background: var(--ph-card);
  color: var(--ph-text);
  cursor: pointer;
}

/* ── Topbar ──────────────────────────────────────────────────────────────── */

.ph-topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  min-height: var(--ph-topbar-min-h);
  padding: 0.5rem 1rem;
  background: var(--ph-page);
  border-bottom: 1px solid var(--ph-border);
}

.ph-topbar__title {
  flex: 1 1 10rem;
  min-width: 0;
}

.ph-topbar__title h1 {
  margin: 0;
  font-size: 1.05rem;
  line-height: 1.3;
  /* A long project name must truncate rather than wrap the topbar to 3 rows. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ph-topbar__sub {
  display: block;
  font-size: 0.78rem;
  color: var(--ph-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ph-topbar__actions {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-left: auto;
}

/* Square icon-only control (notification bell, later a theme switch). */
.ph-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: var(--ph-tap);
  min-height: var(--ph-tap);
  padding: 0;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius);
  background: var(--ph-card);
  color: var(--ph-text);
  cursor: pointer;
}

.ph-icon-btn:hover {
  background: var(--ph-surface);
  text-decoration: none;
}

/* Count bubble. Not an interactive element, so it deliberately does not honour
   --ph-tap; the 44px target is the bell link it sits on. */
.ph-icon-btn__count {
  position: absolute;
  top: -0.35rem;
  right: -0.35rem;
  min-width: 1.15rem;
  padding: 0 0.28rem;
  border-radius: var(--ph-radius-pill);
  background: var(--ph-danger);
  color: var(--ph-text-invert);
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 1.15rem;
  text-align: center;
}

/* Global search. The icon is decorative and 44px wide so the field reads as one
   control; the input beside it keeps its own 44px minimum height. */
.ph-search {
  display: flex;
  align-items: center;
}

.ph-search__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--ph-tap);
  min-height: var(--ph-tap);
  border: 1px solid var(--ph-border);
  border-right: 0;
  border-radius: var(--ph-radius) 0 0 var(--ph-radius);
  background: var(--ph-surface);
  color: var(--ph-text-muted);
}

.ph-search__input {
  width: 14rem;
  border-radius: 0 var(--ph-radius) var(--ph-radius) 0;
}

/* Signed-in user chip in the topbar. */
.ph-user {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: var(--ph-tap);
  max-width: 15rem;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius);
  background: var(--ph-card);
  color: var(--ph-text);
  text-align: left;
  cursor: pointer;
}

.ph-user__meta {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.15;
}

.ph-user__name {
  font-size: 0.85rem;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ph-user__role {
  font-size: 0.72rem;
  color: var(--ph-text-muted);
}

/* ── Content and container ───────────────────────────────────────────────── */

.ph-content {
  flex: 1 1 auto;
  padding: 1rem 0 2.5rem;
  min-width: 0;
}

.ph-container {
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 1rem;
}


/* ==========================================================================
   07. SURFACES — .ph-card
   ========================================================================== */

.ph-card {
  background: var(--ph-card);
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius);
  box-shadow: var(--ph-shadow-sm);
  margin-bottom: var(--ph-gap);
  /* Grid/flex children default to min-width:auto and refuse to shrink below
     their content; a card holding a table needs to be allowed to. */
  min-width: 0;
}

.ph-card:last-child { margin-bottom: 0; }

.ph-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--ph-border);
}

.ph-card__title {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
}

.ph-card__sub {
  margin: 0.15rem 0 0;
  font-size: 0.82rem;
  color: var(--ph-text-muted);
}

.ph-card__body { padding: 1rem; }

.ph-card__body > :last-child { margin-bottom: 0; }

.ph-card__footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.85rem 1rem;
  border-top: 1px solid var(--ph-border);
  background: var(--ph-surface);
  border-radius: 0 0 var(--ph-radius) var(--ph-radius);
}


/* ==========================================================================
   08. BUTTONS
   --------------------------------------------------------------------------
   Every button is at least --ph-tap tall, including .ph-btn--sm. The size
   modifier shrinks PADDING AND TYPE ONLY. It is tempting to make --sm 32px so
   a table action row looks tighter on desktop; do not. The same markup is what
   a site engineer taps on a phone, and a 32px target there is a mis-tap that
   either does nothing or does the wrong thing to a job card.
   ========================================================================== */

.ph-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  min-height: var(--ph-tap);
  padding: 0.5rem 1rem;
  border: 1px solid transparent;
  border-radius: var(--ph-radius);
  background: var(--ph-surface);
  color: var(--ph-text);
  font-family: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.ph-btn:hover { text-decoration: none; }

.ph-btn[disabled],
.ph-btn:disabled,
.ph-btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
  pointer-events: none;
}

/* Primary: the one filled button per view — the action the page exists for.
   White text on --ph-primary (3.67:1, semibold label). See §01 for the full
   contrast rationale and the one-token fix if AA for labels is ever required. */
.ph-btn--primary {
  background: var(--ph-primary);
  border-color: var(--ph-primary);
  color: var(--ph-text-invert);
}

.ph-btn--primary:hover {
  background: var(--ph-primary-strong);
  border-color: var(--ph-primary-strong);
  color: var(--ph-text-invert);
}

.ph-btn--secondary {
  background: var(--ph-card);
  border-color: var(--ph-border-strong);
  color: var(--ph-text);
}

.ph-btn--secondary:hover {
  background: var(--ph-surface);
  border-color: var(--ph-text-muted);
}

/* Ghost: toolbar and table-row actions, where several buttons sit together and
   filled chips would compete with the data. */
.ph-btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--ph-primary);
}

.ph-btn--ghost:hover {
  background: var(--ph-primary-tint);
  color: var(--ph-primary-strong);
}

.ph-btn--danger {
  background: var(--ph-danger);
  border-color: var(--ph-danger);
  color: var(--ph-text-invert);
}

.ph-btn--danger:hover {
  background: #8f1d17;
  border-color: #8f1d17;
  color: var(--ph-text-invert);
}

.ph-btn--sm {
  min-height: var(--ph-tap); /* see the section note above */
  padding: 0.35rem 0.7rem;
  font-size: 0.84rem;
}

.ph-btn--block {
  display: flex;
  width: 100%;
}

/* A button that is really a link inside a paragraph should not add a line box
   taller than the text around it. */
.ph-btn--inline {
  min-height: 0;
  padding: 0.15rem 0.4rem;
}


/* ==========================================================================
   09. BADGES, CHIPS, MONEY
   ========================================================================== */

.ph-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.5rem;
  border: 1px solid transparent;
  border-radius: var(--ph-radius-pill);
  background: var(--ph-neutral-tint);
  color: var(--ph-text-muted);
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1.5;
  white-space: nowrap;
  vertical-align: middle;
}

.ph-badge--active  { background: var(--ph-success-tint); color: var(--ph-success); border-color: var(--ph-success); }
.ph-badge--warning { background: var(--ph-warning-tint); color: var(--ph-warning); border-color: var(--ph-warning); }
.ph-badge--danger  { background: var(--ph-danger-tint);  color: var(--ph-danger);  border-color: var(--ph-danger); }
.ph-badge--info    { background: var(--ph-info-tint);    color: var(--ph-info);    border-color: var(--ph-info); }
.ph-badge--neutral { background: var(--ph-neutral-tint); color: var(--ph-text-muted); border-color: var(--ph-border-strong); }

/* Finance / money badge. Uses the brand gold rather than a sixth hue, which
   keeps the palette to the five status colours plus the brand. */
.ph-badge--money {
  background: var(--ph-primary-tint);
  color: var(--ph-primary-strong);
  border-color: var(--ph-primary);
}

/* Chips: free-form tags. Display-only chips stay compact; the moment a chip is
   an <a> or <button> it is a control and takes the full 44px target. */
.ph-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius-pill);
  background: var(--ph-surface);
  color: var(--ph-text);
  font-size: 0.8rem;
  line-height: 1.5;
}

a.ph-chip,
button.ph-chip {
  min-height: var(--ph-tap);
  text-decoration: none;
  cursor: pointer;
}

a.ph-chip:hover,
button.ph-chip:hover {
  border-color: var(--ph-accent);
  background: var(--ph-primary-tint);
  text-decoration: none;
}

/* Money: tabular figures so a column of amounts lines up on the decimal, and
   nowrap so "K 12,500.00" never breaks across two lines in a narrow cell. */
.ph-money {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}


/* ==========================================================================
   10. FORMS
   ========================================================================== */

.ph-field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
  margin-bottom: 0.9rem;
}

.ph-field__label {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--ph-text);
}

.ph-field__label .ph-req { color: var(--ph-danger); }

.ph-field__hint {
  font-size: 0.78rem;
  color: var(--ph-text-muted);
}

.ph-field__error {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--ph-danger);
}

.ph-input,
.ph-select,
.ph-textarea {
  display: block;
  width: 100%;
  /* 44px minimum on every control, including selects and date pickers — the
     native widget is what the finger actually has to hit. */
  min-height: var(--ph-tap);
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--ph-border-strong);
  border-radius: var(--ph-radius);
  background: var(--ph-card);
  color: var(--ph-text);
  font-family: inherit;
  font-size: 0.95rem;
  line-height: 1.35;
}

.ph-textarea {
  min-height: calc(var(--ph-tap) * 2);
  resize: vertical;
}

.ph-select {
  /* Room for the native arrow. */
  padding-right: 2rem;
  appearance: none;
  background-image: none;
}

.ph-input:hover,
.ph-select:hover,
.ph-textarea:hover {
  border-color: var(--ph-text-muted);
}

.ph-input:focus,
.ph-select:focus,
.ph-textarea:focus {
  border-color: var(--ph-accent);
}

.ph-input::placeholder,
.ph-textarea::placeholder {
  color: var(--ph-text-muted);
  opacity: 1;
}

.ph-input[disabled],
.ph-select[disabled],
.ph-textarea[disabled],
.ph-input[readonly] {
  background: var(--ph-surface);
  color: var(--ph-text-muted);
  cursor: not-allowed;
}

.ph-input--invalid,
.ph-select--invalid,
.ph-textarea--invalid {
  border-color: var(--ph-danger);
}

.ph-input[type="file"] {
  padding: 0.35rem 0.5rem;
}

/* Checkbox / radio row. The label is the target, so the whole row is 44px tall
   and the control itself is enlarged; a 13px native checkbox is not tappable. */
.ph-check {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  min-height: var(--ph-tap);
  padding: 0.4rem 0;
  font-size: 0.92rem;
  cursor: pointer;
}

.ph-check input[type="checkbox"],
.ph-check input[type="radio"] {
  flex: 0 0 auto;
  width: 1.25rem;
  height: 1.25rem;
  margin: 0.15rem 0 0;
  accent-color: var(--ph-primary);
  cursor: pointer;
}

.ph-check span { min-width: 0; }

/* Inline variant for a single "select all" cell in a table header, where a
   44px-tall label would stretch the row. The control itself keeps its size. */
.ph-check--inline {
  min-height: 0;
  padding: 0;
}

.ph-form-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 1rem;
}

.ph-form-grid > .ph-field { margin-bottom: 0; }

.ph-form-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--ph-border);
}

/* Push the destructive action away from the primary one so a thumb aiming for
   "Save" cannot land on "Delete". */
.ph-form-actions__end { margin-left: auto; }


/* ==========================================================================
   11. TABLES
   ========================================================================== */

.ph-table-wrap {
  width: 100%;
  min-width: 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.ph-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.92rem;
}

.ph-table th,
.ph-table td {
  padding: 0.6rem 0.75rem;
  text-align: left;
  vertical-align: middle;
  border-bottom: 1px solid var(--ph-border);
}

.ph-table thead th {
  background: var(--ph-surface);
  color: var(--ph-text-muted);
  font-size: 0.73rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
}

.ph-table tbody tr:hover { background: var(--ph-surface); }
.ph-table tbody tr:last-child td { border-bottom: 0; }

.ph-table tfoot td {
  background: var(--ph-surface);
  font-weight: 700;
  border-top: 2px solid var(--ph-border);
}

.ph-table--compact th,
.ph-table--compact td { padding: 0.35rem 0.5rem; }

.ph-table__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.ph-table__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}


/* ── The mobile stacked-card pattern ────────────────────────────────────────
   Below 768px every row becomes a card and every cell becomes a labelled line:

       <td data-label="Due date">12 Mar 2026</td>
       →  DUE DATE | 12 Mar 2026

   Hard-won details, none of which are optional:

   1. The label comes from `content: attr(data-label)`, so it lives in the
      MARKUP, next to the value, not in a CSS content table. Rename a column
      header and only the header changes — which is right, because the header
      is hidden here and the data-label is now the only thing naming the field.

   2. The value track is `minmax(0, 1fr)`, NOT `1fr`. `1fr` means
      `minmax(auto, 1fr)`, and `auto` as a minimum refuses to shrink below the
      widest unbreakable content. One long job-card reference (or a nowrap
      .ph-money) then blows the grid past the viewport and the whole page
      scrolls sideways at 375px. `minmax(0, 1fr)` allows the track to shrink and
      `overflow-wrap: anywhere` lets the value break. This is the single most
      important line in this section.

   3. A cell with NO data-label collapses to a single full-width track. Without
      that rule an unlabelled cell (a photo strip, a row of action buttons)
      would still reserve the empty 38% label column.

   4. The cards are separated by a border and a gap, and thead is hidden
      outright — `display: none` also removes it from the accessibility tree,
      which is correct: a screen reader gets the data-label text per cell
      instead, so the column names are never lost.
   ------------------------------------------------------------------------- */

@media (max-width: 767px) {
  .ph-table--stack {
    display: block;
    width: 100%;
  }

  .ph-table--stack thead {
    display: none;
  }

  .ph-table--stack tbody,
  .ph-table--stack tr,
  .ph-table--stack td {
    display: block;
    width: 100%;
  }

  .ph-table--stack tr {
    margin-bottom: 0.75rem;
    padding: 0.35rem 0.8rem;
    border: 1px solid var(--ph-border);
    border-radius: var(--ph-radius);
    background: var(--ph-card);
    box-shadow: var(--ph-shadow-sm);
  }

  .ph-table--stack tr:hover { background: var(--ph-card); }
  .ph-table--stack tr:last-child { margin-bottom: 0; }

  .ph-table--stack td {
    display: grid;
    grid-template-columns: minmax(84px, 38%) minmax(0, 1fr);
    gap: 0.5rem;
    align-items: start;
    padding: 0.5rem 0;
    border: 0;
    border-bottom: 1px solid var(--ph-border);
    min-width: 0;
    overflow-wrap: anywhere;
  }

  .ph-table--stack td:last-child { border-bottom: 0; }

  .ph-table--stack td::before {
    content: attr(data-label);
    min-width: 0;
    color: var(--ph-text-muted);
    font-size: 0.73rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    overflow-wrap: anywhere;
  }

  /* Rule 3 above. */
  .ph-table--stack td:not([data-label]) {
    grid-template-columns: minmax(0, 1fr);
  }

  .ph-table--stack td:not([data-label])::before {
    content: none;
  }

  /* Values are often a flex row (badge + link + timestamp). Let it wrap. */
  .ph-table--stack td > * { max-width: 100%; }

  .ph-table--stack .ph-table__actions {
    justify-content: flex-start;
  }

  .ph-table--stack tfoot,
  .ph-table--stack tfoot tr,
  .ph-table--stack tfoot td {
    display: block;
    width: 100%;
  }

  .ph-table--stack tfoot td {
    display: grid;
    grid-template-columns: minmax(84px, 38%) minmax(0, 1fr);
  }

  /* On a stacked table the wrapper must NOT keep scrolling, or the card shadows
     are clipped and a stray horizontal scrollbar appears. */
  .ph-table-wrap--stack { overflow-x: visible; }
}


/* ==========================================================================
   12. GRID AND STATS
   ========================================================================== */

.ph-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--ph-gap);
}

.ph-grid > * { min-width: 0; }

/* Track minimums are 0 so a wide figure inside a stat cannot widen the track. */
.ph-grid--stats { grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); }

.ph-grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); }

.ph-stat {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius);
  background: var(--ph-card);
  box-shadow: var(--ph-shadow-sm);
  min-width: 0;
}

.ph-stat__label {
  font-size: 0.73rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ph-text-muted);
}

.ph-stat__value {
  font-size: 1.55rem;
  font-weight: 700;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.ph-stat__meta {
  font-size: 0.8rem;
  color: var(--ph-text-muted);
}


/* ==========================================================================
   13. TABS AND PANELS
   ========================================================================== */

.ph-tabs {
  display: flex;
  align-items: stretch;
  gap: 0.25rem;
  border-bottom: 1px solid var(--ph-border);
  /* Many tabs on a phone: scroll the strip rather than wrap it into a block
     that pushes the content off the first screen. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}

.ph-tab {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  flex: 0 0 auto;
  min-height: var(--ph-tap);
  padding: 0.5rem 0.85rem;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--ph-text-muted);
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
}

.ph-tab:hover {
  color: var(--ph-text);
  text-decoration: none;
}

.ph-tab--active {
  color: var(--ph-primary-strong);
  border-bottom-color: var(--ph-accent);
}

.ph-panel {
  display: block;
  padding-top: 1rem;
}

/* REQUIRED, not redundant. .ph-panel above sets display:block, which has the
   same specificity as the user-agent `[hidden] { display: none }` rule and
   therefore wins on source order — leaving a "hidden" panel plainly visible.
   This selector is one class + one attribute, so it wins in turn. */
.ph-panel[hidden] {
  display: none;
}


/* ==========================================================================
   14. KEY/VALUE LISTS
   ========================================================================== */

.ph-kv {
  display: block;
  margin: 0;
}

.ph-kv__row {
  display: grid;
  /* Same overflow discipline as the stacked table: the value track is
     minmax(0, 1fr) so a long value wraps instead of widening the card. */
  grid-template-columns: minmax(7.5rem, 34%) minmax(0, 1fr);
  gap: 0.75rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--ph-border);
}

.ph-kv__row:last-child { border-bottom: 0; }

.ph-kv__k {
  color: var(--ph-text-muted);
  font-size: 0.85rem;
  font-weight: 600;
}

.ph-kv__v {
  min-width: 0;
  overflow-wrap: anywhere;
}


/* ==========================================================================
   15. ALERTS
   ========================================================================== */

.ph-alert {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin-bottom: 0.85rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--ph-border-strong);
  border-left-width: 4px;
  border-radius: var(--ph-radius);
  background: var(--ph-surface);
  /* The message body stays in the normal text colour on purpose. Measuring the
     status colours against their own tints gives danger 5.6:1 and warning
     5.3:1, but success only 3.6:1 and info 4.1:1 — both short of the 4.5:1 that
     alert copy needs. So the STATUS is carried by the 4px left border, the tint
     and a coloured icon (three redundant non-text signals), and the words stay
     at full contrast. Do not "simplify" this by colouring the text. */
  color: var(--ph-text);
  font-size: 0.92rem;
}

.ph-alert:last-child { margin-bottom: 0; }

.ph-alert--success { border-color: var(--ph-success); border-left-color: var(--ph-success); background: var(--ph-success-tint); }
.ph-alert--error   { border-color: var(--ph-danger);  border-left-color: var(--ph-danger);  background: var(--ph-danger-tint); }
.ph-alert--warning { border-color: var(--ph-warning); border-left-color: var(--ph-warning); background: var(--ph-warning-tint); }
.ph-alert--info    { border-color: var(--ph-info);    border-left-color: var(--ph-info);    background: var(--ph-info-tint); }

.ph-alert--success .ph-icon { color: var(--ph-success); }
.ph-alert--error   .ph-icon { color: var(--ph-danger); }
.ph-alert--warning .ph-icon { color: var(--ph-warning); }
.ph-alert--info    .ph-icon { color: var(--ph-info); }

/* A link inside a coloured alert inherits the alert's colour, otherwise it
   reverts to brand orange on a red background and becomes unreadable. */
.ph-alert a {
  color: inherit;
  font-weight: 700;
  text-decoration: underline;
}


/* ==========================================================================
   16. EMPTY STATE AND PROGRESS
   ========================================================================== */

.ph-empty {
  padding: 2.25rem 1rem;
  border: 1px dashed var(--ph-border-strong);
  border-radius: var(--ph-radius);
  background: var(--ph-surface);
  color: var(--ph-text-muted);
  text-align: center;
}

.ph-empty > :last-child { margin-bottom: 0; }

.ph-progress {
  position: relative;
  width: 100%;
  height: 8px;
  border-radius: var(--ph-radius-pill);
  background: var(--ph-border);
  overflow: hidden;
}

.ph-progress__bar {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: var(--ph-primary);
  transition: width 0.3s ease;
}


/* ==========================================================================
   17. TOOLBAR, FILTERS, PAGINATION
   ========================================================================== */

.ph-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: var(--ph-gap);
}

.ph-toolbar__group {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  min-width: 0;
}

.ph-filters {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 0.6rem 0.75rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius);
  background: var(--ph-surface);
  margin-bottom: var(--ph-gap);
}

/* Each filter grows to fill the row but never shrinks below a usable width. */
.ph-filters .ph-field {
  flex: 1 1 11rem;
  margin-bottom: 0;
  min-width: 0;
}

.ph-filters .ph-form-actions {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.ph-pagination {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: var(--ph-gap);
}

.ph-pagination a,
.ph-pagination span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--ph-tap);
  min-height: var(--ph-tap);
  padding: 0 0.6rem;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius);
  background: var(--ph-card);
  color: var(--ph-text);
  font-size: 0.88rem;
  text-decoration: none;
}

.ph-pagination a:hover {
  border-color: var(--ph-accent);
  background: var(--ph-primary-tint);
  text-decoration: none;
}

.ph-pagination .is-current {
  border-color: var(--ph-primary);
  background: var(--ph-primary);
  color: var(--ph-text-invert);
  font-weight: 700;
}

.ph-pagination .is-disabled {
  opacity: 0.5;
  pointer-events: none;
}


/* ==========================================================================
   18. PAGE HEADER
   ========================================================================== */

.ph-page-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: var(--ph-gap);
}

.ph-page-header__title {
  margin: 0;
  font-size: 1.35rem;
  line-height: 1.25;
}

.ph-page-header__sub {
  margin: 0.2rem 0 0;
  color: var(--ph-text-muted);
  font-size: 0.88rem;
}


/* ==========================================================================
   19. ROW STATES AND PRIORITY DOTS
   ========================================================================== */

/* Tint the whole row, and mark the left edge of the first cell with an inset
   bar: colour alone is not a signal for a colour-blind user, and the bar
   survives the switch to stacked cards below 768px. */
.ph-row--overdue > td { background: var(--ph-danger-tint); box-shadow: inset 3px 0 0 var(--ph-danger); }
.ph-row--due-soon > td { background: var(--ph-warning-tint); box-shadow: inset 3px 0 0 var(--ph-warning); }

.ph-priority__dot {
  display: inline-block;
  width: 0.62rem;
  height: 0.62rem;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--ph-text-muted);
  vertical-align: middle;
}

.ph-priority__dot--low    { background: var(--ph-success); }
.ph-priority__dot--medium { background: var(--ph-info); }
.ph-priority__dot--high   { background: var(--ph-warning); }
.ph-priority__dot--urgent { background: var(--ph-danger); }


/* ==========================================================================
   20. TIMELINE AND ACTIVITY FEED
   ========================================================================== */

.ph-timeline {
  list-style: none;
  margin: 0;
  padding: 0 0 0 0.5rem;
  border-left: 2px solid var(--ph-border);
}

.ph-timeline__item {
  position: relative;
  padding: 0 0 1.15rem 1.1rem;
  min-width: 0;
}

.ph-timeline__item:last-child { padding-bottom: 0; }

.ph-timeline__item::before {
  content: "";
  position: absolute;
  left: -1.09rem;
  top: 0.4rem;
  width: 0.62rem;
  height: 0.62rem;
  border-radius: 50%;
  background: var(--ph-primary);
  /* Sits on top of the rail's 2px line; the ring keeps it readable. */
  box-shadow: 0 0 0 2px var(--ph-card);
}

.ph-activity {
  list-style: none;
  margin: 0;
  padding: 0;
}

.ph-activity__item {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--ph-border);
}

.ph-activity__item:last-child { border-bottom: 0; }

.ph-activity__body {
  min-width: 0;
  font-size: 0.9rem;
}

.ph-activity__time {
  display: block;
  font-size: 0.78rem;
  color: var(--ph-text-muted);
}


/* ==========================================================================
   21. DROPDOWN MENUS
   --------------------------------------------------------------------------
   JS-driven (.ph-dropdown--open), with no :focus-within fallback on purpose: a
   :focus-within rule keeps the menu open whenever focus is inside it, and
   closing by clicking the toggle again leaves focus exactly there — the menu
   would spring straight back open and the click-to-close gesture would appear
   broken. Without JS the menu simply never opens; the same destinations remain
   reachable from the sidebar (Profile) and the page body.
   ========================================================================== */

.ph-dropdown {
  position: relative;
}

.ph-dropdown__toggle {
  cursor: pointer;
}

.ph-dropdown__menu {
  display: none;
  position: absolute;
  top: calc(100% + 0.35rem);
  right: 0;
  z-index: 50;
  min-width: 13rem;
  max-width: min(20rem, calc(100vw - 2rem));
  padding: 0.3rem;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius);
  background: var(--ph-card);
  box-shadow: var(--ph-shadow-pop);
}

.ph-dropdown--open > .ph-dropdown__menu {
  display: block;
}

.ph-dropdown__item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  /* 44px, like every other control. */
  min-height: var(--ph-tap);
  padding: 0.4rem 0.6rem;
  border-radius: var(--ph-radius-sm);
  color: var(--ph-text);
  font-size: 0.9rem;
  text-decoration: none;
}

.ph-dropdown__item:hover {
  background: var(--ph-surface);
  text-decoration: none;
}

.ph-dropdown__item--danger { color: var(--ph-danger); }
.ph-dropdown__item--danger:hover { background: var(--ph-danger-tint); }

.ph-dropdown__sep {
  height: 1px;
  margin: 0.3rem 0.2rem;
  background: var(--ph-border);
}

.ph-dropdown__head {
  padding: 0.5rem 0.6rem 0.35rem;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ph-text-muted);
}


/* ==========================================================================
   22. MODAL
   ========================================================================== */

.ph-modal {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: var(--ph-scrim);
  overflow-y: auto;
}

/* The display:flex above would otherwise beat the user-agent [hidden] rule. */
.ph-modal[hidden] { display: none; }

.ph-modal__dialog {
  width: 100%;
  max-width: 34rem;
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
  margin: auto;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius-lg);
  background: var(--ph-card);
  box-shadow: var(--ph-shadow-pop);
}

.ph-modal__dialog > .ph-card__header { border-radius: var(--ph-radius-lg) var(--ph-radius-lg) 0 0; }


/* ==========================================================================
   23. AVATARS, CODE, MUTED TEXT, ICONS
   ========================================================================== */

.ph-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border-radius: 50%;
  color: var(--ph-text-invert);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  user-select: none;
}

.ph-avatar--sm { width: 28px; height: 28px; font-size: 0.72rem; }
.ph-avatar--md { width: 36px; height: 36px; font-size: 0.85rem; }
.ph-avatar--lg { width: 56px; height: 56px; font-size: 1.3rem; }

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

.ph-code {
  padding: 0.1rem 0.35rem;
  border: 1px solid var(--ph-border);
  border-radius: var(--ph-radius-sm);
  background: var(--ph-surface);
  color: var(--ph-text);
  font-family: var(--ph-mono);
  font-size: 0.86em;
  overflow-wrap: anywhere;
}

pre.ph-code,
.ph-code--block {
  display: block;
  padding: 0.75rem 0.9rem;
  overflow-x: auto;
  white-space: pre;
  line-height: 1.45;
}

.ph-strong { font-weight: 700; }

.ph-truncate {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ph-nowrap { white-space: nowrap; }


/* ==========================================================================
   24. RESPONSIVE HELPERS
   --------------------------------------------------------------------------
   Naming is "hide at that breakpoint":
     .ph-hide-sm  hidden BELOW 768px (phone)      — use for dense desktop bits
     .ph-hide-md  hidden AT 768px AND UP          — use for mobile-only controls
     .ph-show-sm  absent at 768px+, present below
   Breakpoint is 768px to match the stacked-table pattern; the two must move
   together or a table will stack while its column headers are still hidden.
   ========================================================================== */

/* Base state first: the phone-only helpers start hidden, and the max-width
   block below turns them back on. Declaring `.ph-show-sm { display: none }`
   AFTER the media query would win on source order (equal specificity) and the
   element would never appear — which is exactly the kind of "tidy-up" that
   silently kills a mobile control. */
.ph-show-sm { display: none; }

@media (max-width: 767px) {
  .ph-hide-sm { display: none !important; }
  /* `revert` hands display back to the user-agent value, so a <button> becomes
     inline-block and a <div> block — which is why this is not `display: block`.
     Every browser that supports :focus-visible or CSS grid supports it; in an
     older one the declaration is dropped and the element stays hidden, which is
     the safe direction to fail. */
  .ph-show-sm { display: revert; }
}

@media (min-width: 768px) {
  .ph-hide-md { display: none !important; }
}


/* ==========================================================================
   25. REDUCED MOTION
   --------------------------------------------------------------------------
   One global rule rather than per-component opt-outs: a new transition added
   anywhere in this file is then covered automatically, and there is no way to
   forget. 0.01ms rather than 0 because some browsers never fire transitionend
   for a 0s duration, which would strand any JS waiting on it.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}


/* ==========================================================================
   SMALL-SCREEN COMPONENT ADJUSTMENTS
   --------------------------------------------------------------------------
   Kept after §25 so these read as layout overrides on top of the finished
   component definitions above.
   ========================================================================== */

@media (max-width: 767px) {
  /* Below 768px the sidebar is an off-canvas drawer. It is translated rather
     than moved with `left`, because transform is composited and stays smooth on
     a cheap phone, and it keeps the element out of the layout only visually —
     the box is still positioned, so it cannot be scrolled to accidentally. */
  .ph-sidebar {
    width: min(84vw, 300px);
    transform: translateX(-100%);
    transition: transform 0.2s ease;
  }

  .ph-sidebar--open {
    transform: translateX(0);
    box-shadow: var(--ph-shadow-pop);
  }

  .ph-shell__main {
    margin-left: 0;
  }

  /* The drawer handle is pinned to the viewport (see §06), and it sits in the
     topbar's band. Reserve its width so the page title never runs underneath
     it. Keep this in step with --ph-tap. */
  .ph-topbar {
    padding-left: calc(var(--ph-tap) + 0.85rem);
  }

  /* Search drops to its own full-width row instead of being squeezed. */
  .ph-search {
    order: 3;
    width: 100%;
  }

  .ph-search__input {
    width: 100%;
    min-width: 0;
  }

  /* The name/role block is the first thing to go when space runs out; the
     avatar alone still identifies the signed-in user. */
  .ph-user__meta {
    max-width: 6.5rem;
  }

  .ph-page-header {
    align-items: flex-start;
  }

  .ph-table__actions .ph-btn {
    flex: 1 1 auto;
  }
}

@media (min-width: 768px) {
  .ph-content {
    padding-top: 1.25rem;
  }
}
