/* ==========================================================================
   Garage40 - Shop Management
   Design system: red / black / white, workshop-industrial feel.
   One shared stylesheet for every page - see includes/header.php.
   ========================================================================== */

:root {
  --c-black: #111111;
  --c-black-soft: #1e1e1e;
  --c-red: #d81f26;
  --c-red-dark: #a5151a;
  --c-amber: #b8860b; /* "waiting on someone else, nothing urgent for you
    yet" - a pending customer approval, distinct from the red used for an
    actual job in progress or a declined decision */
  --c-green: #205e20; /* an approved decision - same green flash-message
    already uses for a plain success state */
  --c-white: #ffffff;
  --c-bg: #f2f1ef;
  --c-panel: #ffffff;
  --c-border: #e2e0dc;
  --c-text: #17171a;
  --c-text-muted: #6b6b6f;
  --radius: 3px;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 4px 14px rgba(0, 0, 0, 0.05);
  --font: Verdana, Geneva, sans-serif;
  --header-h: 60px;
}

/* ---- reset / base ---- */
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; font-size: 90%; } /* 10% smaller app-wide - every font-size below is in rem, so this scales all of them at once */
body {
  font-family: var(--font);
  color: var(--c-text);
  background: var(--c-bg);
  margin: 0;
  line-height: 1.45;
}
h1, h2, h3, h4, h5 {
  font-weight: 800;
  letter-spacing: -0.01em;
  margin: 0 0 0.6em 0;
  color: var(--c-black);
}
h1 { font-size: 1.7rem; text-transform: uppercase; letter-spacing: 0.01em; }
/* A page's <h1> with one action button sitting right beside it (e.g.
   job_edit.php's Delete job) instead of buried further down the page -
   wraps to its own line under a narrow title rather than squeezing, since
   the title's own uppercase letter-spacing already eats width fast. */
.page-title-row { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.page-title-row h1 { margin-bottom: 0; }
h3 { font-size: 1.05rem; text-transform: uppercase; letter-spacing: 0.03em; }
/* A labeled subsection within a form - one visual step down from an h3
   card title (no red square, smaller), but still uppercase like every
   other heading level rather than left at h4's own plain sentence case.
   The shared h1-h5 rule above only ever gives a heading bottom margin
   (0.6em) - none of them push away from whatever sits right before, so
   one that follows hard after a field (rather than starting a whole new
   card) needs its own top margin or it visually collides with it. */
.form-section-heading { margin-top: 1.5rem; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.95rem; }
h3::before {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  background: var(--c-red);
  margin-right: 8px;
  transform: translateY(-1px);
}
p { margin: 0 0 0.8em 0; }
a { color: var(--c-red); text-decoration: none; }
a:hover { text-decoration: underline; }
hr { display: none; } /* replaced by the header's hazard stripe */

/* ---- layout shell ---- */
.site-main {
  max-width: 1100px;
  margin: 0 auto;
  padding: 1.75rem 1.25rem 4rem;
}

/* ---- header / nav ---- */
.site-header {
  background: var(--c-black);
  position: sticky;
  top: 0;
  z-index: 100;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
}
.hazard-stripe {
  height: 5px;
  /* A plain (non-repeating) gradient sized to exactly one diagonal tile,
     tiled by background-size/background-repeat, rather than the infinite
     pattern repeating-linear-gradient itself generates - a fixed tile is
     what makes animating background-position by exactly one tile-width a
     guaranteed-seamless "marching tape" loop, with no visible seam/jump. */
  background-image: linear-gradient(
    135deg,
    var(--c-red) 25%, var(--c-black) 25%, var(--c-black) 50%,
    var(--c-red) 50%, var(--c-red) 75%, var(--c-black) 75%
  );
  background-size: 28px 28px;
  animation: hazard-march 2s linear infinite;
}
@keyframes hazard-march {
  from { background-position: 0 0; }
  to { background-position: 28px 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hazard-stripe { animation: none; }
  .auth-stripe { animation: none; }
}
.header-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0 1.25rem;
  height: var(--header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  position: relative; /* lets .nav-links center itself against this bar,
    not just against whatever's left over next to the logo */
}
.brand { display: flex; align-items: center; height: 100%; flex-shrink: 0; position: relative; }
/* Deliberately taller than --header-h (60px) - .header-inner doesn't
   clip its children, so a logo bigger than the bar itself spills past
   its edge instead of being squeezed to fit inside it, like a badge
   sitting on top of the header rather than embedded in it. align-self:
   flex-start (not the centered default) pushes all of that overflow
   down to the bottom edge only - centered, the top half of the overflow
   landed on the hazard stripe above the header, which read as broken/
   overlapping rather than intentional. */
/* position+z-index so the logo paints above .brand::after (the black
   connector tab below the header) instead of behind it - an absolutely
   positioned element paints above plain in-flow content by default
   regardless of DOM order, which was hiding the bottom of the logo
   (the part overlapping the tab) behind that opaque black rectangle. */
/* border-color is var(--c-black), not white - filter:invert(1) below
   flips the whole rendered element, border included, so a border
   specified as black is what actually paints white once inverted (the
   same reason the logo's own black artwork paints white in the first
   place). A border literally set to white here would invert to black and
   vanish against the header's own black background. */
/* margin-top:3px + height 3px shorter than before (69px, was 72px) - a
   small deliberate gap under the hazard stripe instead of sitting flush
   against it, without changing where the logo's own bottom edge lands
   (top + height always adds up to the same 72px total - still exactly
   where .brand::after's tab below expects it, see that rule's own
   comment for how the two line up). */
/* background: #fff (inverts to black, same compensation as the border
   just above) - logo.png's own background is transparent, which is
   invisible at rest since the header's own black already shows through
   it everywhere the logo overlaps the header (plus .brand::after's tab
   below, for the small bit that doesn't). Popping the image 1.3x on
   hover grows it well past both of those - a 66px-tall logo scaled up
   is ~86px tall, versus the 17px tall tab below it - so without its own
   real backing, the newly-exposed overflow showed the page's light
   background (or the hazard stripe above) straight through the
   transparent parts of the artwork instead of black. */
.brand img { height: 66px; width: auto; display: block; filter: invert(1); align-self: flex-start; margin-top: 6px; position: relative; z-index: 1; border: 1px solid var(--c-black); border-radius: 4px; background: #fff; box-sizing: border-box; transition: transform 0.02s ease, box-shadow 0.02s ease; }
/* Pulses in a loop the whole time it's hovered (zoom in, zoom out,
   repeat) instead of popping once and holding - an @keyframes animation
   rather than a plain :hover transition, since a transition only ever
   animates towards one end state and stops there. White glow, not red
   like the main nav icons, so it reads against the header as a bright
   halo rather than competing with the logo's own black/white artwork.
   Still color-compensated for filter: invert(1) above (same reason the
   border above is specified as black, not white) - a box-shadow given
   as plain black is what actually paints white once the whole element
   gets inverted. The logo's own background (transparent, showing the
   header's black through it otherwise) is backed by the background:#fff
   above regardless of how far this scales it, for the same reason. */
@keyframes brand-logo-pop-loop {
  0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 0, 0, 0), 0 0 0 0 rgba(0, 0, 0, 0); }
  50% { transform: scale(1.15); box-shadow: 0 0 0 2px #000, 0 0 32px 10px rgba(0, 0, 0, 0.7); }
}
.brand:hover img { animation: brand-logo-pop-loop 0.7s ease-in-out infinite; position: relative; z-index: 2; }
@media (prefers-reduced-motion: reduce) {
  .brand:hover img { animation: none; }
}
/* A plain black rectangle (same color as the header) hanging off the
   header, spanning the logo's own width - a small visual "connector"
   reaching down from the header into the page below it, merged with
   the header rather than reading as a separate floating block: flush
   against the header's own bottom edge (no gap), same fill color, and
   the header's own box-shadow (0 2px 10px) repeated here too, so the
   drop-shadow follows this shape's actual outline instead of cutting
   straight across it right where the header's plain rectangle ends.
   width:100% (of .brand, whose only child is the logo image) tracks
   the logo's actual rendered width directly, rather than a guessed
   pixel number that would drift out of sync if the logo itself ever
   changes size again.
   top sits 2px *above* the header's own bottom edge (not flush at
   top:100%) and height is 2px taller to match - these are two separate
   shapes each casting their own identical box-shadow, and at some
   viewport widths (fractional logo width from height:72px scaling to a
   non-integer px value, e.g. 768px wide) the two shadows didn't quite
   blend at the exact seam between them, showing as a faint hairline.
   Overlapping this rectangle 2px up into the header's own solid black
   body hides that seam inside plain fill instead of right on a shadow
   boundary - purely a rendering nudge, doesn't change how much of the
   tab is actually visible below the header. */
/* top raised from calc(100% - 2px) to calc(100% - 5px) - the logo's own
   margin-top:3px (added since this was last tuned) shortened it by the
   same 3px, so the tab's old bottom edge started poking out 3px past the
   logo's new, shorter bottom edge instead of staying hidden behind it.
   Raised by exactly that 3px so the two bottom edges line up again - the
   tab's fill is now fully covered by the logo at every point, only its
   box-shadow (wider than the shape itself) still shows past the edge. */
.brand::after {
  content: "";
  position: absolute;
  left: 0;
  top: calc(100% - 5px);
  width: 100%;
  height: 17px;
  background: var(--c-black);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
  border-radius: 0 0 6px 6px;
}

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: var(--radius);
  cursor: pointer;
  flex-shrink: 0;
}
/* Classic 3-bar hamburger, drawn in CSS (no icon font in this project).
   Each bar needs an explicit width - a bare display:block span with no
   content and no width collapses to 0, which is why the very first version
   of this icon was invisible. Morphs into an X when the menu is open
   (aria-expanded="true") - the button already tracks that state. */
.nav-toggle .nav-hamburger { display: flex; flex-direction: column; justify-content: center; gap: 5px; width: 20px; }
.nav-toggle .nav-hamburger span {
  display: block;
  width: 100%;
  height: 2px;
  background: var(--c-white);
  transition: transform 0.15s ease, opacity 0.15s ease;
}
.nav-toggle[aria-expanded="true"] .nav-hamburger span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-hamburger span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-hamburger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.main-nav {
  display: flex;
  align-items: center;
  gap: 1.4rem;
  flex-wrap: wrap;
}
/* Centered in the header bar itself (not just squeezed between the logo
   and the lang/user group) - taken out of .main-nav's own flex flow and
   positioned against .header-inner instead, so the lang-switch/user-switch
   pair (still flowing normally, pushed to the right edge below) can't drag
   it off-center. */
.main-nav .nav-links {
  display: flex; align-items: center; gap: 1.3rem; flex-wrap: wrap;
  position: absolute; left: 50%; top: 0; bottom: 0; transform: translateX(-50%);
}
.main-nav a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: rgba(255, 255, 255, 0.82);
  text-decoration: none;
  padding: 6px 4px;
  border-bottom: 2px solid transparent;
  border-radius: 4px;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent; /* kills the system-default
    blue/teal flash a touch device otherwise paints over the whole link on
    tap - nothing in this file ever asked for that color, it's a browser
    default most sites suppress the same way. */
  transition: transform 0.02s ease, box-shadow 0.02s ease, color 0.02s ease;
}
/* Hidden on the desktop icon-only bar - brought back for the mobile
   dropdown below (max-width: 820px), which has the width to spare. */
.nav-label { display: none; }
/* Icon-only now - sized up from the icon+label version (was 15px) since
   the icon alone is the whole link, not a small mark next to a caption -
   needs to read clearly, and be a comfortable tap target, on its own. */
.main-nav a svg { width: 21px; height: 21px; flex-shrink: 0; }
/* Same pop + glow language as .cal-mini-day/.dash-hour-chip's hover
   (transform: scale + a solid ring/soft-halo box-shadow pair) - just the
   motion/glow half of that recipe, not its red-gradient-fill/color-swap
   half: the nav's own colors (icon opacity, active's red underline) stay
   exactly as they already were, only lifted and outlined on hover. */
.main-nav a:hover { color: var(--c-white); transform: scale(1.3); box-shadow: 0 0 0 2px var(--c-red), 0 0 32px 10px rgba(216, 31, 38, 0.7); position: relative; z-index: 2; }
@media (prefers-reduced-motion: reduce) {
  .main-nav a:hover { transform: none; }
}
.main-nav a.active { color: var(--c-white); border-bottom-color: var(--c-red); }
.nav-badge {
  display: inline-block; min-width: 16px; margin-left: 5px; padding: 1px 5px;
  border-radius: 8px; background: var(--c-red); color: var(--c-white);
  font-size: 0.68rem; font-weight: 800; text-align: center; vertical-align: 2px;
}
/* Jobs' own badge (pending approvals still awaiting a customer) - amber,
   not the red .nav-badge already uses for Mail's unread count. Red reads
   as "something needs you"; this is "someone else hasn't acted yet", a
   different, lower-urgency kind of standing reminder. */
.nav-badge-pending { background: var(--c-amber); }
/* jobs.php's own per-row badge (same two classes above give it the amber
   pill) - .nav-badge's own width/padding is tuned for a 1-2 digit count,
   too tight for an actual word like "Pending", so this widens it back out. */
.job-pending-badge { min-width: 0; padding: 2px 7px; text-transform: uppercase; letter-spacing: 0.02em; }

.lang-switch { display: flex; gap: 0.6rem; margin-left: auto; }
.lang-switch a {
  color: rgba(255, 255, 255, 0.55);
  font-weight: 700;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.lang-switch a.active { color: var(--c-red); }

.user-switch { display: flex; align-items: center; gap: 0.6rem; padding-left: 0.6rem; border-left: 1px solid rgba(255, 255, 255, 0.2); }
.current-username { color: rgba(255, 255, 255, 0.7); font-weight: 700; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; white-space: nowrap; }
.user-switch a { color: rgba(255, 255, 255, 0.55); font-weight: 700; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; }
.user-switch a:hover { color: var(--c-red); }
/* Log out is an icon-only link now (was plain text) - the SVG has no
   intrinsic size of its own, so it needs an explicit box or it falls back
   to the browser's default ~300x150 replaced-element size. display:flex
   centers the stroke paths inside that box regardless of the icon's own
   viewBox proportions. */
.icon-logout { display: flex; align-items: center; }
.icon-logout svg { width: 18px; height: 18px; }

@media (max-width: 820px) {
  .nav-toggle { display: flex; }
  .main-nav {
    display: none;
    position: absolute;
    top: var(--header-h);
    left: 0;
    right: 0;
    background: var(--c-black-soft);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0.5rem 1.25rem 1rem;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
  }
  .main-nav.open { display: flex; }
  .main-nav .nav-links {
    flex-direction: column; align-items: stretch; gap: 0;
    position: static; transform: none; /* undo the desktop bar's centering -
      back in the normal stacked flow of the dropdown */
  }
  .main-nav a {
    justify-content: flex-start; gap: 12px;
    padding: 0.65rem 0; border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  }
  .main-nav a.active { border-bottom: 1px solid rgba(255, 255, 255, 0.08); }
  .nav-label {
    display: inline; font-weight: 700; font-size: 0.82rem;
    text-transform: uppercase; letter-spacing: 0.04em;
  }
  .lang-switch { margin: 0.75rem 0 0; }
  .user-switch { margin: 0.5rem 0 0; padding-left: 0; border-left: none; }
}

/* ---- flash messages ---- */
.flash {
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  margin: 0 0 1rem 0;
  font-weight: 600;
  font-size: 0.9rem;
}
.flash-error { background: #fdecec; border: 1px solid var(--c-red); color: var(--c-red-dark); }
.flash-message { background: #eef7ee; border: 1px solid #2a7a2a; color: #205e20; }

/* ---- approval decided alert bar (header.php, every page) ---- */
/* Full-width, sits directly under .site-header, above .site-main - the
   "shows on every page" part is just that it's rendered from header.php
   like the nav itself, not tied to any one page. */
.approval-alert-bar { background: var(--c-red); }
.approval-alert-btn {
  display: flex; align-items: center; justify-content: center; gap: 0.75rem;
  width: 100%; max-width: 1100px; margin: 0 auto; padding: 0.6rem 1.25rem;
  background: none; border: none; border-radius: 0; clip-path: none;
  color: var(--c-white); font-weight: 700; font-size: 0.88rem;
  text-transform: none; letter-spacing: normal; cursor: pointer;
  animation: approval-alert-blink 1.4s ease-in-out infinite;
}
.approval-alert-btn:hover { background: rgba(0, 0, 0, 0.1); }
.approval-alert-caret { font-size: 0.7rem; line-height: 1; opacity: 0.85; }
@keyframes approval-alert-blink {
  0%, 100% { background-color: var(--c-red); color: var(--c-white); }
  50% { background-color: var(--c-red-dark); color: #ffd54a; }
}
@media (prefers-reduced-motion: reduce) {
  .approval-alert-btn { animation: none; }
}
/* The dropdown revealed by clicking the bar - opening/closing it never
   changes what's blinking (that's driven purely by job_approvals.seen_at,
   server-side), it's just a way to see which job each decision is for
   before acting on it. */
.approval-alert-panel {
  max-width: 1100px; margin: 0 auto; background: var(--c-panel);
  border-bottom: 1px solid var(--c-border); box-shadow: var(--shadow);
}
.approval-alert-item {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.65rem 1.25rem; border-top: 1px solid var(--c-border);
}
.approval-alert-item-link { color: var(--c-text); font-weight: 600; font-size: 0.9rem; flex: 1; min-width: 0; }
.approval-alert-item-link:hover { color: var(--c-red); }
.approval-alert-item-status {
  display: inline-block; font-weight: 800; font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: 0.03em; margin-right: 0.5rem;
}
.approval-alert-item-status-approved { color: var(--c-green); }
.approval-alert-item-status-declined { color: var(--c-red-dark); }

/* The real "Send"/"Reply"/"Send approval request" button every
   previewEmailForm()-driven form (footer.php) reveals in place of Preview
   once staff have actually looked at what's about to go out - this is the
   point of no return (Edit is the only way back), so it blinks the same
   way the approval alert bar does rather than looking like just another
   button in the row. Starts hidden (style="display:none" until JS reveals
   it), so this never blinks before there's actually something to send. */
.email-send-btn { animation: approval-alert-blink 1.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .email-send-btn { animation: none; }
}

/* Same blink, generic name - job_edit.php's Save button switches to this
   once the form's been touched (see the inline script by jobDetailsForm),
   same "needs attention, don't lose this" signal as the two rules above
   but not tied to email at all. */
.btn-blink { animation: approval-alert-blink 1.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .btn-blink { animation: none; }
}

/* ---- cards ---- */
.card {
  background: var(--c-panel);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1.25rem;
  margin-bottom: 1.5rem;
}
.card-row { display: flex; gap: 1.25rem; align-items: flex-start; }
.card-row .card { flex: 1; min-width: 0; }
@media (max-width: 760px) {
  .card-row { flex-direction: column; align-items: stretch; }
}

/* help.php - the numbered getting-started guide and the FAQ below it. */
/* list-style:none, not a <ul> - each step's own title already carries its
   number ("1. Add the customer"), so a native marker would just double it
   up right next to its own text. */
.help-steps { margin: 0; padding-left: 0; list-style: none; }
.help-steps li { margin-bottom: 1rem; }
.help-steps li:last-child { margin-bottom: 0; }
.help-steps li strong { display: block; margin-bottom: 0.2rem; }
.help-steps li p { margin: 0; color: var(--c-text-muted); }
.help-faq { margin: 0.75rem 0 0; }
.help-faq dt { font-weight: 700; margin-top: 1rem; }
.help-faq dt:first-child { margin-top: 0; }
.help-faq dd { margin: 0.2rem 0 0; color: var(--c-text-muted); }

/* Accordion cards (settings.php) - collapsed to just the title until
   clicked. .settings-card-collapse animates via grid-template-rows
   (0fr collapsed, 1fr expanded) rather than max-height: transitions
   smoothly without JS ever having to measure the content's real height
   first, and adapts automatically if a translation makes a card taller
   or shorter than English. */
.settings-card-toggle { cursor: pointer; user-select: none; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
/* The clickable row wraps the real <h3> (not the other way around) so its
   own ::before red-square bullet stays part of the heading's normal inline
   flow instead of becoming a third flex item that fights the title for
   space - h3's own default bottom margin still needs resetting here since
   it's now sitting in a tight flex row, not a standalone block. */
.settings-card-toggle h3 { margin: 0; }
.settings-card-toggle:focus-visible { outline: 2px solid var(--c-red); outline-offset: 3px; }
.settings-card-chevron { display: inline-block; color: var(--c-text-muted); font-size: 0.85em; transition: transform 0.25s ease; }
.settings-card.expanded .settings-card-chevron { transform: rotate(180deg); }
.settings-card-collapse { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.3s ease; }
.settings-card.expanded .settings-card-collapse { grid-template-rows: 1fr; }
.settings-card-collapse-inner { overflow: hidden; min-height: 0; }
.settings-card-actions { display: flex; gap: 0.75rem; margin-top: 1.25rem; }

/* ---- forms ---- */
.field { margin-bottom: 0.85rem; }
.field label {
  display: block;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-text-muted);
  margin-bottom: 3px;
}
input, select, textarea {
  font-family: var(--font);
  font-size: 0.95rem;
  padding: 0.5rem 0.6rem;
  width: 280px;
  max-width: 100%;
  border: 1px solid #cfcdc8;
  border-radius: var(--radius);
  background: var(--c-white);
  color: var(--c-text);
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--c-red);
  box-shadow: 0 0 0 3px rgba(216, 31, 38, 0.15);
}
input[type="checkbox"] { width: auto; }

/* Searchable vehicle picker (vehicle_picker.js) - type a plate or an
   owner/company name, pick a match from a dropdown anchored under the
   text field, instead of scrolling a giant native <select>. */
.vehicle-picker { position: relative; }
.vehicle-picker-results {
  display: none;
  position: absolute;
  z-index: 20;
  top: 100%;
  left: 0;
  right: 0;
  max-width: 280px;
  max-height: 220px;
  overflow-y: auto;
  background: var(--c-white);
  border: 1px solid var(--c-border);
  border-top: none;
  border-radius: 0 0 var(--radius) var(--radius);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
}
.vehicle-picker-row { padding: 0.4rem 0.6rem; font-size: 0.85rem; cursor: pointer; }
.vehicle-picker-row:hover { background: var(--c-panel); color: var(--c-red); }
.vehicle-picker-empty { padding: 0.4rem 0.6rem; font-size: 0.85rem; color: var(--c-text-muted); }
@media (max-width: 600px) {
  input, select, textarea { width: 100%; }
}

/* Status on/off switch (job_edit.php) - a proper checkbox for real (keyboard
   operable, hideable via sr-only patterns if ever needed), just painted as a
   sliding toggle instead of a native checkbox square. Track colors match the
   status-open/status-done text colors used everywhere else (jobs.php's
   table, calendar job labels, ...) - red while open, black once done. */
/* .switch IS a <label> (so clicking the text/track toggles the checkbox
   too), sitting inside a .field - ".field label { display:block; ... }"
   has higher specificity than a bare ".switch" and would otherwise stomp
   the flex layout and paint it with the same small uppercase caption
   style as every other field's plain text label. .field .switch beats it
   and explicitly resets the caption properties it doesn't want. */
.field .switch {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  width: auto;
  margin-bottom: 0;
  text-transform: none;
  font-weight: 400;
  letter-spacing: normal;
  color: var(--c-text);
}
.switch input[type="checkbox"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.switch .switch-track {
  position: relative;
  width: 44px;
  height: 24px;
  flex-shrink: 0;
  background: var(--c-red);
  border-radius: 999px;
  transition: background 0.15s ease;
}
.switch .switch-track::before {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  background: var(--c-white);
  border-radius: 50%;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
  transition: transform 0.15s ease;
}
.switch input:checked + .switch-track { background: var(--c-black); }
.switch input:checked + .switch-track::before { transform: translateX(20px); }
.switch input:focus-visible + .switch-track { box-shadow: 0 0 0 3px rgba(216, 31, 38, 0.25); }
.switch .switch-label-text { font-weight: 700; text-transform: uppercase; font-size: 0.85rem; }

/* job_edit.php's job-status control - a real 3-way choice (booked/under
   work/done), not a done/not-done toggle, so it's three connected segments
   rather than .switch above. Plain radios + <label for=""> (not JS) - each
   input is visually hidden but still keyboard/screen-reader operable, and
   the checked one's following label picks up its own color via the
   adjacent-sibling selectors below (input and label are siblings, not
   nested, specifically so this works with a plain CSS selector). Icon-only
   (title/aria-label on the label carry the real name) - same real feather-
   style SVGs as the nav/mailbox icons (job_status_icon(), helpers.php),
   not text glyphs. */
.status-switch { display: flex; width: 100%; border: 1px solid var(--c-border); border-radius: var(--radius); overflow: hidden; }
.status-switch input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
  outline: none; /* the browser's own default focus ring drawn around this
    near-invisible 1px input showed through as a stray line across the top
    of whichever segment it sits against - the custom box-shadow indicator
    below (:focus-visible) replaces it, so keyboard focus is still visible. */
}
/* .field label (the "STATUS" caption above this whole control, ".field
   label { display:block; ... }") has higher specificity than a bare
   ".status-switch-option" and was silently overriding display:flex back to
   block - every icon+date/time pair was actually laying out as plain
   inline content (no real align-items/justify-content/gap at all, despite
   all three being set), which is why nothing was ever truly centered no
   matter how the gap/padding numbers were tuned. ".field .status-switch-
   option" beats it on specificity, same fix .field .switch already applies
   for the done/not-done toggle above - and resets the caption properties
   (uppercase/letter-spacing/margin-bottom) .field label also hands down
   that this icon-only control never wanted either. */
.field .status-switch-option {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  padding: 0.5rem 0.3rem;
  margin-bottom: 0;
  text-transform: none;
  letter-spacing: normal;
  color: var(--c-text-muted);
  background: var(--c-white);
  border-right: 1px solid var(--c-border);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, flex-grow 0.15s ease;
}
.status-switch-option:last-of-type { border-right: none; }
.status-switch-option svg { width: 18px; height: 18px; flex-shrink: 0; }
/* The date/time sits in every segment's markup (job_edit.php) but only
   ever shows in the one that's currently checked - same growing-flex trick
   as the color change just below, driven by the same radio, no JS.
   text-align:center matters once it wraps to two lines (date/time) - left
   alignment would read as lopsided next to a centered icon. */
.status-switch-time { display: none; font-size: 0.6rem; font-weight: 700; line-height: 1.15; white-space: normal; text-align: center; }
.status-switch input:checked + .status-switch-option { flex: 2.4; gap: 1rem; padding-left: 0.6rem; padding-right: 0.6rem; }
.status-switch input:checked + .status-switch-option .status-switch-time { display: inline; }
.status-switch input:checked + .status-switch-booked { background: var(--c-amber); color: var(--c-white); }
.status-switch input:checked + .status-switch-open { background: var(--c-red); color: var(--c-white); }
.status-switch input:checked + .status-switch-done { background: var(--c-green); color: var(--c-white); }
.status-switch input:focus-visible + .status-switch-option { box-shadow: inset 0 0 0 2px var(--c-black); }
/* The active segment's own icon rocks side to side gently - reads as "this
   one's alive/working" (a wrench being turned, a clock ticking) rather
   than the flatter scale-pulse this replaced. Rotates around its own
   center, so it doesn't nudge the date/time text next to it around. */
.status-switch input:checked + .status-switch-option svg { animation: status-switch-wiggle 1.6s ease-in-out infinite; transform-origin: center; }
@keyframes status-switch-wiggle {
  0%, 100% { transform: rotate(0deg); }
  25% { transform: rotate(-14deg); }
  75% { transform: rotate(14deg); }
}
@media (prefers-reduced-motion: reduce) {
  .status-switch input:checked + .status-switch-option svg { animation: none; }
}

/* A form whose fields should fill the available card width instead of
   sitting in a single narrow ~280px column with a lot of dead space next
   to it (e.g. jobs.php's Add Job form, which lives in a wide card).
   auto-fit + minmax lets fields flow into as many columns as comfortably
   fit and reflow down to one on a narrow screen, no media query needed.
   .field.grow (Vehicle, Description, Notes, ...) claims 2 columns when
   there's room, since those tend to hold longer values than a single
   date/time field. */
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.85rem 1.25rem; }
/* Opts a form out of auto-fit's side-by-side columns entirely, always one
   field per row regardless of card width - for a form like job_edit.php's
   "Send email" where To/Subject/Message crammed onto one row read as three
   unrelated short fields instead of the single top-to-bottom message they
   actually are. */
.form-grid.form-grid-stacked { grid-template-columns: 1fr; }
.form-grid.form-grid-stacked .field.grow { grid-column: span 1; }
.form-grid .field { margin-bottom: 0; }
.form-grid .field.grow { grid-column: span 2; }
/* For a field that must always be full width, not just "wider than a plain
   one" - .grow's span:2 only reads as full-width when nothing else shares
   its row; a lone .grow field (nothing left over to pair with it, e.g. a
   Notes textarea trailing after several plain fields) ends up stuck at
   half the row's width with empty space beside it instead. grid-column:
   1 / -1 spans from the first line to the last one no matter how many
   columns auto-fit actually computed, so this never happens. */
.form-grid .field.field-full { grid-column: 1 / -1; }
.form-grid input, .form-grid select, .form-grid textarea { width: 100%; }
.form-grid textarea { resize: vertical; }
/* VIN and license plate fields (customers.php, index.php, vehicle_edit.php)
   - shown uppercase as soon as it's typed, matching the uppercase the
   server always saves it as (normalize_plate_or_vin(), helpers.php).
   Purely visual - text-transform doesn't touch the field's actual value,
   so the real normalization still has to happen server-side too. */
.uppercase-input { text-transform: uppercase; }
@media (max-width: 480px) {
  /* A .field.grow's span:2 forces the grid to actually create 2 column
     tracks even when the container is too narrow for even one 200px
     minmax track plus gap - Grid shrinks both tracks to fit rather than
     collapsing to one, so a field with no grow class (Scheduled time,
     Cost) ends up squeezed to a sliver next to it instead of stacking
     full-width like everything else. Force a single column outright
     below this width instead of relying on auto-fit's edge case. */
  .form-grid { grid-template-columns: 1fr; }
  .form-grid .field.grow { grid-column: span 1; }
}

/* ---- buttons ---- */
/* Black by default - only an "add" or "delete" action gets .btn-red (see
   below). .btn is the same look applied to a plain <a> link (e.g. a table
   row's "edit"/"view" action) so it reads as a button instead of a bare
   underlined link. */
button, .btn {
  display: inline-block;
  background: var(--c-black);
  color: var(--c-white);
  border: none;
  padding: 0.55rem 1.1rem;
  font-family: var(--font);
  font-weight: 700;
  font-size: 0.82rem;
  /* A <button> doesn't inherit the page's line-height (1.45, set on body) -
     browsers give form controls their own UA-default "normal" line-height
     instead, unrelated to the rest of the page. An <a class="btn"> link
     *does* inherit it, since anchors get no such special-casing - so with
     the same padding and font-size, a real <button> and a .btn-styled <a>
     ended up ~2px different in height (verified via getBoundingClientRect:
     34.6px vs 36.6px) any time the two sat side by side (e.g. Save changes
     / Cancel). Pinning line-height explicitly here makes every button and
     button-styled link compute the exact same height, full stop. */
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  text-decoration: none;
  border-radius: var(--radius);
  cursor: pointer;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 9px 100%, 0 calc(100% - 9px));
}
button:hover, .btn:hover { background: var(--c-black-soft); text-decoration: none; }
button.btn-red, .btn-red { background: var(--c-red); }
button.btn-red:hover, .btn-red:hover { background: var(--c-red-dark); }
form.inline { display: inline; }
/* confirmDelete() (footer.php) puts a delete form into this state while
   it's armed, waiting for the confirming second click - replaces the old
   native confirm() popup, which some browser setups silently suppress with
   no dialog shown at all. Bounding the form's own width and stacking the
   note under the button (rather than beside it) keeps this readable
   wherever the form happens to sit - a table's shrink-to-fit actions
   column left it almost no width to work with, so the note used to wrap
   into a ragged, broken-looking line. */
form.inline.confirm-active { display: inline-block; max-width: 220px; text-align: right; vertical-align: middle; }
.confirm-delete-note { display: block; width: 100%; margin-top: 3px; color: var(--c-red); font-size: 0.78rem; font-weight: 700; white-space: normal; }
/* Type-the-username delete guard (confirmDeleteUser in footer.php) - an
   extra text input inside the same .confirm-delete-note wrapper above, for
   the one delete button (admin.php's user list) where a stray second click
   is a bigger mistake than usual. */
.confirm-delete-input { display: block; width: 100%; box-sizing: border-box; margin-top: 3px; padding: 3px 6px; border: 1px solid var(--c-red); border-radius: 4px; font-size: 0.85rem; font-weight: 400; color: var(--c-text); }
.confirm-delete-input.confirm-input-mismatch { border-color: var(--c-red); background: #fdecea; animation: confirm-input-shake 0.3s; }
.confirm-mismatch-msg { display: block; margin-top: 3px; }
.confirm-mismatch-msg[hidden] { display: none; }
@keyframes confirm-input-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}
/* Wraps a table row's action buttons (edit/delete, view/mark-done, etc.) so
   they sit snug side by side in one cell instead of each getting its own
   column - a class beats the generic mobile "table td" flex rule below, so
   this stays a tight row in the stacked-card layout too. flex-wrap stays
   nowrap on purpose: with .col-actions' width:1% shrink-to-fit below, a
   wrappable flex row reports a much smaller min-content width (as little as
   its single widest button) to the table layout, so the column gets sized
   too narrow and the buttons wrap to two lines instead of sitting side by
   side - the opposite of what this is for. */
.td-actions { display: flex; gap: 8px; align-items: center; flex-wrap: nowrap; justify-content: flex-end; }
/* jobs.php's actions-row mixes plain text (who opened the job) in with the
   buttons - unlike every other .td-actions use, which is buttons only, so
   the shared nowrap rule above (deliberately rigid so button labels of very
   different lengths across EN/ET/FI don't wrap to two lines) would just
   force this row to overflow instead once a longer username is added.
   Scoped to jobs.php only - wrap here, keep every other .td-actions row
   exactly as rigid as before. */
.jobs-table .actions-row .td-actions { flex-wrap: wrap; }
.opened-by-note { font-size: 0.8rem; color: var(--c-text-muted); margin-right: auto; }
.job-people { font-size: 0.85rem; color: var(--c-text-muted); margin: 0.3rem 0; }
/* Plain positive top margin by default - normal breathing room above a
   hint, whatever happens to precede it (a heading, a card just opening, a
   closed form, another paragraph - a hint shows up after all of these
   somewhere in the app). A negative top margin was the *default* here
   before, tuned to pull a hint in snug against a single .field right
   above it (that field's own 0.85rem margin-bottom is what it was always
   meant to counteract) - correct for that one adjacency, but with nothing
   above it to pull against in every other context, it just overlapped/
   crowded whatever heading or boundary actually preceded it there (hit
   for real on settings.php's collapsible-card intros and profile.php's
   card intros - a hint sitting right after an <h3>, not a .field, with no
   padding anywhere in between to absorb a negative pull). Scoped back
   down to just its one correct adjacency below, instead of chasing every
   other context that turned up crowded with its own override the way
   .form-grid + .field-hint already had to. */
.field-hint { font-size: 0.85rem; color: var(--c-text-muted); margin: 0.4rem 0 0.85rem; }
.field + .field-hint { margin-top: -0.3rem; }
.form-grid + .field-hint { margin-top: 0.6rem; }
/* The "this customer prefers language X" note next to every place staff
   actually contact a customer (send email, request approval, send an
   invoice, invite to the portal) - a .field-hint alone (muted gray,
   same weight as body text) was too easy to skim straight past right
   before picking a language. Same small font-size as .field-hint on
   purpose - not a bigger note, just a more visible one: bold text, a
   background tint distinct from the white card it sits on (--c-bg, the
   page's own background), and a colored left edge to draw the eye. */
.lang-hint {
  display: inline-block; font-size: 0.85rem; font-weight: 700; color: var(--c-text);
  background: var(--c-bg); border-left: 3px solid var(--c-red);
  padding: 0.45rem 0.7rem; border-radius: var(--radius); margin: 0.3rem 0 0.7rem;
}
/* job_edit.php's "you have unsaved changes" reminder, next to Save/Undo -
   static (not blinking, unlike the Save button itself) so it doesn't fight
   for attention with it; the red text alone is enough to read as a warning
   next to two buttons that already changed color. */
.unsaved-hint { display: inline-block; margin-left: 0.6rem; font-size: 0.85rem; font-weight: 700; color: var(--c-red); }
/* .col-actions on the (empty) header cell shrinks that whole column down to
   its content instead of soaking up the table's leftover width (the
   default auto table-layout otherwise happily hands a bare "<th></th>"
   most of the free space, leaving the buttons stranded on the left with a
   big blank strip after them) - the classic width:1%+nowrap shrink-to-fit
   trick. .col-price does the same for any short, fixed-format numeric
   column (price, qty, VAT%, line total, ...) that has no business
   stretching wider than its content - job_edit.php's Services/Parts
   tables use it on all four of those, leaving only Description free to
   grow into whatever width auto-layout hands it. min-width:100px keeps
   it wide enough for amounts up to 99999.99 without wrapping even where
   the actual value (e.g. "1.00 kpl") is much shorter. */
.col-actions { width: 1%; white-space: nowrap; }
.col-price { width: 1%; min-width: 100px; white-space: nowrap; }
/* mailbox.php's actions column carries 4 buttons per row (View/Mark/
   Archive/Delete) - too many to hold to a normal labeled-button row at
   anything but a very wide desktop. Icon-only (each button's real label
   lives in its title/aria-label, not on screen) rather than shrinking the
   text small enough to fit, which stopped being legible before it stopped
   overflowing. */
/* min-width/min-height + padding, not a fixed width/height - Delete's
   confirmDelete() arm state (footer.php) swaps this button's icon for its
   data-confirm-again text ("Delete") for a few seconds, and a fixed-size
   box would just clip that text instead of the button growing to fit it. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 30px; min-height: 30px; padding: 7px; white-space: nowrap;
  clip-path: none; /* the notch on the app's normal buttons reads as a
    stray diagonal on something this small and square */
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
/* The app-wide button:hover background darken (#111 -> #1e1e1e) is barely
   visible on something this small - add a slight lift so a row of 4 of
   these actually gives feedback on hover, without the bigger pop/glow
   treatment the nav icons and logo get elsewhere. */
.icon-btn:hover { transform: translateY(-2px); box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3); }
.icon-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
.mailbox-row-actions { flex-wrap: wrap; justify-content: flex-start; gap: 4px; }
/* .col-actions-wide has no shrink-to-fit width rule (see its own comment
   above), so auto table layout otherwise hands it whatever's left over
   after From/Subject/Linked to. A floor width keeps 4 icon buttons from
   ever being squeezed onto two lines at a normal desktop size; From/
   Subject give up the difference by wrapping their own text instead,
   which they already do fine at narrower widths (no white-space:nowrap
   on them). Far smaller than the labeled-button version of this rule
   needed, now that the buttons themselves are ~30px square icons. */
.mailbox-table th.col-actions-wide, .mailbox-table td.col-actions-wide { min-width: 160px; }

/* jobs.php: the actions column used to sit alongside 9 data columns in the
   same row and, being shrink-to-fit, its width (and so every other
   column's, table auto-layout being a zero-sum game) swung with whatever
   language was active - "Merkitse valmiiksi" / "Avaa / muokkaa" (FI) needs
   far more room than "Mark done" / "view / edit" (EN), so the whole row
   visibly reshuffled on every language switch, and the 9 data columns were
   uncomfortably narrow even in English. Simpler fix: give the buttons
   their own full-width row underneath each job's data row (colspan across
   all 9 columns) instead of a 10th column fighting the other 9 for space -
   language no longer affects the data columns' widths at all, and the
   buttons get the whole row's width to lay out in regardless of language. */
/* Each job's two rows (data + actions) round into one distinct "bubble":
   top corners on the data row's outer cells, bottom corners on the
   actions row (border-top:none there so it visually fuses to the data
   row above instead of reading as a 3rd, separate row). A blank
   tr.row-spacer between job groups (real empty row-height, no border,
   painted the page background instead of the table's white) is what
   actually separates one bubble from the next - a real <table> can't do
   per-group gaps via margin (tbody ignores it) or border-spacing (that's
   uniform between every row, not just between groups), so an empty
   spacer row is the working equivalent. table:not(.mini-table) td:empty
   already hides it on mobile, where tbody.job-row's own margin below
   handles spacing between cards instead. */
.jobs-table tbody.job-row tr:first-child td:first-child { border-top-left-radius: var(--radius); }
.jobs-table tbody.job-row tr:first-child td:last-child { border-top-right-radius: var(--radius); }
.jobs-table tr.actions-row td {
  border-top: none;
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
  padding-top: 4px;
}
.jobs-table tr.row-spacer td { border: none; padding: 0; height: 10px; background: var(--c-bg); }
@media (max-width: 760px) {
  /* Each job is now two <tr>s (data + actions) - group them into one
     mobile card via their shared <tbody class="job-row"> instead of the
     usual one-card-per-<tr> rule, so they don't render as two separate
     boxes with a gap between them. The thead-row's own (unclassed, browser
     -implicit) tbody is untouched by this, so it stays invisible as before
     instead of showing an empty bordered box around its hidden heading. */
  /* `tbody tr`, not just `tr` - the generic `table:not(.mini-table) tr`
     mobile card styling further down this file (margin-bottom, border,
     box-shadow, border-radius) has the exact same specificity as a plain
     `table.jobs-table tr` and is declared later, so a same-specificity
     reset here would lose that fight regardless of what it says: the
     generic rule would win and silently reapply its own spacing/border to
     every row. That's what caused both a phantom empty card between job
     cards (tr.row-spacer, still forced hidden below to be extra sure) and
     a stray gap between each job's data row and its buttons row (the
     0.75rem margin-bottom re-appearing on the data <tr>). One extra type
     selector (tbody) makes this reset specific enough to win outright, no
     matter the source order. */
  table.jobs-table tbody tr { margin-bottom: 0; border: none; box-shadow: none; border-radius: 0; }
  /* !important: this specific generic-vs-jobs-table cascade fight has now
     bitten this row three times (a phantom card, a stray gap above the
     buttons, and this) even after fixing the specificity above - so stop
     relying on specificity/order at all for the one thing that must never
     come back under any circumstance and just force it, unconditionally. */
  table.jobs-table tr.row-spacer { display: none !important; }
  table.jobs-table tbody.job-row {
    display: block;
    margin-bottom: 0.75rem;
    background: var(--c-panel);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    overflow: hidden;
  }
  table.jobs-table tr.actions-row td { border-top: 1px solid var(--c-border); padding-top: 8px; }
}

/* Inline "edit in place" row (job_edit.php's services/parts tables): a
   hidden <tr class="edit-row"> sits right under the item it edits, toggled
   by the Edit button instead of navigating to a separate edit page. It's a
   quick inline tweak, not a whole page, so it should read as compact and
   stay close to the row it belongs to.

   Description always gets its own full-width row, and the smaller fields
   (quantity, unit, price, VAT, buttons) form a second, separate row. They
   used to all share one flex row - fine while everything fit on one line,
   but the services form has more fields than the parts form, so at a
   narrower window it would wrap, and the flex-grow description field
   would then stretch to fill whichever line it landed on, reproducing the
   exact "everything stacked full-width" look this was meant to fix.
   Splitting them into two independent rows means the second row's
   wrapping (if it ever needs to, on a very narrow screen) can't drag
   description along with it. */
.inline-edit-form { display: flex; flex-direction: column; gap: 0.6rem; padding: 0.4rem 0; }
.inline-edit-form .field { margin-bottom: 0; }
.inline-edit-form .field.grow input { width: 100%; }
.inline-edit-fields { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; }
.inline-edit-form input, .inline-edit-form select { width: 90px; }
.inline-edit-form select { width: 110px; }
/* The has-VAT checkbox would otherwise inherit the 90px number-field width
   above and render as a huge, oddly-padded checkbox. */
.inline-edit-form input[type="checkbox"] { width: auto; }

/* A short quantity/price/unit/has-VAT group (job_edit.php's "Add a
   service"/"Add a part" forms) - same sizing as .inline-edit-fields above
   (90px number inputs, 110px selects, auto checkbox), so the add-form
   matches the already-compact inline edit-row for the same fields instead
   of each one stretching to fill a whole form-grid column - plenty for
   "9999.99" with room to spare, without turning into a wide, mostly-empty
   box. grid-column: 1 / -1 like .field-full - always spans the row's full
   width so the group has room to wrap onto its own line, but the fields
   inside it size to their own content, not the grid's column tracks - a
   label wider than 90px (e.g. "Unit price ( excl. VAT )") just makes that
   one field wider, never crops. */
.form-grid .field-row-compact {
  grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end;
}
.form-grid .field-row-compact .field { margin-bottom: 0; }
.form-grid .field-row-compact input[type="number"] { width: 90px; }
.form-grid .field-row-compact select { width: 110px; }
.form-grid .field-row-compact input[type="checkbox"] { width: auto; }
@media (max-width: 760px) {
  /* The generic mobile "table td" rule turns every cell into a
     label-left/value-right flex row (fine for a single value, wrong for a
     cell that holds a whole multi-field form) - this cell has its own
     data-label-free layout via .inline-edit-form above, so opt it out. */
  table:not(.mini-table) td.edit-cell { display: block; text-align: left; }
}

/* Higher specificity than "button:hover" so the toggle stays transparent - it's
   not a red action button, just a nav control. */
button.nav-toggle, button.nav-toggle:hover, button.nav-toggle:focus {
  background: transparent;
  clip-path: none;
}
button.nav-toggle:hover { border-color: rgba(255, 255, 255, 0.5); }

/* ---- tables ---- */
table { border-collapse: collapse; width: 100%; margin: 1rem 0; background: var(--c-panel); }
th, td { border: 1px solid var(--c-border); padding: 8px 10px; text-align: left; font-size: 0.9rem; }
th {
  background: var(--c-black);
  color: var(--c-white);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 700;
}
tbody tr:hover { background: rgba(216, 31, 38, 0.045); }
tr[data-href] { cursor: pointer; }

td.status-booked, span.status-booked { color: var(--c-amber); font-weight: 800; text-transform: uppercase; font-size: 0.78em; letter-spacing: 0.02em; }
td.status-open, span.status-open { color: var(--c-red); font-weight: 800; text-transform: uppercase; font-size: 0.78em; letter-spacing: 0.02em; }
td.status-done, span.status-done { color: var(--c-black); font-weight: 800; text-transform: uppercase; font-size: 0.78em; letter-spacing: 0.02em; }
/* job_edit.php's own Customer Approval history - a real three-way status
   (pending/approved/declined), unlike a job's open/done - was borrowing
   .status-open/.status-done (pending fell into the "done" bucket, so a
   still-unanswered request read as visually identical to an approved one).
   Same typography as those two, own colors. */
span.job-approval-status-pending { color: var(--c-amber); font-weight: 800; text-transform: uppercase; font-size: 0.78em; letter-spacing: 0.02em; }
span.job-approval-status-approved { color: var(--c-green); font-weight: 800; text-transform: uppercase; font-size: 0.78em; letter-spacing: 0.02em; }
span.job-approval-status-declined { color: var(--c-red-dark); font-weight: 800; text-transform: uppercase; font-size: 0.78em; letter-spacing: 0.02em; }
/* portal_vehicle.php's decided-approval history reuses these same status
   classes on a real <button> instead of a plain <span> - it's the toggle
   for that entry's Details, not just a label, so it gets a solid color
   fill instead of just colored text (same green/red as above, not the
   .status-open/.status-done pair job_edit.php's own comment above already
   explains is the wrong one to reuse here). */
button.job-approval-status-approved, button.job-approval-status-declined {
  display: inline-block; border: none; cursor: pointer; color: var(--c-white);
  font-family: var(--font); font-weight: 800; text-transform: uppercase;
  font-size: 0.78em; letter-spacing: 0.02em; padding: 3px 10px; border-radius: 3px;
}
button.job-approval-status-approved { background: var(--c-green); }
button.job-approval-status-declined { background: var(--c-red); }
button.job-approval-status-approved:hover, button.job-approval-status-declined:hover { filter: brightness(0.85); }

@media (max-width: 760px) {
  /* Tables become one stacked card per row instead of scrolling sideways -
     horizontal scroll hides most columns off-screen with no visual hint
     they're there, which is unusable for scanning/searching on a phone.
     .mini-table (vehicle profile's small 2-3 column service/part lists) is
     already compact enough as a real table and is left alone. */
  table:not(.mini-table) { display: block; width: 100%; }
  /* Header rows are marked with class="thead-row" (most tables here are a
     bare <tr> of <th>s, not a real <thead>, and structural selectors like
     :first-child can't reliably tell "the header row" apart from "the first
     data row" once thead/tbody enter the picture - explicit is safer). */
  table:not(.mini-table) tr.thead-row { display: none; }
  table:not(.mini-table) tbody, table:not(.mini-table) tr { display: block; width: 100%; }
  table:not(.mini-table) tr {
    margin-bottom: 0.75rem;
    background: var(--c-panel);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    overflow: hidden;
  }
  table:not(.mini-table) td {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    text-align: right;
    border: none;
    border-bottom: 1px solid var(--c-border);
    padding: 8px 10px;
    font-size: 0.88rem;
  }
  table:not(.mini-table) tr > td:last-child { border-bottom: none; }
  table:not(.mini-table) td::before {
    content: attr(data-label);
    font-weight: 700;
    text-transform: uppercase;
    font-size: 0.68rem;
    letter-spacing: 0.03em;
    color: var(--c-text-muted);
    text-align: left;
    flex-shrink: 0;
  }
  table:not(.mini-table) td:empty { display: none; }
  /* .col-actions/.col-price only exist to fight the desktop auto
     table-layout (see the unmedia'd rules above) - on mobile every cell is
     already full-width and stacked, so undo them or the actions row would
     get squeezed down to a sliver instead of spanning the card. */
  table:not(.mini-table) td.col-actions, table:not(.mini-table) td.col-price, table:not(.mini-table) td.col-actions-wide { width: auto; min-width: 0; }

  /* For cells whose value is multi-line (e.g. a customer's several vehicles,
     one link per line) - flex-row-with-label doesn't work for those since
     <br> isn't reliable inside a flex container. Stack the label above
     instead. */
  table:not(.mini-table) td.td-stack {
    display: block;
    text-align: left;
  }
  table:not(.mini-table) td.td-stack::before {
    display: block;
    margin-bottom: 4px;
  }
}

/* ==========================================================================
   Calendar (calendar.php)
   ========================================================================== */
/* Heading alone, centered - Previous/Today/Next live in .cal-nav below as
   their own compact row, not squeezed in on either side of the heading.
   That used to be one flex row (Previous, heading, Next, space-between),
   which depending on the exact heading/button label lengths and viewport
   width sometimes let only two of the three items fit on a line, shoving
   the third onto its own line alone - a cramped, accidental-looking result.
   One centered heading plus one centered button row is simple enough to
   just work at every width, so it needs no responsive overrides at all. */
.cal-header { text-align: center; margin-bottom: 0.5rem; }
.cal-header h1 { margin: 0; }
.cal-nav { display: flex; justify-content: center; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.8rem; }
/* Smaller than the site's standard .btn - Previous/Today/Next all need to
   fit on one line together, and the standard button padding/font-size was
   comfortably roomy for a single "Edit" or "Save" but too wide for three in
   a row once the label is a long one (Estonian/Finnish "Edellinen",
   "Seuraava »" run noticeably longer than English "Previous", "Next »"). */
.cal-nav .btn { padding: 0.4rem 0.7rem; font-size: 0.72rem; }
.cal-tabs { margin-bottom: 1rem; display: flex; gap: 6px; flex-wrap: wrap; }
.cal-tabs a {
  display: inline-block; padding: 6px 14px; border: 1px solid var(--c-border); border-radius: var(--radius);
  text-decoration: none; color: var(--c-text); font-weight: 700; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em;
  background: var(--c-panel);
}
.cal-tabs a.active { background: var(--c-black); color: var(--c-white); border-color: var(--c-black); }

/* Same pill-tab look as .cal-tabs, generic name - used wherever a couple of
   pages need switching between at a glance (currently just the admin area:
   admin.php's user list vs. settings.php's company/invoicing settings,
   which share the "Admin" nav link instead of each getting their own). */
.sub-tabs { margin-bottom: 1rem; display: flex; gap: 6px; flex-wrap: wrap; }
.sub-tabs a {
  display: inline-block; padding: 6px 14px; border: 1px solid var(--c-border); border-radius: var(--radius);
  text-decoration: none; color: var(--c-text); font-weight: 700; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em;
  background: var(--c-panel);
}
.sub-tabs a.active { background: var(--c-black); color: var(--c-white); border-color: var(--c-black); }

/* mailbox.php's search+sort row, right under the folder tabs. A plain
   <input>/<select> and a <button> don't share a height by default (see the
   line-height comment on button/.btn above - the same UA mismatch shows up
   here too, on top of input/select's own larger font-size and different
   padding), so pinning all three controls to one explicit height is what
   actually gets them to "line up" rather than sitting a few px off from
   each other. */
.mailbox-toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 1rem; }
.mailbox-toolbar input[type="text"] { flex: 1 1 220px; min-width: 160px; }
.mailbox-toolbar select { width: auto; }
.mailbox-toolbar input[type="text"], .mailbox-toolbar select, .mailbox-toolbar button, .mailbox-toolbar .btn { height: 38px; }
/* Small solid dot ahead of an unread row's "From" - the bold row text alone
   (the pre-existing treatment) reads as "slightly different" more than
   "new", easy to skim past in a longer list; this is the same at-a-glance
   signal every mail client uses. */
.mail-unread-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--c-red); margin-right: 6px; vertical-align: middle; }

.cal-grid { display: grid; grid-template-columns: 20px repeat(7, 1fr); gap: 4px; }
.cal-daylabel { font-weight: 700; text-align: center; padding: 4px; font-size: 0.78rem; color: var(--c-text-muted); text-transform: uppercase; letter-spacing: 0.03em; }
/* Small ISO week number, one per row, in the narrow leading column added
   above - deliberately understated (muted, no border/background of its
   own) since it's a reference number, not something to click. */
.cal-week-num { display: flex; align-items: center; justify-content: center; font-size: 0.6rem; color: var(--c-text-muted); }
.cal-cell { border: 1px solid var(--c-border); border-radius: var(--radius); height: 100px; padding: 0; font-size: 0.75rem; background: var(--c-panel); overflow: hidden; position: relative; cursor: pointer; }
.cal-cell.other-month { background: #ececea; color: #aaa; }
.cal-cell.today { border-color: var(--c-red); border-width: 2px; }
.cal-cell-inner { height: 100%; padding: 4px; display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box; background: inherit; border-radius: inherit; position: relative; z-index: 0; }
/* Expanded on click (.expanded, toggled by JS - see the script at the bottom
   of calendar.php) so a full day's jobs are reachable the same way on every
   device - mouse or touch. Deliberately NOT also triggered by :hover: that
   used to pop the full list open just from resting the pointer over a cell,
   which made an actual click look like it did nothing (the cell already
   looked "expanded" from the hover, and the click's own expand-first step -
   needed so a click doesn't land straight on a job under the pointer - had
   no visible effect on top of that). Click is now the only trigger, so the
   collapsed -> expanded transition is always visible and the first click
   always means "open", never "navigate". */
.cal-cell.expanded { overflow: visible; z-index: 30; }
/* Pops out into a centered card with its own scroll, same on every screen
   size (desktop included - a day with many jobs has nowhere to grow in a
   ~100px grid cell either, and this way desktop and mobile behave and look
   identical, not just "click opens something"). job_edit.php's .wh-match
   popup (further down this file) uses the exact same "dimmed backdrop +
   centered card" mechanism for the same reason, so the properties that are
   genuinely identical between the two are written ONCE here with a combined
   selector rather than duplicated - only each page's own real differences
   (the gear-flicker workaround below, box sizing) stay in their own rules,
   further down for .wh-match. */
.cal-cell.expanded::before, .wh-match.expanded::before {
  content: '';
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 149;
}
.cal-cell.expanded .cal-cell-inner, .wh-match.expanded .wh-match-inner {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(90vw, 420px);
  max-height: 75vh;
  z-index: 150;
  background: var(--c-white);
  border: 2px solid var(--c-red);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
}
.cal-cell.expanded .cal-cell-inner {
  right: auto;
  min-height: 0;
  height: auto;
  /* hidden, not auto: the gear is a fixed 4.5rem square, but its bounding
     box isn't - a spinning gear shape's axis-aligned extent keeps changing
     (teeth sweeping in and out), so at some rotation angles it's briefly
     taller than a short one-job card, which made this box's own
     scrollHeight flicker past its clientHeight and flash a scrollbar in
     and out roughly 4 times per rotation (confirmed by sampling
     scrollHeight at 15° steps through a full turn). overflow:hidden here
     never shows a scrollbar regardless, since it only ever clips - the new
     .cal-cell-scroll wrapper below is where the job list actually scrolls
     now, decoupled from the gear entirely. job_edit.php's popup has no
     spinning gear, so .wh-match-inner just scrolls itself directly instead
     (see its own overflow-y:auto further down) rather than needing this
     same split-scroll-wrapper workaround. */
  overflow: hidden;
  font-size: 0.85rem;
  padding: 12px;
}
.cal-cell-scroll { display: flex; flex-direction: column; min-height: 0; }
.cal-cell.expanded .cal-cell-scroll { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.cal-cell.expanded .cal-daynum { font-size: 1.1rem; margin-bottom: 6px; }
.cal-cell.expanded .cal-job { white-space: normal; padding: 4px 6px; margin-bottom: 4px; }
.cal-daynum { font-weight: 800; margin-bottom: 2px; position: relative; z-index: 1; }
.cal-daynum a { color: inherit; text-decoration: none; }
/* A slowly-spinning gear watermark filling the whole day box - not just
   next to each job line - so a day with an open/in-progress job is visible
   at a glance even before you look at (or expand) its job list. Sits behind
   every other cell child (negative z-index within cal-cell-inner's own
   stacking context, set up by its position+z-index below), so it never
   obscures the day number or job entries. An actual cog SVG (a real toothed
   gear outline with a bored center hole), not the Unicode ⚙ glyph used next
   to individual job lines - that font character renders as a crude blob at
   this size and varies a lot across platforms; the SVG looks like a gear at
   any size and on every browser. */
/* Uses spin-centered, not the plain spin below - that one only rotates, and
   would replace (not add to) the translate(-50%,-50%) centering here every
   frame, sending the glyph sliding out of the cell as it spun. */
/* transform-box: border-box is the part that actually fixes the wobble -
   without it, a plain <svg> element's percentage transform-origin (the
   50% 50% baked into spin-centered's rotate()) can resolve against its
   internal viewBox coordinate system instead of its real rendered CSS box,
   pivoting the rotation around the wrong point and making the whole gear
   visibly swing off-center as it spins instead of turning cleanly in
   place. will-change: transform hints the browser to promote this to its
   own compositor layer up front, since it's continuously animating (the
   expanded day card keeps spinning behind scrollable content) - without
   it, some browsers repaint this element as part of the same layer as its
   position:fixed/overflow:auto ancestor, which can show as flicker while
   that ancestor is also being composited/scrolled. */
.cell-gear { position: absolute; left: 50%; top: 50%; width: 4.5rem; height: 4.5rem; transform: translate(-50%, -50%); transform-box: border-box; transform-origin: center; will-change: transform; z-index: -1; color: #b3b3b3; opacity: 0.16; animation: spin-centered 3s linear infinite; pointer-events: none; }
.cal-job { display: block; margin-bottom: 2px; padding: 1px 4px; border-radius: 2px; background: var(--c-white); border: 1px solid var(--c-border); border-left-width: 3px; text-decoration: none; color: var(--c-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Smaller while the cell is collapsed (0.7em of .cal-cell's own 0.75rem)
   - a busy day easily has 5+ jobs stacked into a ~100px-tall cell, and at
   full size several of them were getting their own row height clipped by
   the cell's overflow:hidden mid-line instead of cleanly hidden whole
   (the "+N more" count below already accounts for exactly how many don't
   fit - this just lets more of them actually fit before that clipping
   kicks in). :not(.expanded) keeps the popped-open day-detail view (which
   already switches to white-space:normal below) at its full, easier-to-
   read size - only the small collapsed grid cell needs the squeeze. */
.cal-cell:not(.expanded) .cal-job { font-size: 0.7em; }
.cal-cell.expanded .cal-job { white-space: normal; }
.cal-job.status-done { border-left-color: var(--c-black); }
.cal-job.status-open { border-left-color: var(--c-red); }
.cal-job.status-booked { border-left-color: var(--c-amber); }
/* Compact "H38"/"H40" badge on each job entry in month/week/day views -
   which of the two workshops it's booked into, so an hour busy in one is
   still visibly free in the other without needing a separate grid per hall. */
.cal-job-hall { display: inline-block; padding: 0 3px; margin: 0 2px; border-radius: 2px; background: var(--c-panel); border: 1px solid var(--c-border); font-size: 0.68em; font-weight: 700; color: var(--c-text-muted); }
.cal-more { font-size: 0.7rem; color: var(--c-text-muted); margin-top: auto; font-weight: 700; }
.cal-cell.expanded .cal-more { display: none; }
.gear { display: inline-block; animation: spin 2.5s linear infinite; color: var(--c-red); }
/* No spin on this one, unlike .gear - "booked" is waiting, not active work
   in progress. */
.hourglass { display: inline-block; color: var(--c-amber); }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes spin-centered { from { transform: translate(-50%, -50%) rotate(0deg); } to { transform: translate(-50%, -50%) rotate(360deg); } }

.cal-year-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
.cal-mini { border: 1px solid var(--c-border); border-radius: var(--radius); padding: 0.6rem; background: var(--c-panel); }
.cal-mini h4 { margin: 0 0 0.4rem 0; text-align: center; text-transform: uppercase; font-size: 0.85rem; letter-spacing: 0.03em; }
.cal-mini h4 a { color: inherit; text-decoration: none; }
.cal-mini-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; font-size: 0.65rem; }
/* Shared by the year view's <a> days (navigate to that day) and the
   dashboard mini calendar's <button> days (fill the quick-add form
   instead) - the reset properties only matter for the button case, a
   plain <a> never had a background/border/font to begin with. */
.cal-mini-day { text-align: center; padding: 3px 0; border-radius: 2px; text-decoration: none; color: var(--c-text); background: none; border: none; font: inherit; font-weight: 400; width: 100%; cursor: pointer; transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, color 0.15s ease, font-weight 0.15s ease; }
.cal-mini-day.other-month { color: #ccc; }
.cal-mini-day.today { background: var(--c-red); color: var(--c-white); font-weight: 700; }
.cal-mini-day.has-jobs:not(.today) { background: #f4d9da; }
/* The dashboard's version of this is a real <button> (the year view's is
   a plain <a>, unaffected either way) - without its own :hover, the
   generic button:hover rule (dark background, meant for the app's black
   action buttons, which also set white text) took over: same dark
   background here, but this element's text stays var(--c-text) - dark
   on dark, the number effectively disappearing on hover. This base
   rule is also what the year view's 12 tiny tiled mini-months use
   (0.65rem text, 3px padding, vs. the dashboard's 0.95rem/9px - see
   .cal-mini-grid above and .dash-cal-layout's override further down) -
   same red gradient + pop + glow language as the dashboard's own
   below, just scaled down to fit: a smaller pop (1.1 vs 1.18) and a
   thinner ring/tighter blur, so it reads as a glow without swallowing
   its cropped neighbors in this much denser grid. */
.cal-mini-day:hover { background: linear-gradient(135deg, #fdecec, #f9d2d0); color: var(--c-red); font-weight: 700; transform: scale(1.1); box-shadow: 0 0 0 1px var(--c-red), 0 0 8px 2px rgba(216, 31, 38, 0.5); position: relative; z-index: 2; }
.cal-mini-day.today:hover { background: var(--c-red-dark); color: var(--c-white); }
.cal-mini-day.has-jobs:not(.today):hover { background: linear-gradient(135deg, #f6c6c4, #eeaeac); }
@media (prefers-reduced-motion: reduce) {
  .cal-mini-day:hover { transform: none; }
}
/* The dashboard's single, much larger mini calendar (index.php only -
   see .dash-cal-layout .cal-mini-grid's own bigger font-size/padding
   above) gets the full treatment instead: a red gradient fill, a pop
   (scale), and a glow - two stacked box-shadows, a crisp solid 2px
   ring (0 blur) plus a soft blurred halo behind it, since a blur-only
   shadow read as barely-there against the reddish fill itself. Same
   glow as the free dash-hour-chip's own hover, below, so the two
   clickable-slot affordances in this one widget read as one hover
   language. `transform` never affects layout, so neighboring days
   never shift - only the hovered one grows. `position: relative`/
   `z-index` keep it drawing over its neighbors while enlarged. */
.dash-cal-layout .cal-mini-day:hover { background: linear-gradient(135deg, #fdecec, #f9d2d0); color: var(--c-red); font-weight: 700; transform: scale(1.18); box-shadow: 0 0 0 2px var(--c-red), 0 0 16px 4px rgba(216, 31, 38, 0.5); position: relative; z-index: 2; }
.dash-cal-layout .cal-mini-day.today:hover { background: var(--c-red-dark); color: var(--c-white); }
.dash-cal-layout .cal-mini-day.has-jobs:not(.today):hover { background: linear-gradient(135deg, #f6c6c4, #eeaeac); }
@media (prefers-reduced-motion: reduce) {
  .dash-cal-layout .cal-mini-day:hover { transform: none; }
}
/* Dashboard mini calendar only (index.php) - which day the job list/hour
   chips/quick-add form below are currently filled in for. An outline
   ring rather than a background/text color change, so it reads clearly
   layered on top of .today's red fill or .has-jobs' pink tint, not just
   on a plain day. */
.cal-mini-day.selected { outline: 2px solid var(--c-black); outline-offset: -2px; }

/* Dashboard search box (index.php) - sits right up against the Quick Add
   Job card above it with barely any breathing room. */
/* flex + align-items:stretch (the default cross-axis behavior, made
   explicit here) so the button always matches the input's actual
   rendered height instead of the two differing by a few px - a <button>
   and a text <input> compute their box height from different padding/
   line-height rules by default, and stretch is a self-correcting fix for
   that (unlike hardcoding a padding number that'd only match today's
   font-size). */
#searchForm { margin-top: 1.75rem; margin-bottom: 1.75rem; display: flex; align-items: stretch; gap: 0.5rem; }

/* Dashboard quick-add-job widget (index.php): three columns sharing the
   card - what's already scheduled that day (.dash-day-joblist-col),
   the calendar plus its free/busy hour chips (.dash-col-calendar), and
   the form to book into it (.dash-col-form). Grid, not flex - the 1fr
   third column is what makes the form column actually stretch to fill
   whatever's left of the card's width, rather than the three columns
   just sitting at their natural width with a bare gap somewhere. The
   job-list and form columns stay empty (or the job-list's "no jobs"
   line) until a day's picked - that's expected, not a layout bug. */
.dash-cal-layout {
  display: grid;
  /* minmax(0, ...), not a bare 260px/340px/1fr - a plain grid track never
     shrinks below its content's own min-content width by default (a real
     CSS grid gotcha, not obvious from the rule itself), so a wide input
     inside the 1fr form column could force the whole grid - and so the
     whole page - to scroll horizontally instead of the column just
     getting narrower. minmax(0, ...) is what actually lets each track
     shrink past that floor. */
  grid-template-columns: minmax(0, 260px) minmax(0, 340px) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}
/* Prev/current/next, in one row, pinned to the same width the grid
   below spans - the arrows sit at the far left/right (over the grid's
   own leftmost/rightmost columns, not just squeezed up against the
   month name) with the month label centered between them. justify-
   content: space-between does the pinning; the label itself doesn't
   need flex:1/text-align:center - two equal-width arrows on either
   side already center it directly by symmetry. */
.dash-cal-layout .cal-header { margin-bottom: 0.5rem; display: flex; align-items: center; justify-content: space-between; }
/* Uppercase like every other calendar header already is - calendar.php's
   own month view gets this for free from the global h1 rule (it really is
   an <h1>), and its year view's mini-month headings (.cal-mini h4) and the
   day-of-week labels (.cal-daylabel) both set it explicitly since neither
   inherits it otherwise - this is a <strong>, not an h1/h4, so it needs
   the same explicit rule or it's the one calendar heading left plain. */
.dash-cal-layout .cal-header strong { font-size: 1.1rem; text-transform: uppercase; letter-spacing: 0.02em; }
.dash-cal-layout .cal-mini-nav { flex: 0 0 auto; padding: 0.35rem 0.6rem; font-size: 0.7rem; }
/* Bigger than .cal-mini-grid/.cal-mini-day's shared base size (also used
   by calendar.php's year view, 12 tiny mini-calendars tiled at once,
   which must stay small) - scoped to just the dashboard's one calendar,
   which has an entire column to itself and room to be read at a glance
   rather than squinted at. */
.dash-cal-layout .cal-mini-grid { grid-template-columns: 16px repeat(7, 1fr); font-size: 0.95rem; gap: 3px; }
/* .cal-week-num's base rule (calendar.php's month view) assumes 100px-tall
   cells with room to center in - this grid's rows are just a few px tall,
   so it needs its own smaller/tighter sizing rather than the shared one. */
.dash-cal-layout .cal-week-num { font-size: 0.58rem; padding: 3px 0; }
.dash-cal-layout .cal-mini-day { padding: 9px 0; }
/* This one card's own breakpoint, wider than the 760px every other mobile
   rule in this file switches at - the other two columns alone are 260px +
   340px fixed, so anywhere from 760px up to ~900px the third (form)
   column was already being squeezed down to a sliver before the
   minmax(0, ...) fix above even kicks in, which is a bad look even though
   it no longer overflows the page. Stack earlier instead of living in
   that squeeze for a whole ~140px range. */
@media (max-width: 900px) {
  .dash-cal-layout { grid-template-columns: 1fr; }
  /* DOM order is job-list, calendar, form (matches the desktop left-to-
     right reading), but stacked on a phone that reads oddly - an empty
     "no jobs" line above the calendar you haven't even picked a day on
     yet. Reordered to calendar first, job-list second, form last. */
  .dash-col-calendar { order: 1; }
  .dash-day-joblist-col { order: 2; }
  .dash-col-form { order: 3; }
}

/* Clicked-day hour timeline (index.php's dashboard) - one small chip per
   business hour, busy or free, so a gap to schedule into is visible at a
   glance instead of having to check each existing job's time by hand. */
.dash-day-timeline { margin-top: 0.75rem; }
/* One block per hall (Hall 38 / Hall 40) - two physical workshops, each
   with its own independent busy/free row, since an hour can be booked in
   one and wide open in the other. */
.dash-day-halls { display: flex; flex-direction: column; gap: 0.5rem; }
.dash-hall-label { margin: 0 0 0.2rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--c-text-muted); }
.dash-day-hours { display: flex; flex-wrap: wrap; gap: 3px; }
.dash-hour-chip { padding: 2px 5px; border-radius: 2px; font-size: 0.68rem; font-weight: 400; border: 1px solid var(--c-border); color: var(--c-text-muted); background: var(--c-panel); font-family: inherit; transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease, color 0.15s ease, font-weight 0.15s ease; }
/* .free is a real <button> (click fills Scheduled time) - .busy-* are
   plain <span>s, not interactive. */
.dash-hour-chip.free { color: #9a9a9a; cursor: pointer; }
/* Same red glow + pop + bold-in as .cal-mini-day:hover above -
   `transform` doesn't affect layout, so the row of hour chips around
   this one never reflows, only the hovered chip grows. `position:
   relative`/`z-index` keep it drawing over its neighbors while
   enlarged. */
.dash-hour-chip.free:hover { background: linear-gradient(135deg, #fdecec, #f9d2d0); border-color: var(--c-red); color: var(--c-red); font-weight: 700; transform: scale(1.18); box-shadow: 0 0 0 2px var(--c-red), 0 0 16px 4px rgba(216, 31, 38, 0.5); position: relative; z-index: 2; }
@media (prefers-reduced-motion: reduce) {
  .dash-hour-chip.free:hover { transform: none; }
}
.dash-hour-chip.busy-open { background: var(--c-red); border-color: var(--c-red); color: var(--c-white); font-weight: 700; }
.dash-hour-chip.busy-booked { background: var(--c-amber); border-color: var(--c-amber); color: var(--c-white); font-weight: 700; }
.dash-hour-chip.busy-done { background: var(--c-black); border-color: var(--c-black); color: var(--c-white); font-weight: 700; }
/* The one free hour actually picked into the form - a ring rather than
   a fill (like busy-open/busy-done above) so it can't be mistaken for
   an occupied slot; still free, just the one currently chosen. */
.dash-hour-chip.selected { outline: 2px solid var(--c-black); outline-offset: -2px; color: var(--c-text); font-weight: 700; }
/* The actual per-job list for the clicked day - its own column now (left
   of the calendar), not stacked underneath the hour chips - so it needs
   its own heading to identify it (there's no "Hall 38"-style label
   above it the way the hour chips have). */
.dash-joblist-heading { margin: 0 0 0.5rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--c-text-muted); }
.dash-joblist-empty { color: var(--c-text-muted); font-size: 0.82rem; margin: 0; }
/* Times visible outright (used to only be a hover tooltip on the busy
   chips, which doesn't work on a touch screen at all). Each entry reads
   as its own small card - a colored left border (same booked=amber/
   open=red/done=black convention as vehicle_profile.php's .timeline-entry),
   not just plain run-in text - with the same status-*-text colors used
   throughout the rest of the app on the time/hall portion. */
.dash-day-joblist { list-style: none; padding: 0; margin: 0; font-size: 0.82rem; }
.dash-day-joblist li {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.1rem 0.5rem;
  background: var(--c-white);
  border: 1px solid var(--c-border);
  border-left: 3px solid var(--c-border);
  border-radius: 0 2px 2px 0;
  padding: 0.4rem 0.6rem;
  margin-bottom: 5px;
  transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.dash-day-joblist li:last-child { margin-bottom: 0; }
.dash-day-joblist li.status-open { border-left-color: var(--c-red); }
.dash-day-joblist li.status-booked { border-left-color: var(--c-amber); }
.dash-day-joblist li.status-done { border-left-color: var(--c-black); }
.dash-day-joblist .status-open, .dash-day-joblist .status-booked, .dash-day-joblist .status-done { font-weight: 700; white-space: nowrap; }
.dash-day-joblist .dash-job-desc { color: var(--c-text); transition: font-weight 0.15s ease; }
/* Same red glow + pop + bold-in as the calendar day cells/hour chips
   above (they're read-only, not buttons - this is a plain <li>, not
   interactive - but the user asked for the same hover feel here too,
   for consistency across the whole "Quick Add Job" card). Scaled down
   from those two (1.05 vs 1.18) since this is a full-width row, not a
   small square - 1.18 here would overflow well past its column into
   the calendar next to it. `transform` still never affects layout, so
   the job above/below never shifts, only the hovered row grows. */
.dash-day-joblist li:hover { background: linear-gradient(135deg, #fdecec, #f9d2d0); transform: scale(1.05); box-shadow: 0 3px 10px rgba(216, 31, 38, 0.35); position: relative; z-index: 2; }
.dash-day-joblist li:hover .dash-job-desc { font-weight: 700; }
@media (prefers-reduced-motion: reduce) {
  .dash-day-joblist li:hover { transform: none; }
}

/* minmax(0, 1fr), not bare 1fr - a bare 1fr track is really
   minmax(auto, 1fr), so a day with several jobs whose labels can't wrap
   (.cal-job is white-space: nowrap, truncated with an ellipsis instead)
   was growing that column past its fair 1/7th share to fit its own
   min-content, while a day with nothing scheduled shrank toward zero -
   confirmed directly via getComputedStyle: real columns measured
   374px/281px/216px/83px/83px/83px/83px for 7 equal calendar days.
   minmax(0, ...) forces every day column to actually split the
   available width evenly regardless of content - .cal-job's own
   existing overflow:hidden/text-overflow:ellipsis is what then makes a
   long label truncate cleanly inside its fixed-width column instead. */
/* First column widened from 50px to 76px - the hour labels ("07:00" etc.)
   fit 50px fine, but the "all day"/Unscheduled row's own label doesn't:
   measured its real text width in all 3 languages (canvas measureText,
   this exact font/size) - "Ajoittamaton" (Finnish) needs ~65.6px, plus
   4px padding each side = ~73.6px, more than the column had. Overflow
   was genuinely spilling past the grid's own left edge (confirmed via
   scrollWidth 68 vs clientWidth 50 for English "Unscheduled"), not just
   looking cramped. */
.cal-time-grid { display: grid; grid-template-columns: 76px repeat(var(--ndays), minmax(0, 1fr)); border: 1px solid var(--c-border); background: var(--c-panel); }
.cal-swipe-hint { display: none; }
.cal-time-grid .cal-time-label { font-size: 0.7rem; color: var(--c-text-muted); padding: 2px 4px; border-top: 1px solid var(--c-border); text-align: right; }
.cal-time-grid .cal-time-cell { border-top: 1px solid var(--c-border); border-left: 1px solid var(--c-border); min-height: 32px; padding: 1px; }
.cal-time-grid .cal-time-daylabel { font-weight: 700; text-align: center; padding: 4px; border-left: 1px solid var(--c-border); font-size: 0.8rem; text-transform: uppercase; }
/* Hovering a job entry that's been truncated (.cal-job is white-space:
   nowrap + ellipsis, so a long description/plate routinely is) reveals
   its full text by growing outward from its own horizontal center -
   like curtains opening from the middle - rather than to one side,
   so it reads naturally whether the day column it's in sits near the
   left or right edge of the week. min-height on the slot (~= a single
   .cal-job's own real rendered height, measured directly - 24.9px text
   box + 2px margin, rounded up) reserves that job's normal space at all
   times, so switching the job itself to position:absolute on hover
   doesn't collapse its slot and shove any OTHER job stacked in the same
   hour cell up to fill the gap - confirmed live, a cell with two jobs
   stacked in the same hour doesn't shift when either one is hovered. */
.cal-time-grid .cal-job-slot { position: relative; min-height: 27px; }
/* Half size while closed (0.5em - relative to the inherited font-size,
   not a hardcoded px value, so this stays correct if that base size
   ever changes) - fits more of a busy hour into the same slot at rest;
   font-size: 1em on the expanded states below resolves against the
   parent's own font-size regardless of this rule (em always does),
   restoring the original full size the moment a job actually expands,
   whether by direct hover or the whole-day version further down. */
.cal-time-grid .cal-job { font-size: 0.5em; }
/* .cal-job-span (below) is excluded from the expand-on-hover treatment -
   it's placed directly on the grid (grid-row/grid-column, not nested in
   a .cal-time-cell/.cal-job-slot), so the position:absolute + centering
   trick these rely on has no correct local anchor to center against. */
.cal-time-grid .cal-job:not(.cal-job-span):hover,
.cal-time-grid .cal-job:not(.cal-job-span):focus-visible,
.cal-time-grid .cal-time-cell.cal-day-hovered .cal-job {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: none;
  font-size: 1em;
  z-index: 20;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
  outline: none;
}
/* A job spanning several hours (calendar.php's $spans, placed via inline
   grid-row/grid-column) used to get duplicated into every hour cell it
   touched - the same box repeated down the column. One bar instead,
   drawn over the (now empty) cells it spans - z-index only needs to
   clear the plain 1px cell borders/grid lines it's drawn on top of. */
.cal-time-grid .cal-job-span { z-index: 5; }
/* Whole-day version of the same idea, from calendar.php's own small
   hover-tracking script (a plain :hover can't reach outside its own
   element to affect every OTHER hour row in the same day column) -
   hovering the day header or any cell in a column tags every element
   sharing that day, and .cal-job inside a tagged cell gets the exact
   same expand treatment as a directly-hovered one (the selector right
   above), so a whole busy day becomes readable at once instead of one
   truncated entry at a time. The light red tint + bold header is just
   "this column is now in focus" feedback - purely visual, doesn't
   affect layout the way expanding every job in it already doesn't
   (each one still only grows within its own reserved-height slot). */
.cal-time-grid .cal-time-cell.cal-day-hovered { background: rgba(216, 31, 38, 0.05); }
.cal-time-grid .cal-time-daylabel.cal-day-hovered { color: var(--c-red); }

/* 1024px, not 760px like the rest of the site's mobile rules: this block
   fixes real overflow/click bugs (the day-expand popover, the week/day
   grid escaping its box) that are just as broken on a tablet's narrower,
   still-touch screen as on a phone - a 768px-wide tablet was falling
   through to the desktop hover-only behavior and hitting the exact same
   bugs the mobile fixes below solve. Covers tablet portrait (768) and
   landscape (1024); real desktops are comfortably wider than that. */
@media (max-width: 1024px) {
  .cal-year-grid { grid-template-columns: repeat(2, 1fr); }
  .cal-cell { height: 72px; font-size: 0.68rem; }
  .cal-grid { grid-template-columns: 16px repeat(7, 1fr); }
  .cal-week-num { font-size: 0.55rem; }
  /* The week/day hour-grid has fixed-width day columns (see below) that add
     up to well over a phone's width - it used to just be left to size to
     its full content width, which pushed the WHOLE PAGE wider than the
     screen instead of scrolling in place. Keep the grid itself capped to
     the screen and let it scroll internally, so the page stays put and
     only the grid pans sideways. */
  .cal-time-grid-wrap { max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  /* minmax(92px, 1fr): day columns never shrink below a readable 92px, but
     grow to fill the screen when there's room for them to (day view - one
     column - used to stay pinned at 92px wide with a large dead strip next
     to it instead of filling the phone). When there isn't room (week view),
     the 92px floor takes over and the wrapper above scrolls instead.
     First column widened to 76px too, same as the desktop rule above and
     for the same reason (the Unscheduled/all-day label overflowing its
     column) - this grid already scrolls sideways on narrow screens, so a
     bit more total width here just means a slightly longer scroll, not a
     layout problem the way the overflow was. */
  .cal-time-grid { grid-template-columns: 76px repeat(var(--ndays), minmax(92px, 1fr)); }
  .cal-swipe-hint { display: block; font-size: 0.72rem; color: var(--c-text-muted); margin: -0.4rem 0 0.6rem; }
  /* The centered expanded-day card itself (.cal-cell.expanded) is defined
     once, unscoped, above - it's the same popout on every screen size. */
}
@media (max-width: 480px) {
  .cal-year-grid { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Vehicle profile / service history (vehicle_profile.php)
   ========================================================================== */
.profile-header { display: flex; gap: 1.5rem; align-items: center; flex-wrap: wrap; }
.plate-badge {
  display: inline-block; background: var(--c-black); color: var(--c-white); font-weight: 800; font-size: 1.3rem;
  padding: 8px 16px; border-radius: 3px; border: 2px solid var(--c-red); letter-spacing: 1px; font-family: var(--font);
}
/* portal_dashboard.php's "My Vehicles" list - a compact clickable row per
   vehicle (plate badge + name + an affordance arrow, vertically centered)
   rather than the old bare <a class="card"> wrapping two unaligned inline
   children, which left a big mostly-empty box with no hint it was
   clickable. Hover uses the same subtle red-tinted background already
   established for clickable table rows (tr[data-href]:hover). */
.vehicle-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  background: var(--c-panel);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 0.9rem 1.1rem;
  margin-bottom: 0.85rem;
  text-decoration: none;
  color: var(--c-text);
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.vehicle-card:hover, .vehicle-card:focus-visible {
  border-color: var(--c-red);
  background: rgba(216, 31, 38, 0.045);
  outline: none;
}
.vehicle-card-name { flex: 1; min-width: 0; font-size: 1rem; }
.vehicle-card-arrow { width: 20px; height: 20px; color: var(--c-text-muted); flex-shrink: 0; }
.profile-meta { font-size: 0.9rem; color: var(--c-text-muted); }
.profile-meta div { margin-bottom: 3px; }
.timeline { margin-top: 1.5rem; }
.timeline-entry { border-left: 4px solid var(--c-border); padding: 0.85rem 1.1rem; margin-bottom: 1rem; background: var(--c-panel); border-radius: 0 var(--radius) var(--radius) 0; box-shadow: var(--shadow); }
.timeline-entry.status-done { border-left-color: var(--c-black); }
.timeline-entry.status-open { border-left-color: var(--c-red); }
.timeline-entry.status-booked { border-left-color: var(--c-amber); }
.timeline-head { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 0.5rem; }
.timeline-date { font-weight: 700; }
/* Generic row of .btn buttons - used for a job's Invoice PDF/Edit (used to
   be plain " · "-separated text links crammed onto the mileage/cost line -
   easy to miss and cramped on a narrow screen) and for the page-level Back/
   Edit actions above the timeline. Real buttons throughout, same as every
   other list of actions in the app (jobs.php, admin.php, etc.). */
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 0.6rem; }
/* Each sub-section (Services/Parts/Approval history) within a timeline
   entry gets a top divider once there's more than one, so a customer/staff
   member scanning a job with both a services and a parts table can tell at
   a glance where one list ends and the next begins - previously just the
   small uppercase h5 label separated them, easy to miss when scrolling. */
.timeline-sub { margin-top: 0.5rem; }
/* Adjacent-sibling, not :first-of-type - :first-of-type matches by tag name
   alone, and .timeline-head/.btn-row above are divs too, so it would have
   matched the wrong element. This only adds the divider to a .timeline-sub
   that directly follows another one, which is exactly "not the first
   sub-section actually rendered" regardless of which ones a given job
   happens to have. */
.timeline-sub + .timeline-sub { margin-top: 0.9rem; padding-top: 0.9rem; border-top: 1px solid var(--c-border); }
.timeline-sub h5 { margin: 0 0 0.35rem 0; font-size: 0.72rem; color: var(--c-text-muted); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700; }
/* table-layout:fixed + explicit widths on the quantity/price columns below -
   without it, the browser's normal "auto" table layout sizes every column
   from its actual content on every render, so switching language (a
   quantity's unit word changing length - "kpl" vs "tk" vs "pcs" - or even
   just a longer/shorter description) reflows the whole table and visibly
   shifts the quantity/price columns sideways. Fixed layout locks each
   column to the width given here regardless of what ends up in it. */
.mini-table { width: 100%; font-size: 0.85rem; border-collapse: collapse; table-layout: fixed; }
.mini-table td, .mini-table th { padding: 4px 6px; border: none; border-bottom: 1px solid var(--c-border); background: none; color: var(--c-text); font-size: 0.85rem; text-transform: none; letter-spacing: normal; font-weight: 400; overflow: hidden; text-overflow: ellipsis; }
/* Header row (Description/Quantity/Price) - overrides the plain-td look
   .mini-table th inherited above, since a customer couldn't otherwise tell
   these were column labels rather than just another data row. */
.mini-table th { color: var(--c-text-muted); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 700; border-bottom: 2px solid var(--c-border); }
/* Quantity and price right-align as their own numeric column instead of
   trailing wherever the description text happens to end, so a column of
   prices actually reads as a column - price is bold too, being the number
   someone's most likely scanning for. Both are narrow and fixed-width - a
   job's cost is capped well under four figures in practice (the 9999.99
   ceiling these widths are sized for), so there's no reason to let these
   columns claim more room than that, wide description text included. */
.mini-table td:nth-child(2), .mini-table th:nth-child(2) { width: 92px; text-align: right; white-space: nowrap; color: var(--c-text-muted); }
.mini-table td:last-child, .mini-table th:last-child { width: 78px; text-align: right; white-space: nowrap; }
.mini-table td:last-child { font-weight: 700; }

/* ==========================================================================
   Job diagnostics (job_edit.php) - before/after fault codes, notes, photos
   ========================================================================== */
.diagnostic-entry { border: 1px solid var(--c-border); border-radius: var(--radius); padding: 0.85rem; margin-bottom: 0.85rem; background: var(--c-bg); }
.diagnostic-entry p { margin: 0.3rem 0; font-size: 0.9rem; }
.diagnostic-entry-head { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.4rem; }
.mail-thread-subject { font-weight: 800; font-size: 1rem; margin: 0 0 0.6rem; }
.diagnostic-stage-badge { display: inline-block; padding: 3px 10px; border-radius: 999px; font-weight: 800; text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.03em; color: var(--c-white); }
.diagnostic-stage-before { background: var(--c-red); }
.diagnostic-stage-after { background: var(--c-black); }
.diagnostic-entry-date { font-size: 0.8rem; color: var(--c-text-muted); }
/* A customer-approval entry that got edited after the customer already
   decided on it (job_approval_edit.php) - amber, same color language as
   the pending-status badge above, since this also means "needs a fresh
   look" rather than a plain informational note. The before/after table
   right below it uses the same red/green "removed/added" convention as
   a code diff - old value struck through and muted, new value in the
   normal text color so it doesn't look like it's also being removed. */
.job-approval-edited-notice { background: rgba(230, 162, 60, 0.12); border: 1px solid var(--c-amber); border-radius: var(--radius); padding: 0.5rem 0.7rem; color: var(--c-text); }
.approval-diff-table { width: 100%; border-collapse: collapse; margin: 0.5rem 0; font-size: 0.85rem; }
.approval-diff-table th, .approval-diff-table td { border: 1px solid var(--c-border); padding: 4px 8px; text-align: left; vertical-align: top; }
.approval-diff-table th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--c-text-muted); }
.approval-diff-old { color: var(--c-text-muted); text-decoration: line-through; }
.approval-diff-new { font-weight: 700; }
.diagnostic-photos { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 0.5rem; }
.diagnostic-photo-thumb { width: 80px; height: 80px; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--c-border); display: block; }
/* warehouse_tools.php's own list - same idea as .diagnostic-photo-thumb
   above (a small square crop, so a photo of any aspect ratio still reads
   as a clean thumbnail) but smaller, sized for sitting inline in a table
   row rather than a full gallery. */
.warehouse-list-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--c-border); display: block; }
/* job_edit.php's "Matching warehouse parts & tools" card - collapsed by
   default to just the name (.wh-match-head). A click pops it out into a
   centered floating card (.wh-match-body revealed) over a dimmed backdrop -
   the exact same "pop out of the grid, not just unfold in place" treatment
   as .cal-cell/.cal-cell-inner in calendar.php (see that block's own
   comments for the full reasoning), split the same way across two nested
   elements for the same reason: .wh-match itself carries the dimmed ::before
   backdrop (fixed, inset:0) and must stay untransformed for that fixed
   positioning to escape to the viewport, while its child .wh-match-inner is
   what actually goes position:fixed/centered/bordered/shadowed - putting
   both the backdrop and the transform on the same element would make the
   ::before's "fixed" resolve against that element's own transformed
   containing block instead of the viewport, shrinking the backdrop down to
   the card's own footprint. Same accordion behavior too - click to open,
   opening one closes any other already-open match, and (since a
   pseudo-element's click bubbles as a click on its host, same as the
   calendar's backdrop) clicking the dimmed backdrop re-fires this item's own
   click handler and closes it - no extra JS needed beyond what the inline
   <script> next to this card already does. */
.wh-match { cursor: pointer; }
.wh-match-inner { border: 1px solid var(--c-border); border-radius: var(--radius); padding: 0.65rem 0.85rem; margin-bottom: 0.6rem; background: var(--c-bg); }
.wh-match-head { font-weight: 700; }
.wh-match-body { display: none; margin-top: 0.6rem; }
.wh-match.expanded .wh-match-body { display: block; }
.wh-match.expanded { cursor: default; }
/* Popup body layout modeled directly on a real product-spec page the user
   shared as the target look: a photo next to the product/EAN numbers up
   top, then a clean label-left/value-right spec list below (each row its
   own thin bottom border, last row's border removed), rather than a stack
   of plain sentences. */
.wh-match-top { display: flex; align-items: flex-start; gap: 0.75rem; margin-bottom: 0.5rem; }
.wh-match-photo { width: 84px; height: 84px; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--c-border); display: block; flex-shrink: 0; }
.wh-match-ids { margin: 0; font-size: 0.8rem; color: var(--c-text-muted); }
.wh-spec-list { border-top: 1px solid var(--c-border); margin-bottom: 0.6rem; }
.wh-spec-row { display: flex; justify-content: space-between; align-items: baseline; gap: 0.75rem; padding: 0.4rem 0; border-bottom: 1px solid var(--c-border); font-size: 0.88rem; }
.wh-spec-label { color: var(--c-text-muted); }
.wh-spec-value { font-weight: 700; text-align: right; }
/* The shared backdrop/centered-card rules (position/size/border/shadow/
   z-index) live above, combined with .cal-cell.expanded's identical ones -
   only this page's own real differences (self-scrolling, no gear so no
   split-scroll-wrapper needed, and margin:0 to cancel .wh-match-inner's own
   base margin-bottom) stay here. */
.wh-match.expanded .wh-match-inner {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0;
}
.diagnostic-file-icon {
  width: 80px; height: 80px; display: flex; align-items: center; justify-content: center;
  background: var(--c-black); color: var(--c-white); font-weight: 800; font-size: 0.8rem;
  letter-spacing: 0.03em; border-radius: var(--radius); border: 1px solid var(--c-border);
}
/* ==========================================================================
   Auth (login.php, setup.php) and user management (admin.php)
   ========================================================================== */
/* login.php/setup.php don't include header.php's full nav (nothing to
   navigate to while signed out) - just the branded header bar above a
   centered card, reusing .site-main/.card as-is. */
.auth-main {
  display: flex;
  justify-content: center;
  align-items: center;
  /* Vertically centers the card in whatever's left below the sticky
     header (5px hazard stripe + --header-h) instead of it sitting near
     the top with a lot of empty gray space beneath. min-height (not
     height) so a card taller than the viewport - a long validation
     error, say - still just grows and scrolls, never clips. */
  min-height: calc(100vh - var(--header-h) - 5px);
  padding: 2rem 1.25rem;
}
/* Same black + hazard-stripe treatment as .site-header itself - the card
   built to match the header it sits under, rather than a plain white
   box. Padding moves to .auth-card-body since the stripe wraps the
   card's full outer edge, unbroken by .card's own padding. */
.auth-card { width: 100%; max-width: 380px; margin-bottom: 0; padding: 0; background: var(--c-black); border: none; overflow: hidden; position: relative; }
/* Same cut-corner notch every button/.btn has (button, .btn's own
   clip-path below) - just scaled up from 9px to 28px, since a 9px cut is
   sized for a ~36px-tall button and would barely register on a card this
   big. Desktop only - on a narrow phone screen the card already runs
   edge-to-edge-ish, and a 28px diagonal bite out of the corner there
   crowds the content instead of reading as a design flourish. */
@media (min-width: 761px) {
  .auth-card { clip-path: polygon(0 0, 100% 0, 100% 100%, 28px 100%, 0 calc(100% - 28px)); }
}
/* The hazard stripe wrapping the card, marching continuously CLOCKWISE
   around it (up the left edge, right along the top, down the right
   edge, left along the bottom - matching a clock face) - not something
   a single shared sliding background can do. A uniform slide moves the
   same way everywhere by definition, so one shared layer (tried first)
   made top and bottom flow the same direction as each other, and left
   and right flow the same direction as each other too - locally
   coherent on each edge alone, but not an actual rotation around the
   loop. Four independent strips - one per edge, each its own element
   with its own background-position animation direction - is what
   actually gets that right, plus a fifth short rotated strip for the
   notch (desktop only) continuing the same rotation across it. */
.auth-stripe {
  position: absolute;
  background-image: linear-gradient(
    135deg,
    var(--c-red) 25%, var(--c-black) 25%, var(--c-black) 50%,
    var(--c-red) 50%, var(--c-red) 75%, var(--c-black) 75%
  );
  background-size: 28px 28px;
  animation: 2s linear infinite;
  pointer-events: none;
}
/* Every corner these 4 strips meet at (top-left, top-right, bottom-right
   always; bottom-left too, below 761px where there's no notch) used to
   just be two full-length strips overlapping in the shared 5x5 corner
   square, whichever one happened to paint on top winning that square
   outright - an abrupt, mismatched block right at the corner instead of
   the two diagonals meeting cleanly. Each strip's clip-path now miters
   both its ends at 45deg instead - exactly like a picture-frame moulding
   cut - so the outer edge (the true card edge) runs full length into the
   corner point while the inner edge stops 5px short, and the two strips'
   cut lines land on the exact same diagonal. 45deg is also this pattern's
   own stripe angle, which is what makes a mitered joint disappear into
   the material on a real frame - not a coincidence here either. */
.auth-stripe-top {
  top: 0; left: 0; right: 0; height: 5px; animation-name: hazard-march-right;
  clip-path: polygon(0 0, 100% 0, calc(100% - 5px) 100%, 5px 100%);
}
.auth-stripe-right {
  top: 0; right: 0; bottom: 0; width: 5px; animation-name: hazard-march-down;
  clip-path: polygon(100% 0, 100% 100%, 0 calc(100% - 5px), 0 5px);
}
/* left/bottom run full-length, both ends mitered, by default (no notch
   below 761px - see .auth-card's own min-width media query) - the 28px
   inset making room for the notch, and the bottom-left end going back to
   a plain flat cut (it meets the notch's own connector piece instead of
   another strip directly), are both added back on desktop only, just
   below. */
.auth-stripe-bottom {
  right: 0; bottom: 0; height: 5px; left: 0; animation-name: hazard-march-left;
  clip-path: polygon(5px 0, calc(100% - 5px) 0, 100% 100%, 0 100%);
}
.auth-stripe-left {
  top: 0; left: 0; bottom: 0; width: 5px; animation-name: hazard-march-up;
  clip-path: polygon(0 0, 0 100%, 5px calc(100% - 5px), 5px 5px);
}
/* The notch's own short diagonal segment - a 5x40px strip rotated 45deg
   about its own center and placed at the diagonal cut's midpoint
   (14px, 100%-14px), continuing the same clockwise flow from the
   bottom edge (flowing left) into the left edge (flowing up). Hidden
   below 761px along with the notch itself (see .auth-card's own
   min-width media query above). */
.auth-stripe-notch { display: none; }
@media (min-width: 761px) {
  /* The bottom-left corner isn't a plain 90deg meeting anymore, it's the
     notch's diagonal cut - each strip's end there goes back to a plain
     flat cut (no miter) since it's the notch connector piece bridging
     them now, not one strip meeting another directly. */
  .auth-stripe-bottom { left: 28px; clip-path: polygon(0 0, calc(100% - 5px) 0, 100% 100%, 0 100%); }
  .auth-stripe-left { bottom: 28px; clip-path: polygon(0 0, 0 100%, 5px 100%, 5px 5px); }
  /* translateY(-2.5px), run *after* the rotate (so it shifts along the
     bar's own now-diagonal axis, not the page's) - without it the bar was
     centered exactly ON the notch's diagonal cut line, so half its 5px
     thickness fell outside the card's own clip-path and got clipped away,
     leaving only the inner half visible: half as thick as every other
     strip, which is flush against the card's actual edge instead of
     straddling it. Shifting inward by half the strip thickness puts its
     outer edge on the cut line instead, same as the straight strips'
     outer edge sitting flush at top:0/right:0/etc. */
  .auth-stripe-notch {
    display: block;
    width: 40px;
    height: 5px;
    left: 14px;
    top: calc(100% - 14px);
    transform: translate(-50%, -50%) rotate(45deg) translateY(-2.5px);
    animation-name: hazard-march-notch;
  }
}
@keyframes hazard-march-right { from { background-position: 0 0; } to { background-position: 28px 0; } }
@keyframes hazard-march-left { from { background-position: 0 0; } to { background-position: -28px 0; } }
@keyframes hazard-march-down { from { background-position: 0 0; } to { background-position: 0 28px; } }
@keyframes hazard-march-up { from { background-position: 0 0; } to { background-position: 0 -28px; } }
/* Starts at 8px (not 0) - this connector's own tile phase at 0 happened
   to land a color-band edge right at its trimmed 40x5 box, showing as a
   thin sliver of red instead of a properly-proportioned stripe (found by
   testing phase offsets directly against the rendered notch - the math
   for where a diagonal tile's bands fall inside a box this small, after
   the 45deg rotation, isn't worth deriving by hand when the target is
   "does it look right"). 8px is where the band both reads as a normal
   width and sits closest to the left-edge strip's own band just above
   the notch. Still slides a full 28px loop (-20 = 8 - 28), same seamless
   period as every other strip. */
@keyframes hazard-march-notch { from { background-position: 8px 0; } to { background-position: -20px 0; } }
/* Left/right/bottom padding bumped from 1.5rem to account for the 5px
   .auth-stripe-* strips now running around all four edges (previously
   only the top had a real 5px .hazard-stripe element eating into the
   layout; these are absolutely-positioned overlays, so nothing pushes
   the content in on its own anymore - this does it by hand). Explicit
   z-index so the real form content always paints on top of the (z-index:
   auto) stripe strips, regardless of DOM order. */
.auth-card-body { position: relative; z-index: 2; padding: 1.75rem calc(1.5rem + 5px) calc(1.5rem + 5px); color: var(--c-white); }
.auth-card-body h1 { text-align: center; color: var(--c-white); }
.auth-card-body p { color: #c9c9cb; }
.auth-card-body .field label { color: #b6b6b8; }
/* The site-wide input rule caps width at a fixed 280px (right for a
   .form-grid column, too narrow for this single-column card) - here the
   field should fill the same width as its label above it. */
.auth-card-body input { width: 100%; }
/* White on the header bar, and now here too - the logo artwork itself
   is black-on-transparent, made for a light background (see
   invoice_template.html's use of the same file unfiltered). */
.auth-logo { display: block; height: 64px; width: auto; margin: 0 auto 1.25rem; filter: invert(1); }

.role-badge { display: inline-block; padding: 3px 10px; border-radius: 999px; font-weight: 800; text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.03em; color: var(--c-white); }
.role-admin { background: var(--c-red); }
.role-staff { background: var(--c-black); }

/* mailbox_thread.php's rendering of an inbound HTML email body - a
   scriptless sandboxed iframe (no allow-scripts, so the inbound HTML can
   never execute anything) is the only place inbound HTML ever touches the
   page, see that file's own comment on why. Starts at a small fallback
   height; the page's own (trusted) onload handler resizes it to the
   message's real height once it loads, so the message reads inline with
   the rest of the card instead of sitting in its own little scrollbox. */
.mail-body-frame { width: 100%; height: 80px; border: 1px solid var(--c-border); border-radius: var(--radius); background: #fff; }
.mail-attachment-list { list-style: none; margin: 0.75rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.mail-attachment-list li { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
/* A generated filename like "Audi_WAUZZZF27LN075387_AllSystemDTC_
   20260902180711.pdf" is one long unbroken token (no spaces for the
   browser to wrap at) - without min-width: 0 a flex item never shrinks
   below its content's own width by default (the same flexbox gotcha
   noted on .dash-cal-layout above), so this link was forcing the whole
   row - and the page - wider than the viewport on mobile instead of
   wrapping. min-width: 0 lets it shrink to the row's actual width;
   overflow-wrap actually breaks the long filename across lines once it
   does. */
.mail-attachment-list a { min-width: 0; overflow-wrap: break-word; word-break: break-all; }
.mail-attachment-list a::before { content: "\1F4CE\FE0E "; } /* paperclip */
/* Was reusing .field-hint for this, but that rule's own negative top
   margin/large bottom margin (tuned for a hint sitting alone under a
   field, not a second inline item in a flex row) fought the row's own
   alignment and spacing instead of just sitting quietly next to the
   filename. */
.mail-attachment-size { font-size: 0.8rem; color: var(--c-text-muted); white-space: nowrap; }
/* mailbox.php's own list rows - a small paperclip + count next to the
   subject, so which messages actually have attachments (and how many)
   shows at a glance without opening each one to check. */
.mail-attachment-badge { display: inline-flex; align-items: center; gap: 2px; margin-left: 0.4rem; font-size: 0.78rem; font-weight: 400; color: var(--c-text-muted); white-space: nowrap; vertical-align: middle; }
.mail-attachment-badge svg { width: 13px; height: 13px; }

/* vehicle_qr.php's printable sticker label - QR + plate/make/model, sized
   to read at arm's length once printed and stuck on a vehicle. White
   background regardless of the app's own panel color, since this is meant
   to be cut out and printed, not viewed as a themed page element. */
.qr-label { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; max-width: 320px; margin: 0 auto; text-align: center; background: #fff; }
/* Tells anyone looking at the sticker what scanning it actually does,
   before they even scan it - printed above the code itself. Pinned to
   the same 220px width as the QR code/plate/vehicle line below it (all
   share this width now) so the whole label reads as one consistent
   block instead of the caption/text looking wider than the code. */
.qr-label-caption { width: 220px; font-weight: 800; font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--c-black); }
/* height:auto, not a second fixed 220px - the image is no longer square:
   the plate badge and vehicle line are baked into it below the code now
   (qr_lib.php), making it 1.3x as tall as it is wide. Pinning the height
   as well would squash the code itself out of shape, and a squashed QR is
   a QR that may not scan. */
.qr-wrap { width: 220px; }
.qr-label-image { width: 220px; height: auto; display: block; }
/* The Garage40 logo used to be a second absolutely-positioned <img> here,
   centered over the code with CSS (transform + a white plate div) - moved
   into qr_lib.php instead, composited directly onto the QR's own pixels
   with GD before this <img> ever gets its src. Same result (still on a
   solid white plate, per the same eccLevel::H/scanability reasoning that
   used to live in this comment), but now there's exactly one image, no
   separate overlay layer for this page and the invoice PDF to each
   position slightly differently - they call the same function and get
   back the same bytes. */
.qr-label-text { width: 220px; font-weight: 700; }
.qr-label-text .plate-badge { margin: 0 auto 0.4rem; }

/* Printing vehicle_qr.php should produce just the label, not the site
   chrome around it - the header/nav, the back/print buttons, and the hint
   paragraph all only make sense on screen. */
@media print {
  .site-header, .btn-row, .field-hint, h1 { display: none; }
  .qr-label { box-shadow: none; border: none; }
}

/* admin_activity_log.php - the device/user-agent column is a long raw
   string (e.g. a full browser UA), truncated with an ellipsis rather than
   left to stretch the whole table wide; the untruncated value is still on
   the cell's title attribute for a hover tooltip. */
.activity-ua { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ======================================================================
   Customer-portal arcade (portal_games.php + static/js/portal_games.js)
   - a few small games for a customer killing time in the waiting room.
   Portal-only, but lives here since the app keeps one shared stylesheet.
   ====================================================================== */
.arcade-menu {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}
.arcade-card {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  width: 100%;
  text-align: left;
  font-family: var(--font);
  background: var(--c-panel);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1rem;
  cursor: pointer;
  transition: transform 0.1s ease, border-color 0.1s ease, box-shadow 0.1s ease;
}
/* Pops out to 1.5x and gets a red glow (same pop+glow language as the
   main nav/logo hover elsewhere in the app), well past its own grid
   cell - position:relative + a high z-index is what lets it actually
   draw over its neighbors instead of being clipped/hidden behind them.
   The name/description also switches to full-contrast text (was muted
   gray, hard to read against the card's own light background) - no
   separate font-size bump needed for "bigger", the transform:scale(1.5)
   below already renders everything inside the card, text included,
   1.5x larger on screen without changing any real layout property.
   That distinction matters: an earlier version *did* bump font-size on
   hover, which (unlike transform, a pure paint-time effect the grid
   never sees) genuinely grew the hovered card's own untransformed
   layout height - and since CSS Grid sizes each row by its tallest
   item, that nudged every row below it down a few px while hovering,
   which is exactly the "other cards move a little" bug reported.
   Confirmed via getBoundingClientRect before/after hovering - every
   card below the hovered row shifted down in lockstep, not just
   neighbors in the same column, matching a row-height change exactly.
   background: var(--c-panel) is the fix for "hard to read" - .arcade-
   card is a real <button>, so the app-wide button:hover rule
   (background: var(--c-black-soft), meant for the black/red action
   buttons elsewhere) was ALSO winning here with nothing more specific
   overriding it, turning the card almost black on hover while its text
   stayed the muted-gray meant for a light card - confirmed via computed
   styles, not just guessed. */
.arcade-card:hover,
.arcade-card:focus-visible {
  transform: translateY(-2px) scale(1.5);
  border-color: var(--c-red);
  background: var(--c-panel);
  box-shadow: 0 0 0 2px var(--c-red), 0 0 32px 10px rgba(216, 31, 38, 0.4), 0 8px 20px rgba(0, 0, 0, 0.15);
  outline: none;
  position: relative;
  z-index: 10;
}
.arcade-card-icon { color: var(--c-red); width: 40px; height: 40px; }
.arcade-card-icon svg { width: 100%; height: 100%; display: block; }
.arcade-card-name { font-weight: 700; font-size: 1.05rem; color: var(--c-text); }
.arcade-card-how { font-size: 0.8rem; color: var(--c-text-muted); line-height: 1.45; transition: color 0.1s ease; }
.arcade-card:hover .arcade-card-how,
.arcade-card:focus-visible .arcade-card-how {
  color: var(--c-text);
}

.arcade-stage { max-width: 460px; margin: 1rem auto 0; }
.arcade-stage-head { display: flex; align-items: center; gap: 0.5rem; }
.arcade-title { flex: 1; min-width: 0; margin: 0; font-size: 1.1rem; font-weight: 700; }
.arcade-btn-sm { min-width: 0; min-height: 38px; padding: 8px 12px; font-size: 0.8rem; }

.arcade-hud { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.7rem 0; }
.arcade-stat {
  display: flex;
  flex-direction: column;
  min-width: 62px;
  padding: 4px 10px;
  background: var(--c-panel);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
}
.arcade-stat-k { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--c-text-muted); }
.arcade-stat-v { font-size: 0.95rem; font-weight: 700; font-variant-numeric: tabular-nums; }

.arcade-play { position: relative; }
.arcade-canvas-wrap {
  background: #111;
  border-radius: var(--radius);
  overflow: hidden;
  line-height: 0;
}
.arcade-canvas {
  display: block;
  width: 100%;
  height: auto;
  margin: 0 auto;
  touch-action: none;
}

.arcade-memory-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  max-width: 320px;
  margin: 0 auto;
}
.arcade-memory-grid.is-paused { filter: blur(3px); pointer-events: none; }
.arcade-tile {
  aspect-ratio: 1 / 1;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  perspective: 600px;
}
.arcade-tile-inner {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.35s ease;
}
.arcade-tile.is-flipped .arcade-tile-inner { transform: rotateY(180deg); }
.arcade-tile-front,
.arcade-tile-back {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  backface-visibility: hidden;
  border-radius: var(--radius);
}
.arcade-tile-front {
  background: linear-gradient(135deg, #1e1e1e 0%, #111 100%);
  border: 1px solid #333;
}
.arcade-tile-front::after {
  content: "";
  width: 26%;
  height: 26%;
  border-radius: 50%;
  background: var(--c-red);
  opacity: 0.85;
}
.arcade-tile-back {
  background: var(--c-panel);
  border: 1px solid var(--c-border);
  color: var(--c-black);
  transform: rotateY(180deg);
}
.arcade-tile-back svg { width: 56%; height: 56%; }
.arcade-tile.is-matched .arcade-tile-back { border-color: var(--c-red); color: var(--c-red); opacity: 0.5; }

/* On-screen play controls - sized for thumbs, not mice. Road Dash gets two
   wide paddles that split the stage width; Fuel Line gets a cross-shaped
   d-pad (.is-dpad, buttons grid-placed by portal_games.js). */
.arcade-console { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 0.9rem; }
.arcade-console[hidden] { display: none !important; }
.arcade-console.is-dpad {
  display: grid;
  grid-template-columns: repeat(3, 76px);
  grid-auto-rows: 76px;
  gap: 10px;
  justify-content: center;
}
.arcade-btn {
  font-family: var(--font);
  font-weight: 700;
  font-size: 0.9rem;
  color: var(--c-text);
  background: var(--c-panel);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  min-width: 44px;
  min-height: 44px;
  padding: 10px 16px;
  cursor: pointer;
}
.arcade-btn:hover { background: var(--c-bg); }
.arcade-btn:active { background: var(--c-border); }
.arcade-btn:focus-visible { outline: 2px solid var(--c-red); outline-offset: 2px; }
.arcade-btn-pad { flex: 1 1 120px; max-width: 220px; min-height: 72px; padding: 0; font-size: 1.8rem; line-height: 1; }
.arcade-console.is-dpad .arcade-btn-pad { flex: none; max-width: none; width: 76px; min-height: 76px; }

.arcade-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 0.7rem; }

.arcade-overlay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 1rem;
  background: rgba(17, 17, 17, 0.78);
  border-radius: var(--radius);
  z-index: 3;
}
.arcade-overlay[hidden] { display: none; }
.arcade-panel {
  background: var(--c-panel);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1.25rem;
  max-width: 300px;
  text-align: center;
}
.arcade-panel p { margin: 0.15rem 0; }
.arcade-overlay-title { font-weight: 700; font-size: 1.15rem; }
.arcade-overlay-best { color: var(--c-red); font-weight: 700; margin-top: 0.35rem; }
.arcade-panel .arcade-actions { justify-content: center; margin-top: 0.9rem; }

/* DOM-based games (Spot the Fault, Diagnostic Sequence, Sliding Puzzle,
   Tic-tac-toe) render into #arcadeBoard, each setting its own grid class. */
.arcade-fault-grid,
.arcade-slide-grid,
.arcade-ttt-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; max-width: 280px; margin: 0 auto; }
.arcade-simon-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; max-width: 260px; margin: 0 auto; }
.arcade-slide-grid.is-paused { filter: blur(3px); pointer-events: none; }

.arcade-light {
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  border: 2px solid var(--c-border);
  background: #1b1d24;
  cursor: pointer;
  transition: background 0.08s ease, box-shadow 0.08s ease, border-color 0.08s ease;
}
.arcade-light.is-on { background: var(--c-red); border-color: #ff6b6b; box-shadow: 0 0 16px rgba(216, 31, 38, 0.7); }

.arcade-simon-pad {
  aspect-ratio: 1 / 1;
  border: 2px solid rgba(0, 0, 0, 0.25);
  border-radius: var(--radius);
  cursor: pointer;
  opacity: 0.5;
  transition: opacity 0.1s ease, transform 0.1s ease;
}
.arcade-simon-pad.is-lit { opacity: 1; transform: scale(0.96); }
.arcade-simon-pad.is-fail { opacity: 1; background: var(--c-red) !important; }
.arcade-simon-pad.pad-0 { background: #c8342b; }
.arcade-simon-pad.pad-1 { background: #4374b1; }
.arcade-simon-pad.pad-2 { background: #c99a3e; }
.arcade-simon-pad.pad-3 { background: #5aae8b; }

.arcade-slide-tile {
  aspect-ratio: 1 / 1;
  font-family: var(--font);
  font-size: 1.5rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--c-text);
  background: var(--c-panel);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  cursor: pointer;
}
.arcade-slide-blank { aspect-ratio: 1 / 1; }

.arcade-ttt-cell {
  aspect-ratio: 1 / 1;
  font-size: 2rem;
  line-height: 1;
  color: var(--c-text);
  background: var(--c-panel);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  cursor: pointer;
}
.arcade-ttt-cell.is-x { color: var(--c-red); }

@media (prefers-reduced-motion: reduce) {
  .arcade-card { transition: none; }
  .arcade-tile-inner { transition: none; }
  .arcade-simon-pad, .arcade-light { transition: none; }
}
