/* 2026-07-20 — per-form sticky pinning REMOVED from the step head and page picker.
   It existed because .wizard-stage was the scroller: the head would scroll out of its
   own private scroll box, so it was pinned at top:0 of THAT box. With the document
   scrolling and the portal nav itself sticky, top:0 now resolves to the viewport top —
   i.e. BEHIND the nav — so the pin no longer put anything anywhere useful.
   The rule was always 'every page opens with its title on the same baseline', not
   'the title occupies viewport space forever'. The baseline is set by the step's top
   padding and is unchanged (99.39px); the head now simply scrolls away under the opaque
   nav like normal page content. Padding/background are kept so the box model — and
   therefore the baseline and the header→content buffer — do not move. */
/* ============================================================
   Field Portal — deficiency-report.css
   PAGE LAYOUT ONLY. All components (.field/.input/.btn/.chip/
   .portal-nav/.wizard…) come from assets/css/portal.css, which itself
   sits on assets/css/base.css (brand tokens + reset).

   This form is MOBILE-FIRST: base styles below are the phone layout
   (the capture loop lives on a phone on a walk); ≥1025px adjustments
   ride at the end. Square corners everywhere (inherited); focus =
   caret only (inherited). The D-number badge is brand red by default and
   takes the selected severity's colour (via --dr-sev-color) — same badge
   language as the photo-grid numbers / PDF badges.

   Page-specific components:
     .dr-snap                    the one "New deficiency" capture button (native
                                 Take Photo / Library sheet), at every width
     .dr-list / .dr-item         the numbered deficiency list
     .dr-item.is-open .dr-card   the in-place card editor (Severity dropdown · sub · comment)
   ============================================================ */


/* ============================================================
   1. CONTENT BOUNDARIES — box all content between the yellow guide
   lines (--gr-logo-x), identical to the other forms' pages.
   ============================================================ */
.dr-form .wizard-stage {
    /* 2026-07-20 — was calc(100vw - …). 100vw INCLUDES the vertical scrollbar; the
     document scrolls now, so on desktop that made the stage wider than its own
     container and produced a horizontal page scroll. 100% is the same width
     measured against the content box, which is what was always meant. */
  width: calc(100% - 2 * var(--gr-logo-x));
  max-width: none;
  /* Safe-area, not 0 (2026-07-25) — these pages ship viewport-fit=cover, and zeroing
     dropped the shared max(gutter, env(safe-area-inset-*)). On a notched iPhone in
     LANDSCAPE the content column and the Back button sat under the sensor housing:
     --gr-logo-x computes to ~51.8px against a 59px inset. Carried over from
     toolbox-talk.css, which got this on the same day. */
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
  /* clip, NOT hidden: `overflow-x:hidden` forces the other axis to compute to `auto`,
     which would make this stage a vertical scroll container again — exactly the nested
     scroller the rebuild removed. `clip` suppresses horizontal paint with no such
     side effect, which is all this ever wanted. (2026-07-20) */
  overflow-x: clip;
}
/* (2026-07-20 — the scrollbar-hiding pair on .wizard-stage was removed: the stage stopped
   being a scroller when the document took over scrolling, so there is no scrollbar left to
   hide. This form's horizontal rails — .dr-list in cards view and .dr-card-photos — keep
   theirs, since those genuinely do still scroll sideways.) */
/* This form used to hide the step indicator (a two-real-step flow made the dot bar
   read as noise) while keeping its row in flow so its title still lined up with the
   other forms. 2026-07-20 — the dot bar is gone portal-wide and every form now
   carries the shared "Page N / M" picker, this one included: it names both pages
   and is far quieter than a dot row. Anchor it to the yellow lines like the others.
   Vertical alignment across forms + pages stays a HARD rule — the picker's flow box
   is what holds this form's title on the same line as Weekly / Toolbox / Orientation,
   so never take it out of flow (that is what `display:none` did wrong before). */
/* 2026-07-20 FIX — the per-form picker override that sat here has been DELETED. It was carried
   over from the old absolutely-positioned dot bar (left/right/transform/zeroed padding). On a
   phone the picker is in NORMAL FLOW, so those declarations could not reposition it; all they did
   was strip its inset, dropping the picker line to x=0 hard against the screen edge while the title
   stayed on the 0.081 margin. No other form carries an override and every other form was correct,
   so this form inherits the shared rules too. Do NOT re-add a per-form picker rule. */
.dr-form .wizard-nav {
/* 2026-07-20 — `left`/`right`/`transform:none` dropped from this rule. They existed to
     undo the shared bar's absolute centring (left:50% + translateX(-50%)); the bar is in
     normal flow now, so all three were inert. width/max-width/padding-0 are KEPT and are
     doing real work: the bar sits inside .wizard-stage, which already carries the
     --gr-logo-x gutter, so the bar should fill its parent exactly rather than re-inset
     itself and end up double-guttered. */
  width: auto; max-width: none;
  /* Safe-area, not 0 (2026-07-25) — these pages ship viewport-fit=cover, and zeroing
     dropped the shared max(gutter, env(safe-area-inset-*)). On a notched iPhone in
     LANDSCAPE the content column and the Back button sat under the sensor housing:
     --gr-logo-x computes to ~51.8px against a 59px inset. Carried over from
     toolbox-talk.css, which got this on the same day. */
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
  padding-top: 10px;                 /* was 25px — dramatically shorter nav (2026-07-18) */
  z-index: 6; background: var(--white);
}
/* Shorter BACK / PREVIEW buttons too, so the whole bottom bar reclaims vertical room
   for the cards. */
.dr-form .wizard-nav .btn { min-height: 44px; }
.dr-nav .portal-nav-inner {
  width: calc(100% - 2 * var(--gr-logo-x));
  max-width: none;
  /* Safe-area, not 0 (2026-07-25) — these pages ship viewport-fit=cover, and zeroing
     dropped the shared max(gutter, env(safe-area-inset-*)). On a notched iPhone in
     LANDSCAPE the content column and the Back button sat under the sensor housing:
     --gr-logo-x computes to ~51.8px against a 59px inset. Carried over from
     toolbox-talk.css, which got this on the same day. */
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}
.dr-nav .portal-nav-logo { margin-left: 0; }

/* Primary always rightmost on mobile (shared mobile nav is flex-start). */
@media (max-width: 640px) {
  .dr-form .wizard-nav { justify-content: space-between; }
  .dr-form .wizard-nav [data-wizard-next],
  .dr-form .wizard-nav button[type="submit"] { order: 2; }
}

/* STEP HEAD — the h1 (Site details) and the h2 (Deficiencies) sit on the SAME baseline, so
   flipping between the two steps never jumps the title vertically. That is still a hard rule
   and still holds (measured 99.39px, identical to every other form).
   What changed 2026-07-20: the head no longer PINS. It was sticky because the stage was its
   own scroller; the document scrolls now, so the head scrolls away under the opaque nav like
   ordinary content. Only the padding below still matters — it sets the title→content buffer. */
.dr-form .wizard-stage > .dr-intro-head,
.dr-form .wizard-stage > .dr-step-head {
  background: var(--white);
  margin-bottom: 0;
  padding-bottom: var(--s3);   /* tight buffer under the title — keeps the capture row high */
}
/* Phones: the picker is IN FLOW above the head, so it pins first and the head pins
   directly under its box (--pick-trail exists only in this breakpoint). */
@media (max-width: 640px) {
  .dr-form .wizard-stage > .wizard-pagepick {
    background: var(--white);
    margin-bottom: 0;
    padding-bottom: var(--pick-trail);
  }
  /* (The head's sticky `top:` offset rule was removed here with the sticky itself — the
     whole rule went, selectors included: a selector list with no declaration block is
     invalid CSS and the parser discards it, taking the next brace with it.) */
}
.dr-form .wizard-stage > .dr-step-head + * { margin-top: var(--s2); }

/* This form's count is plain text — no pill fill, no hairline stroke (2026-07-18). */
.dr-count {
  padding: 0;
  background: none;
  box-shadow: none;
}


/* ============================================================
   2. INTRO STEP — title + the two-cell meta grid (Site · Completed by)
   ============================================================ */
.dr-intro-head { margin-bottom: var(--s7); }
.dr-intro-head .page-title { margin: 0; }

.dr-meta-grid {
  display: grid;
  grid-template-columns: 1fr;      /* phone: single column */
  gap: var(--s5);
  margin-top: var(--s2);
}
.dr-meta-grid .field { margin: 0; }

.dr-step-head .section-sub { margin-top: var(--s2); }


/* ============================================================
   3. CAPTURE — ONE button, "New deficiency", at the BOTTOM of the list so it
   moves down as deficiencies are added. Tapping it opens the device's native
   sheet (Take Photo / Photo Library); on a laptop it opens the file picker.
   ============================================================ */
/* The New-deficiency CTA is the LAST child of the list, so it takes the shape of the
   active view — a full-width red row in List, a red tile in Grid, a red card in the
   swipe rail — a full red CTA like the site's red cards. (Per-view sizing: Grid in §4b,
   Cards in §4c; List is the default full-width row below.) */
.dr-capture { display: flex; }
.dr-snap {
  flex: 1;
  display: flex; flex-direction: row; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 48px; padding: 0 var(--s3);
  border: 0;
  background: var(--accent); color: var(--white);
  font-family: 'Inter', Arial, sans-serif; font-size: var(--t-body); font-weight: 700;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.dr-snap:active { background: #C42A2F; }
.dr-snap .dr-snap-ico { display: inline-flex; }
.dr-snap .dr-snap-ico svg { width: 20px; height: 20px; display: block; }
/* Flash on a blocked Preview with zero deficiencies — a white inset ring shows on
   the red button (brief, in place, no layout shift). */
.dr-snap.is-error { box-shadow: inset 0 0 0 3px var(--white); }

/* 2026-07-20 — EMPTY SWIPE VIEW: with zero deficiencies there is nothing to swipe through, so
   the full-height red CTA is hidden in Cards view only. Deficiency #1 is created from List (the
   default landing view) or Grid, both of which keep their own smaller CTA. The tall card returns
   automatically once one deficiency exists. List/Grid CTAs are untouched by this rule. */
.dr-defs-step.is-cards.is-empty .dr-capture { display: none; }

/* Grid view → the CTA is a red SQUARE tile (matches the grid cards), with a big,
   left-aligned, site-style Inter label. */
.dr-defs-step.is-grid .dr-capture { aspect-ratio: 1 / 1; }
.dr-defs-step.is-grid .dr-snap,
.dr-defs-step.is-cards .dr-snap {
  width: 100%; height: 100%;
  flex-direction: column; justify-content: flex-end; align-items: flex-start;
  gap: 0; text-align: left;
}
/* A big white "+" accent above the title (replaces the old bar). */
.dr-defs-step.is-grid .dr-snap::before,
.dr-defs-step.is-cards .dr-snap::before { content: none; }
.dr-defs-step.is-grid .dr-snap .dr-snap-ico,
.dr-defs-step.is-cards .dr-snap .dr-snap-ico { display: inline-flex; color: var(--white); }
.dr-defs-step.is-grid .dr-snap .dr-snap-label,
.dr-defs-step.is-cards .dr-snap .dr-snap-label {
  font-family: 'Inter', sans-serif; font-weight: 900; line-height: 0.92;
  letter-spacing: -0.04em; text-transform: uppercase; color: var(--white);
}
/* Swipe = full-size CTA; text lifted ~40px off the bottom. */
.dr-defs-step.is-cards .dr-snap { padding: 40px 28px 62px; }
.dr-defs-step.is-cards .dr-snap .dr-snap-ico { margin-bottom: 14px; }
.dr-defs-step.is-cards .dr-snap .dr-snap-ico svg { width: 34px; height: 34px; }
.dr-defs-step.is-cards .dr-snap .dr-snap-label { font-size: clamp(30px, 8vw, 44px); }
/* Grid = scaled down. The "+" and label are bottom-anchored (justify-content:flex-end), so the
   BOTTOM padding is what lifts them off the tile edge — 2026-07-20 trimmed 37px → 22px to move
   both DOWN 15px. Grid tile only; the List row and Cards card are unaffected. */
.dr-defs-step.is-grid .dr-snap { padding: 16px 14px 22px; }
.dr-defs-step.is-grid .dr-snap .dr-snap-ico { margin-bottom: 6px; }
.dr-defs-step.is-grid .dr-snap .dr-snap-ico svg { width: 22px; height: 22px; }
.dr-defs-step.is-grid .dr-snap .dr-snap-label { font-size: 19px; }
/* List = a red ROW: big label LEFT (inset from the edge), "+" on the RIGHT (also inset).
   --dr-cta-size drives BOTH the label and the "+" so they're the SAME size; it is the
   label's prior clamp(17.6/4.8vw/21.6) × 0.8 = both matched AND 20% smaller (2026-07-21). */
.dr-defs-step:not(.is-grid):not(.is-cards) .dr-snap {
  --dr-cta-size: clamp(14.08px, 3.84vw, 17.28px);
  flex-direction: row; justify-content: space-between; align-items: center;
  padding-left: var(--s5); padding-right: var(--s5);
  font-weight: 700; text-transform: none;
}
.dr-defs-step:not(.is-grid):not(.is-cards) .dr-snap .dr-snap-label { order: 0; }
.dr-defs-step:not(.is-grid):not(.is-cards) .dr-snap .dr-snap-ico { order: 1; }
/* "+" matched to the label via --dr-cta-size. Stroke stays uniform across views (SVG
   non-scaling-stroke, 2.4px). */
.dr-defs-step:not(.is-grid):not(.is-cards) .dr-snap .dr-snap-ico svg {
  width: var(--dr-cta-size); height: var(--dr-cta-size);
}
/* Swipe view → the CTA is a red CARD at the end of the rail: same width as the cards,
   snapping like the rest. When a deficiency card sits beside it, `align-self: stretch`
   matches its height; when the list is EMPTY (no card to stretch against) the min-height
   below holds the SAME height so the swipe CTA is never a shorter "4th size". The formula
   mirrors the open card: its fixed chrome (~320px of header + fields + actions + padding)
   plus the same photo-cell clamp, so it tracks the card across every viewport. */
.dr-defs-step.is-cards .dr-capture {
  flex: 0 0 100%; min-width: 0;
  scroll-snap-align: center;
  align-self: stretch;
  min-height: calc(var(--dr-card-chrome) + var(--dr-ph-h));   /* BOTH tokens — the CTA must always equal the card's height in swipe view. Never hardcode either figure here again. (2026-07-20) */
  transform-origin: center center;   /* recede from centre like the deficiency cards (coverflow) */
  will-change: transform, opacity;
}


/* ============================================================
   4. THE DEFICIENCY LIST — collapsed rows
   ============================================================ */
.dr-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}
/* Kill the 300ms double-tap-zoom delay on every loop control (capture speed law). */
.dr-snap, .dr-item-row, .dr-ph-cap, .dr-ph-add,
.dr-ph > img, .dr-ph-remove,
.dr-card-actions .btn { touch-action: manipulation; }
.dr-item {
  position: relative;                      /* anchors the D-badge severity picker (below) */
  display: flex; align-items: center;      /* collapsed: the row button */
  background: var(--white);
  box-shadow: inset 0 0 0 1px var(--border);
}

/* ── RIGHT-CLICK CONTEXT MENU (owner 2026-07-22, List + Grid, laptop/desktop only) ─────────────
   Square corners and portal tokens like every other surface. position:fixed because it is placed
   against the VIEWPORT at the cursor, and the controller flips it left/up near an edge. */
.dr-ctx {
  position: fixed; z-index: 60;
  min-width: 210px; padding: var(--s2) 0;
  background: var(--white);
  box-shadow: 0 1px 0 0 var(--black), 0 10px 28px rgba(16, 24, 32, 0.16), inset 0 0 0 1px var(--border);
}
.dr-ctx-head {
  padding: var(--s2) var(--s3) var(--s2);
  font-size: 11px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-soft, #6B6B6B);
}
.dr-ctx-sep { height: 1px; margin: var(--s2) 0; background: var(--border); }
.dr-ctx-item {
  display: flex; align-items: center; gap: var(--s3);
  width: 100%; min-height: 34px; padding: 0 var(--s3);
  background: none; border: 0; cursor: pointer;
  font-family: inherit; font-size: 15px; color: var(--text); text-align: left;
}
.dr-ctx-item:hover, .dr-ctx-item:focus-visible { background: var(--fill); outline: none; }
.dr-ctx-item.is-danger { color: var(--accent); font-weight: 700; }
.dr-ctx-label { flex: 1; min-width: 0; }
/* Severity swatch — a filled square in the severity's own colour, same language as the D-badge. */
.dr-ctx-sw { flex: none; width: 14px; height: 14px; background: var(--fill); }
.dr-ctx-sw.is-none { background: transparent; box-shadow: inset 0 0 0 1.5px var(--border); }
.dr-ctx-tick { flex: none; display: inline-flex; color: var(--text); }

/* ── D-BADGE SEVERITY PICKER (owner 2026-07-22) ────────────────────────────────────────────────
   A real, fully transparent <select> parked exactly over the visible D-badge, so tapping the badge
   opens the DEVICE's own control (iOS wheel / Android dropdown / desktop menu). Identical technique
   to .wizard-pagepick-select — the portal's established way of getting a native picker without
   building one. It is a SIBLING of the three faces, never a child: the List badge sits inside
   .dr-item-row, which is a <button>, and a <select> inside a <button> is invalid and inert.
   Positioned per view because only one face is on screen at a time. Verified by hit-test at every
   canonical viewport, not by eyeballing the box. */
.dr-sev-pick {
  position: absolute; z-index: 6;
  margin: 0; padding: 0; border: 0; border-radius: 0;
  background: transparent; color: inherit;
  font-size: 16px;                 /* ≥16px or iOS zooms the page when the control opens */
  opacity: 0;
  -webkit-appearance: none; appearance: none;
  cursor: pointer;
}
.dr-sev-pick:focus-visible { outline: none; }
/* Focus + hover are shown on the BADGE itself, since the control it belongs to is invisible —
   without this the badge reads as a label rather than something you can act on. The card's Severity
   field remains the other way in. Hover is pointer-gated so touch never shows a stuck hover. */
.dr-item:has(.dr-sev-pick:focus-visible) .dr-num { box-shadow: 0 0 0 2px var(--text); }
@media (hover: hover) and (pointer: fine) {
  .dr-item:has(.dr-sev-pick:hover) .dr-num { box-shadow: 0 0 0 2px rgba(16, 24, 32, 0.35); }
}
.dr-item:has(.dr-sev-pick:active) .dr-num { transform: scale(0.94); }
.dr-num { transition: background var(--dur) var(--ease-out), color var(--dur) var(--ease-out), transform var(--dur) var(--ease-out); }

/* LIST — badge sits at the photo tile's top-left, inset by the row's own padding. The hit box is
   the badge plus a small bleed; deliberately NOT grown to a full 44px square, because on the 76px
   phone tile that would swallow half the photo and steal taps meant to open the card. */
.dr-defs-step:not(.is-grid):not(.is-cards) .dr-sev-pick {
  left: calc(var(--s3) - 3px); top: calc(var(--s3) - 3px);
  width: 46px; height: 30px;
}
@media (min-width: 861px) {   /* 48px tile + 34x18 badge */
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-sev-pick { width: 40px; height: 24px; }
}
/* GRID — badge is pinned to the tile's top-left corner (no row padding to account for). */
.dr-defs-step.is-grid .dr-sev-pick { left: 0; top: 0; width: 52px; height: 32px; }
/* CARDS / open card — badge leads the card head, inset by the card's padding. ONE rule for every
   width: since the ×1.4 desktop card scale was retired (2026-07-22) the card padding is --s3 and the
   badge is 44x28 on phone AND laptop, so the desktop override that chased a 60x38 badge at --s4 is
   gone with it. Re-add an override only if the card head's own metrics diverge again. */
/* Sized to the card HEAD, not to the badge: the head is 44px tall (the collapse chevron holds the
   tap floor) and centres its 28px badge, so the badge sits 8px lower than the card padding. Matching
   the head's box covers the badge wherever that centring puts it, instead of chasing a magic top
   offset that breaks the moment the head's height changes. Width stops short of the time text. */
.dr-defs-step.is-cards .dr-sev-pick { left: var(--s3); top: var(--s3); width: 50px; height: 44px; }
/* An OPEN card inside List/Grid shows the card face, so the picker follows it there too. */
.dr-defs-step:not(.is-cards) .dr-item.is-open .dr-sev-pick { left: var(--s3); top: var(--s3); width: 56px; height: 34px; }
/* One <li> holds BOTH faces; .is-open flips row → card (no overlay, no re-render). */
.dr-item .dr-card { display: none; }
.dr-item.is-open { display: block; }
.dr-item.is-open > .dr-item-row { display: none; }
.dr-item.is-open .dr-card { display: flex; }
/* Sortable drag ghost — calm, no tilt (single-axis motion law). */
.dr-item.sortable-ghost { opacity: 0.4; }

/* The whole collapsed row is the tap target (opens the card). */
.dr-item-row {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3);
  background: none; border: 0; cursor: pointer;
  font-family: inherit; text-align: left; color: var(--text);
  -webkit-tap-highlight-color: transparent;
}

/* D-number badge — brand red by default, or the selected severity's colour
   (set as --dr-sev-color on the <li> by the controller). Same badge language
   as the photo-grid numbers; the PDF badge matches via item.sevColor. */
.dr-num {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; height: 28px; padding: 0 var(--s2);
  background: var(--dr-sev-color, var(--accent)); color: var(--dr-sev-text, var(--white));
  font-size: 12px; font-weight: 700; letter-spacing: 0.02em;
  transition: background var(--dur) var(--ease-out), color var(--dur) var(--ease-out);
}

/* Left tile — a 76px square that holds the photo (or a --fill placeholder), the D-badge
   overlaid top-left, and the +N count bottom-right. */
.dr-thumb-wrap {
  position: relative; flex: none;
  width: 76px; height: 76px;
  overflow: hidden;
  background: var(--fill);
}
.dr-thumb {
  width: 100%; height: 100%;
  object-fit: cover;             /* screen tile only — the PDF always shows the WHOLE photo */
  display: block;
}
/* Photo-off placeholder — faint centred camera glyph over the fill (mirrors the grid tile). */
.dr-thumb-ph {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-soft, #6B6B6B); opacity: 0.5;
}
/* +N more photos overlay count, bottom-right of the tile */
.dr-thumb-more {
  position: absolute; right: 0; bottom: 0; z-index: 2;
  padding: 1px 5px;
  background: var(--black); color: var(--white);
  font-size: 10px; font-weight: 700;
}

/* View switcher (List / Grid / Cards) in the pinned header — pinned right; the count
   stays beside the title on the left. One icon per view; the active one fills. (Grid
   rules live in §4b, Cards in §4c.) */
.dr-defs-step .photos-head-row { align-items: center; }
/* …but the TITLE opts back out to the top of that row. align-items:center centres
   every child against the row's height, which is set by the 30px view switcher —
   that pushed the "Deficiencies" h2 (23.75px) down by (30 − 23.75) / 2 = 3.12px,
   so this form's page-2 title started 3.12px lower than its own page 1 and lower
   than Weekly / Toolbox / Orientation / Minutes. Pinning the h2 to flex-start puts
   its top back on the row's top — the shared title line — while the switcher stays
   optically centred beside it. Vertical alignment across forms and pages is a HARD
   rule; verified at 430×932 on every page of all five forms. (2026-07-20) */
.dr-defs-step .photos-head-row > .section-h { align-self: flex-start; }
.dr-view-switch { margin-left: auto; flex: none; display: inline-flex; gap: 2px; }
.dr-view-btn {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 30px; padding: 0;
  background: none; border: 0; cursor: pointer;
  color: var(--text-soft, #6B6B6B);
  -webkit-tap-highlight-color: transparent;
}
.dr-view-btn.is-active { color: var(--text); background: var(--fill); }   /* square fill, on-brand */

/* ---- TAP TARGETS ON THE SWITCHER (2026-07-29) --------------------------------------
   `_BUILD/page-state-audit.mjs` flagged all three controls at 32x30 painted / 34px hit
   against the portal's 44px floor.
   ⚠ THE BOXES CANNOT SIMPLY GROW. This switcher already ran off a 360px phone once and was
   fixed by SHRINKING it (see "THE SWITCHER WAS RUNNING OFF THE PHONE" below — 319px needed
   against a 295px stage); widening each control to 44 puts it straight back over the edge.
   So the hit area grows and the LAYOUT DOES NOT: an absolutely-positioned `::before`, the
   same technique `.repeat-remove` uses in portal.css.
   IT GROWS VERTICALLY ONLY. Horizontally the neighbours are 2px apart, so any meaningful
   sideways expansion would put one button's hit zone on top of the next one's — tapping
   Grid and getting List is worse than a small target. Vertically there is nothing to
   collide with.
   MEASURED on the real control at 360 after this rule, hit-swept exactly the way
   page-state-audit does: vertical run 30 -> 43.5px, horizontal 33.5px, and it steals from NO
   other control. Tap area up ~45% with zero layout change.
   ⚠ IT STILL DOES NOT CLEAR THE PORTAL'S 44px FLOOR, AND page-state-audit STILL REPORTS IT —
   correctly. That check is min(vertical, horizontal), so the number stays 34 however tall the
   zone gets. A true 44x44 means giving the switcher its OWN ROW at phone widths (4 x 44 plus
   the divider is ~199px, which fits 360 but not the ~295px left beside the heading) — a
   layout change needing the owner's sign-off, not a CSS tweak. Recorded, not claimed fixed. */
.dr-view-btn, .dr-select-toggle { position: relative; }
.dr-view-btn::before, .dr-select-toggle::before {
  content: "";
  position: absolute;
  inset: -7px 0;            /* 30 + 7 + 7 = 44 tall; 0 sideways, so no neighbour overlap */
}
@media (hover: hover) and (pointer: fine) { .dr-view-btn:not(.is-active):hover { color: var(--text); } }

/* Select-mode toggle + divider — sit LEFT of the view buttons inside the switcher, ~30px each
   side of a hairline divider (owner 2026-07-22). Toggling enters multi-select (List view); the
   trash then appears at the switcher's right end. List view only (hidden in Grid/Cards). */
.dr-select-toggle {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 30px; padding: 0;
  background: none; border: 0; cursor: pointer;
  color: var(--text-soft, #6B6B6B);
  -webkit-tap-highlight-color: transparent;
}
.dr-select-toggle.is-active { color: var(--white); background: var(--accent); }   /* on = brand accent fill */
/* Hairline divider. margin-inline is ONE value, so the space either side is equal by construction
   and cannot drift as buttons are added or reordered — the switcher's 2px flex gap also applies
   equally on both sides. Total clearance = 28 + 2 = 30px each side (asserted in the QA harness).
   Never split this into separate left/right margins. (owner 2026-07-22) */
.dr-switch-div {
  flex: none; align-self: center; width: 1px; height: 18px;
  margin-inline: var(--dr-switch-div-gap, 28px);
  background: rgba(16, 24, 32, 0.18);
}
@media (hover: hover) and (pointer: fine) { .dr-select-toggle:not(.is-active):hover { color: var(--text); } }

/* THE SWITCHER WAS RUNNING OFF THE PHONE AND BEING SILENTLY CLIPPED (2026-07-25).
   Measured at 360: stage 295px, heading 114px, switcher 193px → 319px needed, overflowing
   by 24px and clipped by `.dr-form .wizard-stage { overflow-x: clip }`. Cards went first,
   then Grid — with no scroll and no wrap, two of three views were simply unreachable, and
   turning multi-select on adds the trash button and makes it worse.
   Two changes, smallest first:
   1. The divider is a 1px rule carrying 28px margins EACH side — 57px, the single biggest
      consumer in the group. 8px each side on phones keeps it legible as a separator and
      returns 40px. The comment above still holds: one `margin-inline` value, never split.
   2. ⚠ SUPERSEDED 2026-07-27 — kept for the reasoning, NOT as the current rule.
      This said the switcher takes its OWN LINE on phones, always. Plain `flex-wrap: wrap`
      had been tried and was wrong: it fits at rest, then multi-select adds the trash,
      crosses the budget and wraps — the switcher JUMPS DOWN under the user's finger,
      mid-task. A control must not move because you used it. `flex-basis: 100%` solved that
      by spending a whole line in EVERY state. Predictable beats compact. (owner 2026-07-25)
      What changed: select mode now SWAPS the row's contents instead of adding to them (see
      the block that follows), so the control count — and the row height — is identical in
      both states. The requirement is unchanged and still met; it no longer costs a line.
      Do not reinstate `flex-basis: 100%` without also reverting that swap. */
/* ============================================================
   SELECT MODE SWAPS THE ROW — it never grows it. (owner 2026-07-27)
   Entering select mode used to ADD two controls to a row that already held three,
   which is what forced the switcher onto its own line at ≤640 (the 2026-07-25 rule
   ABOVE): six controls cannot share a 360px row with the title and the counter.
   Hiding the three view buttons while selecting means the row holds the SAME number
   of controls in both states, so it can live on ONE line without ever reflowing —
   the "predictable beats compact" requirement is met by keeping the height constant,
   not by spending a whole line on it. You are not switching views while choosing
   items to delete, so nothing is lost.
   ============================================================ */
.dr-defs-step.is-multiselect .dr-view-btn,
.dr-defs-step.is-multiselect .dr-switch-div { display: none; }
/* Breathing room between Select and Trash — at the shared 2px they read as one
   smudged control (owner 2026-07-27: "trash icon also appears too close to the
   select icon"). Only applies while both are visible. */
.dr-defs-step.is-multiselect .dr-view-switch { gap: 10px; }

@media (max-width: 640px) {
  .dr-view-switch {
    --dr-switch-div-gap: 8px;
    justify-content: flex-end;
  }
  /* ⚠ `flex-basis: 100%` REMOVED here 2026-07-27. It existed so the switcher could not
     jump a line when select mode revealed the trash — but that only happened because the
     row GREW. The swap above keeps the control count constant, so the switcher can sit on
     the title's line and the row never reflows. The original requirement (a control must
     not move because you used it) still holds; it is now met without spending ~38px of
     vertical space on every deficiency walk. */
  .dr-defs-step .photos-head-row {
    /* `flex-wrap: nowrap` was HERE and was dead — the 2026-07-29 rule below sets
       `wrap` on the identical selector 20 lines further down, so it always won.
       Removed 2026-08-13; the wrap declaration below is the single source. */
    justify-content: flex-start;   /* title + counter hug LEFT … */
    align-items: baseline;
    gap: var(--s2);
  }
  /* ⚠ THE SWITCHER TAKES ITS OWN LINE AGAIN AT PHONE WIDTHS (owner 2026-07-29) — and this
     REVERSES the 2026-07-27 change described directly above, deliberately, not by accident.
     WHY: the controls measured 32x30 painted / 33.5 x 43.5 hit-swept against the portal's own
     44px floor (design contract D6 — Apple HIG 44pt / WCAG AAA). They could not reach 44 on the
     line: the neighbours sit 2px apart, so widening one puts its hit zone over the next one's,
     and a tap that lands on Grid when you meant List is worse than a small target. A vertical-
     only ::before got the height to 43.5 but page-state-audit scores min(width, height), so 34
     was the number no matter what.
     On its own line there is room: 4 x 44 + the divider and its margins ≈ 199px, comfortable
     inside a 360px phone.
     COST, stated rather than hidden: ~44px of vertical space on the deficiencies step, which is
     exactly what the 2026-07-27 change bought back. The owner chose the tap floor over the
     height. The 2026-07-25 note above about `flex-basis: 100%` and the JUMP still applies and is
     still satisfied — the switcher is on its own line in EVERY state, so it cannot move because
     you used it, and the select-mode swap below is untouched. */
  .dr-defs-step .photos-head-row { flex-wrap: wrap; row-gap: var(--s2); }
  .dr-defs-step .dr-view-switch {
    flex: 0 0 100%;
    margin-left: 0;
    justify-content: flex-end;     /* icons keep the right gutter, as before */
    align-self: center;
  }
  /* The BOX is the tap target now, so the vertical-only overlay is retired here — an inset:0
     pseudo-element on a 44px box would be exactly the box. */
  .dr-defs-step .dr-view-btn,
  .dr-defs-step .dr-select-toggle { width: 44px; height: 44px; }
  .dr-defs-step .dr-view-btn::before,
  .dr-defs-step .dr-select-toggle::before { content: none; }
  .dr-defs-step .dr-count { flex: none; white-space: nowrap; }
  .dr-defs-step .section-h { flex: 0 1 auto; min-width: 0; }
  /* Other forms' photo headers keep the wrapping behaviour they were designed with. */
  .photos-head-row { flex-wrap: wrap; row-gap: var(--s2); }
}

/* 44px TAP FLOOR ON THE CAPTURE-LOOP CONTROLS (2026-07-25).
   These render at 32x30 / 36x36 — under the portal's own 44px floor (design decision D6:
   Apple HIG 44pt, WCAG 2.1 AAA). The file already applies the floor, but only at ≥861px
   ("EXCEPTION 1 — collapse chevron stays 44px … the 900px tablet"), so the PHONE — the
   stated primary device for this form — was the one surface that never got it, on controls
   a gloved hand uses in daylight.
   Expanded with a `::before` overlay rather than by growing the boxes, which is the same
   technique `.photos-head-row .btn-link` and `.repeat-remove` already use here: the glyph
   keeps its visual weight and only the hit area grows, so nothing reflows.
   ⚠ `.dr-ph-remove` matters most: it is 36px pinned to the top-right corner OF THE PHOTO,
   whose own tap opens the annotate editor — and deleting a photo has no confirm. Its
   overlay is deliberately grown INWARD/UP only (`inset: -4px -4px 0 0` gives 44x40 without
   reaching further across the image), so widening the target does not steal more of the
   photo from the editor tap. */
.dr-view-btn, .dr-select-toggle, .dr-trash-btn,
.dr-card-collapse, .dr-photoview-toggle, .dr-ph-remove { position: relative; }
/* ⚠ THE HORIZONTAL BLEED MUST NOT EXCEED HALF THE SWITCHER'S GAP (2026-07-27).
   -6px each side across a 2px gap made every pair of neighbours OVERLAP BY 10px — hit-tested
   34px effective per button, with ~5px at each edge belonging to the button next door. So a
   tap near the edge of List selected Grid: a target that is not merely small but WRONG, which
   is worse than a small one. -1px is exactly half the 2px gap, so adjacent targets share an
   edge and can never trade taps.
   THAT COSTS THE HORIZONTAL 44 AND THERE IS NOWHERE TO GET IT BACK. Measured at 360: the
   head row has 14.9px of slack between the counter and the switcher, and three 44px-wide
   targets need 32px more than the row spends today (3x44 vs 3x32 + 2x2). Widening the gap to
   12px so the -6px bleeds just touch needs 20px — also more than exists. The targets are
   44px TALL and 34px wide, above WCAG 2.5.8's 24px minimum and below 2.5.5's 44px; going
   further means redesigning what that row holds at 360, not adjusting a number here.
   The vertical -7px is untouched: nothing sits above or below these buttons. */
/* ⚠ AND IT MUST NOT BLEED UPWARD AT ALL. In cards view — the DEFAULT from 641px up —
   `.dr-defs-step.is-cards .wizard-stage` is `overflow: hidden`, deliberately, so the
   coverflow rail can never spill down over Back/Preview. This head row sits FLUSH with
   that clip's top edge (both measured at y=142), so every pixel of an upward bleed is
   clipped away: a -7px top hit-tested 37px at 900 and 1440 while giving the full 44 on
   a phone, which defaults to list view and does not clip. Growing DOWN instead costs
   nothing — the nearest control below is 13.5px past the old overlay, and this leaves
   6.5px of that. Do not restore a negative `top` here without first removing that clip,
   and do not remove that clip without re-reading why it exists. */
/* 34 × 44 IS A DECISION, NOT AN OVERSIGHT (owner 2026-07-28) — it will keep showing up in
   `page-state-audit` as TINY-TAP, and it should be left alone.
   MEASURED at the four canonical widths: the switcher is 153px wide inside a 295px row at 360,
   leaving **28px free**. Five controls each need +10px to reach 44 wide — 50px required against
   28px available. IT DOES NOT FIT AT 360, which is the binding surface.
   The two ways to make it fit were both rejected: widening only at ≥390 splits behaviour by
   viewport, and reclaiming the divider's 28px `margin-inline` gives up separation the owner
   specified on 2026-07-22 (and that comment forbids splitting it).
   Why it is acceptable: the HEIGHT is already 44, so this is short on one axis only. 34×44
   clears WCAG 2.5.8 AA (24×24) with room to spare, and Carbon's own guidance is the same shape
   — "must be at least 24×24… prefer Carbon's built-in sizes (44×44)"
   (carbon-builder/references/accessibility-rules.md:341-345). These are also view-mode siblings:
   a mis-tap switches view, it destroys nothing.
   REVISIT AT THE CARBON INJECTION, not before — Carbon will own this row's sizing, and the
   `--cds-layout-size-height-min: 2.75rem` floor pre-placed in portal.css makes 44 the minimum
   its components can render. Hand-tuning now is work the injection would redo. */
.dr-view-switch > button::before {
  content: "";
  position: absolute;
  inset: 0 -1px -14px -1px;  /* 30 + 14 = 44 tall, all downward · 32 + 2 = 34 wide, gap-limited */
}
/* THE CARD'S OWN ICON CONTROLS KEEP THE SYMMETRIC OVERLAY. They are NOT in the head row:
   they sit inside the card, nowhere near the stage's top clip, so they never lost their
   upward bleed and never needed the downward-only variant. Scoped apart from it because
   the two rules were briefly merged (2026-07-27) and the 14px downward bleed then reached
   out of the card head and over the first field beneath it — a fix for one row leaking
   into a component that was never broken. Keep them separate.
   Sized against their own neighbours: `.dr-card-head` has a 12px gap, so a -6px bleed
   makes adjacent targets touch without overlapping. */
.dr-card-collapse::before,
.dr-photoview-toggle::before {
  content: "";
  position: absolute;
  inset: -7px -6px;          /* 30 + 14 = 44 tall · 32 + 12 = 44 wide */
}
/* Only the LAST control in the switcher is flush with the content column's right edge, so
   only its overlay's right bleed lands outside — MEASURED as this step's entire horizontal
   overflow at 360 and 390 (6px then; 1px since the gap-limit fix above). Constraining just
   that one keeps every other control's full target; what is lost here was off-column against
   the screen gutter anyway. (Third instance of this pattern found 2026-07-26 — see the two
   check-all buttons and .photos-head-row .btn-link.) */
.dr-view-switch > *:last-child::before { inset-inline-end: 0; }

.dr-ph-remove::before {
  content: "";
  position: absolute;
  inset: -4px -4px 0 0;      /* 36 + 4 = 40 wide/tall, grown away from the photo centre */
}

/* MULTI-SELECT (List + Grid) — the ☑ toggle in the switcher is the ONLY way in on every device
   (the List-only swipe-right gesture was removed 2026-07-22); the trash sits beside it and
   selected rows/tiles get a red ring. The rows' `touch-action: pan-y` below OUTLIVES that gesture
   deliberately: rows sit in a vertical scroller, and declaring pan-y still stops the browser
   starting a horizontal pan on a row that SortableJS wants for a reorder drag. */
.dr-trash-btn {
  flex: none;   /* 2026-07-22 — now a child of the switcher (right end); no margin-left:auto */
  display: inline-flex; align-items: center; gap: 4px;
  height: 30px; padding: 0 8px;
  background: none; border: 0; cursor: pointer;
  color: var(--accent);
  -webkit-tap-highlight-color: transparent;
}
.dr-trash-n { font-size: 12px; font-weight: 700; min-width: 6px; }
.dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-row { touch-action: pan-y; }
.dr-item.is-selected { box-shadow: inset 0 0 0 2px var(--accent); }

/* ── LIST ROW HOVER — laptop and above only (owner 2026-07-22) ─────────────────────────────────
   A very quiet "this row is live": scale(1.006) — about 6px across a 996px row, read as movement
   rather than seen as growth — plus a 2% ink fill and a slightly firmer border.
   TRIPLE-GATED, deliberately: `hover: hover` + `pointer: fine` excludes touch, and `min-width:1025`
   excludes the TABLET band (861–1024) as well, because a tablet can report a fine pointer when a
   mouse is attached and the owner's call is no hover on mobile OR tablet.
   Transform-only, so it can never reflow the list or nudge its neighbours. Asymmetric timing —
   quicker in than out — which is what stops a fast pointer sweep down the list from strobing. */
@media (hover: hover) and (pointer: fine) and (min-width: 1025px) {
  /* ⚠ NO TRANSFORM ON HOVER (owner 2026-07-29: "hover state grows too much — the grey fill must
     stay inside the row, not touching the strokes on any side").
     MEASURED while genuinely hovered, at 1440: the FILL was already correct — 54.2 x 989 inside
     a 64.2 x 999 row, i.e. exactly 5px clear on all four sides. The growth was
     `transform: scale(1.003)` on the ROW: it took the row from 996 to 999px wide, so the
     hovered row's own border pushed past the borders of the rows above and below it and the
     whole thing read as swelling into its strokes. Scaling a bordered row cannot look calm —
     the border scales with it.
     The fill alone now marks hover, which is what the inset block was for. Its opacity
     transition stays (compositor-only, no repaint as the pointer sweeps the list); the
     transform transition and the prefers-reduced-motion override that existed ONLY to cancel
     this transform went with it. */
  /* The fill is an INSET BLOCK, not a background on the row: 5px in from every edge, so it reads as
     a highlight sitting inside the row rather than the row itself changing colour, and it never
     touches the row's border. Carried on a pseudo-element and animated by OPACITY, which is a
     compositor-only property — no repaint per row as the pointer sweeps the list. (owner 2026-07-22) */
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item::before {
    content: ''; position: absolute; inset: 5px; z-index: 0;
    background: rgba(16, 24, 32, 0.045);
    opacity: 0; pointer-events: none;
    transition: opacity 180ms var(--ease-out);
  }
  /* Row content must sit above the fill. Scoped to the ROW only — a blanket `.dr-item > *` also hits
     the absolutely-positioned .dr-sev-pick, forcing it to position:relative and dropping the badge's
     severity control behind the badge (caught by the hit-test, not by looking). Never widen this. */
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item > .dr-item-row { position: relative; z-index: 1; }
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item:not(.is-selected):not(.is-open):hover::before {
    opacity: 1; transition-duration: 120ms;   /* quicker in than out — stops a fast sweep strobing */
  }
}
.dr-defs-step.is-multiselect .dr-capture { display: none; }      /* no "New" while selecting */

/* Grid select-mode (2026-07-22, owner) — the list `.is-selected` inset ring is overridden by
   the grid tile's `box-shadow:none`, so re-assert an accent ring on the selected tile (higher
   specificity wins) and drop in a filled accent check-circle top-right. */
.dr-defs-step.is-grid .dr-item.is-selected { box-shadow: inset 0 0 0 3px var(--accent); }
.dr-tile-check {
  position: absolute; right: 8px; top: 8px; z-index: 3;
  display: none; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--accent); color: var(--white);
}
.dr-tile-check svg { display: block; }
.dr-defs-step.is-grid .dr-item.is-selected .dr-tile-check { display: inline-flex; }
.dr-defs-step.is-grid .dr-item.is-selected .dr-tile-count { display: none; }   /* check takes the top-right slot */
/* (2026-07-22 — the switcher NO LONGER hides in select mode: the select-toggle lives inside it
   and must stay visible to toggle back out; the trash sits at the switcher's right end.) */

/* List rows: a square photo tile (badge overlaid) + a three-line meta stack:
   TIME · LOCATION · PRIORITY. */
/* Left inset matches top/bottom (--s2) so the photo tile is evenly framed in the row
   corner; right stays --s3 for the meta text. (2026-07-19) */
.dr-item-row { padding: var(--s2) var(--s3) var(--s2) var(--s2); }

.dr-item-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* Time (top) — muted; Location (main) — body weight; Priority (bottom) — the severity
   word, tinted to its heat-map colour inline by syncRow. */
.dr-item-note { font-size: 12px; color: var(--text-soft, #6B6B6B); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dr-item-note:empty { display: none; }
.dr-item-loc  { font-size: var(--t-body); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dr-item-loc:empty { display: none; }
.dr-item-sub  { font-size: 12px; font-weight: 600; color: var(--text-soft, #6B6B6B); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dr-item-sub:empty { display: none; }

/* ══ DESKTOP LIST ROW — the full snapshot (owner 2026-07-22) ═══════════════════════════════════
   The List view is the "see everything at once" view, and at 916x48 per row there was room for far
   more than time/location/severity. All six fields we hold now show, in two groups:

     LEFT, flexible   line 1  Location · Responsible sub
                      line 2  the note, taking whatever width is left
     RIGHT, fixed     Severity · Time · photo count, right-aligned

   Flexible-left + fixed-right is the point: on a 3440px ultrawide the extra width goes to the
   location and the note (the fields that actually run long) instead of just padding out columns.
   These three fields are hidden below 861px so the phone keeps its existing three-line stack. */
.dr-item-resp, .dr-item-cmt, .dr-item-count { display: none; }

@media (min-width: 861px) {
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-meta {
    display: grid;
    /* loc | resp(flex) | severity | time | count */
    grid-template-columns: auto minmax(0, 1fr) auto auto auto;
    grid-template-rows: auto auto;
    column-gap: 14px; row-gap: 1px;
    align-items: center;
  }
  /* Location (13px) and sub (12px) share line 1 at DIFFERENT sizes, so they are aligned on their
     BASELINES, not their box tops — box-centring left the two words sitting 1px apart, which is
     exactly the kind of thing that reads as "slightly off" without being nameable. */
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-loc   { grid-column: 1; grid-row: 1; max-width: 320px; align-self: baseline; }
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-resp  { grid-column: 2; grid-row: 1; display: block; align-self: baseline;
    font-size: 12px; color: var(--text-soft, #6B6B6B); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* The note spans the whole flexible half on line 2 — the longest value gets the most room. */
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-cmt   { grid-column: 1 / span 2; grid-row: 2; display: block;
    font-size: 11px; color: var(--text-soft, #6B6B6B); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* Right group — each a fixed minimum so the columns hold their line down the whole list even when
     a value is blank. Severity carries a square swatch in its own colour, same language as the badge. */
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-sub   { grid-column: 3; grid-row: 1 / span 2;
    display: flex; align-items: center; gap: 6px; min-width: 92px; justify-content: flex-end; }
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-sub::before {
    content: ''; flex: none; width: 9px; height: 9px; background: var(--dr-sev-color, transparent);
  }
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-sub:empty { display: none; }
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-note  { grid-column: 4; grid-row: 1 / span 2;
    min-width: 62px; text-align: right; }
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-count { grid-column: 5; grid-row: 1 / span 2;
    display: flex; align-items: center; gap: 4px; min-width: 30px; justify-content: flex-end;
    font-size: 11px; font-weight: 700; color: var(--text-soft, #6B6B6B); }
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-count::before {
    content: ''; flex: none; width: 10px; height: 9px;
    box-shadow: inset 0 0 0 1.2px currentColor;      /* tiny photo-frame glyph, no extra asset */
  }
  /* The row states the photo count outright now, so the thumb's "+N" overlay is redundant here. */
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-thumb-more { display: none; }
}



/* ============================================================
   4b. GRID VIEW — 2-column SQUARE photo tiles (header toggle → .is-grid
   on the .dr-defs-step). ~4 tiles per screen (2×2), vertical scroll. The
   tile face lives in every <li> but shows only here; tap = open the same
   in-place card. Crop-to-fill is intentional in this browse index (unlike
   the PDF / editor, which always show the WHOLE photo).
   ============================================================ */
.dr-item-tile { display: none; }   /* hidden in List view */

.dr-defs-step.is-grid .dr-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s3);
}
/* An OPEN card takes the full width — drop the grid back to block flow so the
   card isn't trapped in one column (siblings are already hidden by the :has rule). */
.dr-defs-step.is-grid:has(.dr-item.is-open) .dr-list { display: block; }

/* Each item becomes a square photo tile (row + tools faces hidden). */
.dr-defs-step.is-grid .dr-item {
  display: block; position: relative;
  aspect-ratio: 1 / 1;
  background: var(--fill);
  box-shadow: none;          /* the photo fills the cell edge-to-edge */
  overflow: hidden;
}
.dr-defs-step.is-grid .dr-item > .dr-item-row { display: none; }

/* The open card is NOT a square tile — restore normal flow + the black focus box. */
.dr-defs-step.is-grid .dr-item.is-open {
  aspect-ratio: auto;
  background: var(--white);
  overflow: visible;
  box-shadow: inset 0 0 0 1.5px var(--black);
}
.dr-defs-step.is-grid .dr-item.is-open .dr-item-tile { display: none; }

.dr-defs-step.is-grid .dr-item-tile {
  display: block; position: absolute; inset: 0;
  width: 100%; height: 100%; padding: 0;
  background: none; border: 0; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.dr-tile-img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Photo-off placeholder — faint centred camera glyph over the fill. */
.dr-tile-ph {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-soft, #6B6B6B); opacity: 0.5;
}
/* D-badge — reuses .dr-num (severity colour + text), pinned top-left, sized down. */
.dr-tile-badge {
  position: absolute; left: 0; top: 0; z-index: 2;
  min-width: 0; height: 22px; padding: 0 6px;
  font-size: 11px;
}
/* Photo-count chip — value only (glyph + number), pinned top-right. */
.dr-tile-count {
  position: absolute; right: 0; top: 0; z-index: 2;
  display: inline-flex; align-items: center; gap: 3px;
  height: 22px; padding: 0 6px;
  background: rgba(0,0,0,0.55); color: var(--white);
  font-size: 11px; font-weight: 700;
}
.dr-tile-count svg { display: block; }
/* INFO BAR — a solid UI-black strip across the tile bottom (no gradient), so the
   text reads the same over any photo AND over the light no-photo fill. Line 1 =
   "Location | Severity" (severity text colour-coded to the heat-map convention via
   an inline colour set by the controller); line 2 = the responsible sub. */
.dr-tile-meta {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; flex-direction: column; gap: 1px;
  padding: 6px var(--s2);
  background: var(--black);          /* #191A1C — nice UI black, not pure black */
  text-align: left; color: var(--white);
  pointer-events: none;
}
/* Line 1: location. */
.dr-tile-loc {
  font-size: 12px; font-weight: 700; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dr-tile-loc:empty { display: none; }
/* Line 2: severity — colour-coded to the heat-map convention (hex set inline). */
.dr-tile-sev {
  font-size: 11px; font-weight: 700; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dr-tile-sev:empty { display: none; }


/* ============================================================
   4c. SWIPE (CARDS) VIEW — the DEFAULT view. Each deficiency is its FULL open card
   (edit right on screen), laid in a horizontal COVERFLOW: the focused card is centred
   and prominent; the cards left/right of it PEEK, cut off by the margins, and are
   scaled a touch + ghosted by the controller (applyCoverflow) for a "discography
   library" feel. A "N of TOTAL" counter beside the title tracks position — no dots.
   ============================================================ */
/* Swipe view goes FULL-VIEWPORT-WIDTH with plain `overflow: hidden` — the one clip that
   behaves identically on every engine (desktop Chrome AND iOS Chrome). The horizontal clip
   then falls at the SCREEN edges (invisible, so the neighbours still peek in the gutters),
   and the vertical clip falls above the bottom nav (so nothing — card or red CTA — can ever
   spill down over BACK/PREVIEW). The header is re-padded back to the content column. */
.dr-defs-step.is-cards .wizard-stage {
  width: 100vw; max-width: 100vw;
  margin-inline: calc(-1 * var(--gr-logo-x));
  overflow: hidden;
}
/* Cards view goes FULL-BLEED so the coverflow rail can run edge to edge; anything that must stay
   on the content margin gets its inset back individually. The step head was the only element
   doing that, so when the page picker arrived (2026-07-20) it rendered at x=0, hard against the
   screen edge, while the title beside it sat on the normal margin. Both need the inset. */
/* 2026-07-21 — .wizard-nav ADDED. The pattern above repeated itself exactly: the action bar
   used to be absolutely positioned, so it floated above the stage and this full-bleed could
   not reach it. Now that it sits in normal flow INSIDE the stage it inherited the 100vw
   bleed and ran off both screen edges — Back clipped at the left, Preview Report at the
   right, both colliding with the card above. Anything living in this stage that must stay on
   the content margin belongs in this list; only the coverflow rail is meant to reach edge
   to edge. */
.dr-defs-step.is-cards .dr-step-head,
.dr-defs-step.is-cards .wizard-nav { padding-inline: var(--gr-logo-x); }
/* THE PAGE PICKER IS SCOPED TO ≤640, NOT LISTED ABOVE (2026-07-26). It only shares the
   full-bleed problem while it is IN FLOW inside this stage. From 641px up portal.css §11b
   makes it `position: absolute` against `.wizard-step` — which is NOT full-bleed — and pins
   it to --gr-logo-x itself, so adding the padding there insets it a SECOND time.
   Measured in cards view before this scoping: at 700px the line sat at x=92 against a title
   at x=46, exactly one --gr-logo-x too far in (and 641/860 the same). That had been true on
   the left since the picker arrived; it became visible on both ends once the box was pinned
   to both gutters. The old fix zeroed the padding back out in the ≥861 desktop block, which
   left the 641–860 band broken — that override is now DELETED, since scoping by the same
   breakpoint the absolute positioning uses fixes the whole band at the source. */
@media (max-width: 640px) {
  .dr-defs-step.is-cards .wizard-pagepick { padding-inline: var(--gr-logo-x); }
}
.dr-defs-step.is-cards .dr-list {
  display: flex; flex-direction: row; align-items: flex-start; gap: 12px;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory;
  /* Never let the rail chain a scroll to the page — the idiomatic (CSSWG) way to keep the
     background from scrolling when the rail hits its ends. Pairs with the non-passive wheel
     handler that maps every wheel/trackpad delta to the rail. (2026-07-22) */
  overscroll-behavior: contain;
  /* The rail already fills the 100vw stage; the gr-logo-x padding = the half-peek so the
     card is the CONTENT width (wide) and the first/last card can still centre. */
  padding-inline: var(--gr-logo-x);
  scroll-padding-inline: var(--gr-logo-x);
}
/* Desktop coverflow is click-draggable (2026-07-22) — grab cursor at rest, grabbing while
   dragging; during a drag the controller drops scroll-snap + text-selection so the pan is
   free, and on release the throw decelerates onto the nearest card in one continuous motion.
   Pointer-driven, so touch is unaffected. NOTE: the controller holds `scroll-snap-type: none` as an
   INLINE style for the whole release, and hands it back only once the rail has landed — dropping
   .is-dragging while `x mandatory` is live re-snaps the rail in a single frame. (2026-07-22) */
@media (min-width: 861px) {
  .dr-defs-step.is-cards .dr-list { cursor: grab; }
  .dr-defs-step.is-cards .dr-list.is-dragging {
    cursor: grabbing; scroll-snap-type: none; user-select: none;
  }
}
.dr-defs-step.is-cards .dr-list::-webkit-scrollbar { display: none; }
/* Card fills the rail's CONTENT box (full viewport minus the gutter padding each side) =
   the content-column width, so the focused card is WIDE and the neighbours peek in the
   gutters. scale + opacity are set live by the controller (coverflow). */
.dr-defs-step.is-cards .dr-item {
  flex: 0 0 100%;
  min-width: 0;                 /* CRITICAL: without this the slide grows to fit its photo
                                   rail (min-width:auto) instead of keeping the card fixed
                                   and letting the rail scroll inside. */
  display: block; position: relative;
  scroll-snap-align: center;
  transform-origin: center center;
  will-change: transform, opacity;
  background: var(--white);
  box-shadow: inset 0 0 0 1.125px var(--black);   /* open-card frame, 25% thinner */
}
/* Show the open EDITOR for every card; hide the collapsed row / grid tile. */
.dr-defs-step.is-cards .dr-item > .dr-item-row,
.dr-defs-step.is-cards .dr-item > .dr-item-tile { display: none; }
.dr-defs-step.is-cards .dr-item > .dr-card { display: flex; }


/* ============================================================
   5. THE OPEN CARD — in-place editor (no overlay, no focus trap)
   ============================================================ */
/* Focus an OPEN card: hide the other collapsed rows so the open card + the
   New-deficiency button fit one screen (no scroll). Restored when it closes. */
.dr-defs-step:has(.dr-item.is-open) .dr-list .dr-item:not(.is-open) { display: none; }
.dr-item.is-open { box-shadow: inset 0 0 0 1.5px var(--black); }
/* PHOTO-CELL HEIGHT — declared on the STEP, not on .dr-card, so the swipe CTA (a SIBLING of the
   cards, not a descendant) can read the same token. It previously lived on .dr-card and the CTA
   hardcoded a duplicate `240px`; changing one silently desynced the other, and the red CTA is
   required to be exactly the card's height in swipe view. One token, both consumers.
   Value is a FIXED px on purpose — NOT a viewport unit. svh was meant to be stable but iOS Chrome
   still shifts it when the keyboard opens, which reflowed the whole card mid-typing (blank space
   grew, Done slid down). A fixed height cannot respond to the keyboard or the toolbar, so the card
   is rock-steady in every device-UI state. */
.dr-defs-step {
  /* 190 → 242: the 52px the removed Severity field gave back goes STRAIGHT to the photo
     preview (owner 2026-07-29 — "that change should allow more preview area and a larger
     container in the carousel inside the card"). MEASURED on Android 360 before the change:
     card 295x474, three 44px fields with 8px gaps.
     ⚠ THE TWO TOKENS MOVE TOGETHER AND MUST STILL SUM TO 474, so the card's overall height is
     unchanged and the swipe CTA (which is sized from `chrome + ph-h`, line ~256) still equals
     a real card exactly. Change one, change the other.
     ⚠ It is 242, not the ~270 two FULL rows of the in-card 2-col grid would need (two 131px
     cells plus the 8px gap) — 28px short. Closing that gap means taking height from the card
     or shrinking the cells, which is a separate owner decision, not something to sneak in. */
  --dr-ph-h: 242px;          /* photo cell — 2026-07-29, was 190 (and 240 on .dr-card before 07-20) */
  --dr-card-chrome: 232px;   /* everything else in the open card: head + TWO fields + actions +
                                padding + gaps. Tokenised 2026-07-20 so the swipe CTA tracks it.
                                MUST be updated whenever any of those parts change size.
                                2026-07-29 — 284→232: the card's Severity field was removed, and
                                that row was 44px + an 8px gap.
                                2026-07-22 — corrected 320→284 (measured real chrome): the extra
                                36px was a stale reserve for a caption ROW that became an absolute
                                overlay on 2026-07-21, so it double-counted and left the CTA 36px
                                taller than a real card. Now card == CTA, empty or full. */
}

/* 2026-07-20 — a keyboard-open photo-cell collapse (--dr-ph-h: 56px) was tried here and REVERTED.
   It was built to squeeze the open card into the space left in a Chrome BROWSER TAB, where ~490px
   of browser chrome (top banner + URL bar + toolbar) leaves too little room and the card clipped.
   The PWA has no browser chrome and the card fits fine, and the PWA is the surface the crew
   actually uses (home-screen icon, full screen, offline). Shrinking the photo while typing would
   have been a real cost in the app to fix a problem that only exists in a tab. Do not reinstate
   without checking the installed app first. The footer bar IS still hidden while typing
   (portal.css) — that one is a straight gain in either surface. */
.dr-card {
  padding: var(--s4); display: flex; flex-direction: column; gap: var(--s4);
}

.dr-card-head { display: flex; align-items: center; gap: var(--s3); cursor: pointer; }
.dr-card-time { font-size: 12px; color: var(--text-soft, #6B6B6B); margin-right: auto; }
/* Card-head × COLLAPSES the card (not destructive) — neutral, never red. */
.dr-card-collapse {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  background: none; border: 0; cursor: pointer;
  color: var(--text-soft, #6B6B6B);
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) and (pointer: fine) { .dr-card-collapse:hover { color: var(--text); } }
/* Photo-layout toggle (rail ⇄ 2-col grid) — mirrors the collapse chevron's quiet grey. */
.dr-photoview-toggle {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  background: none; border: 0; cursor: pointer;
  color: var(--text-soft, #6B6B6B);
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) and (pointer: fine) { .dr-photoview-toggle:hover { color: var(--text); } }
.dr-photoview-toggle .pv-ico { display: block; }
.dr-photoview-toggle .pv-rail { display: none; }        /* rail mode → show the grid glyph */
.dr-card.is-photogrid .dr-photoview-toggle .pv-grid { display: none; }   /* grid mode → show the filmstrip glyph */
.dr-card.is-photogrid .dr-photoview-toggle .pv-rail { display: block; }

/* Photo CAROUSEL — a horizontal rail of CELLS (photo + its caption line beneath),
   with the "+ Add photo" tile ALWAYS last; the controller lands it scrolled to
   that tile. Tap a thumb to MARK IT UP.
   Tiles are a consistent PORTRAIT rectangle: a phone-shot vertical photo fills it;
   a horizontal photo shows WHOLE, letterboxed with blank bands (contain, no crop). */
/* The rail runs the FULL card width so photo 1 sits flush-left with the fields
   below it. Each photo CELL is 10% narrower than the rail (see .dr-ph-cell), so the
   next photo peeks ~10% on the right as a "there's more →" cue; scroll-snap lands
   each photo back on that same left edge. */
.dr-photos-wrap { position: relative; }
/* Edge peek + fade hint — a soft fade appears on whichever side still has off-screen
   tiles (JS toggles the class on scroll). We land on the Add tile, so the left fade
   cues "swipe right → more photos". Subtle, never in the way, pointer-through. */
.dr-photos-wrap::before,
.dr-photos-wrap::after {
  content: ''; position: absolute; top: 0; bottom: 0; width: 8px;   /* narrower still — barely-there hint */
  pointer-events: none; z-index: 2;
  opacity: 0; transition: opacity var(--dur) var(--ease-out);
}
.dr-photos-wrap::before { left: 0; background: linear-gradient(to right, var(--white), transparent); }
.dr-photos-wrap::after  { right: 0; background: linear-gradient(to left,  var(--white), transparent); }
.dr-photos-wrap.show-fade-left::before  { opacity: 0.65; }   /* -35% — lighter hint */
.dr-photos-wrap.show-fade-right::after  { opacity: 0.65; }

.dr-card-photos {
  display: flex; flex-wrap: nowrap; align-items: flex-start; gap: var(--s2);
  min-width: 0;                      /* keep the rail bounded to the card so photos SCROLL,
                                        never widen the card (flexbox min-width:auto trap) */
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory;     /* each swipe lands a photo flush on the left edge */
  /* Swiping the photo strip must NEVER chain out to the card rail (which would flip to the
     next deficiency). `contain` stops the scroll from chaining to the coverflow when the
     strip reaches its end; `touch-action: pan-x` keeps the horizontal pan owned by the
     strip so a photo swipe can't be read as a card swipe. */
  overscroll-behavior-x: contain;
  touch-action: pan-x;
}
.dr-card-photos::-webkit-scrollbar { display: none; }
/* A cell = one photo stacked over its caption. Cells are 90% of the rail width (10%
   narrower) so the NEXT photo peeks on the right; photo 1 lands flush-left with the
   fields below. CROP-TO-FILL thumbnail (CompanyCam browse-index model) — the WHOLE,
   uncropped photo is one tap away (the editor) and on the PDF. */
.dr-ph-cell {
  flex: none;
  position: relative;          /* positioning context for the overlaid photo note */
  display: flex; flex-direction: column; gap: 4px;
  width: 94%;               /* ~6% peek (was 10%) — the next photo shows 40% less on upload */
  scroll-snap-align: start;
}
.dr-ph {
  position: relative; margin: 0; overflow: hidden;
  width: 100%; height: var(--dr-ph-h);
  background: var(--fill);
}
.dr-ph > img { width: 100%; height: 100%; object-fit: cover; display: block; cursor: pointer; }  /* crop-to-fill thumbnail; tap to open the whole photo. PDF is never cropped. */
/* Per-photo corner number removed on screen (owner 2026-07-19) — only the deficiency
   number (.dr-num D-01) is shown; the per-photo count no longer clutters each tile. */
.dr-ph-num { display: none; }
/* Per-photo caption — one-line preview (or "Add note…"); tap opens the popup. */
/* 2026-07-21 — the photo note is OVER the photo in EVERY view now, not just grid.
   It used to be a separate white row beneath each photo in the carousel, which cost a band
   of vertical space per photo and read as a second element rather than part of the photo.
   Grid already overlaid it; this promotes that treatment to the base so both views match
   and the grid-specific copies below could be deleted. */
.dr-ph-cap-row { position: absolute; left: 0; right: 0; bottom: 0; width: auto; display: block; }
.dr-ph-cap {
  width: 100%;
  min-height: 0;
  text-align: left;
  /* SAME TREATMENT AS THE SHARED PHOTO STRIP (owner 2026-07-29: "on deficiency card open
     view the comment block is a gradient — make fill same idea there"). The gradient scrim
     that shipped 2026-07-21 is GONE: near-black `--black`, white text at 700, the same 12px
     vertical padding, so a comment reads identically on a deficiency photo and on a Weekly
     Report photo. It was the one caption in the app that looked like a different app.
     ⚠ Trade-off taken deliberately: a solid band covers the bottom strip of the image where
     the scrim used to fade over it — which was the scrim's whole reason for existing. */
  padding: var(--s3) 8px;
  background: var(--black);
  box-shadow: none;
  border: 0; cursor: pointer;
  font-family: inherit; font-size: 12px; color: var(--white); font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  -webkit-tap-highlight-color: transparent;
}
/* Same invitation styling as the shared strip — see `.photo-cap.is-empty` in portal.css. */
.dr-ph-cap.is-empty { color: var(--placeholder-ghost-on-dark); font-weight: 500; font-style: italic; }
/* No mark-up chip — tapping the photo opens it (the editor). */
.dr-ph-remove {
  position: absolute; right: 0; top: 0; z-index: 2;   /* above the photo + blur backfill */
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  background: var(--black); color: var(--white);
  border: 0; cursor: pointer;
}
/* "+ Add photo" tile — the LAST tile of the carousel; same size as a photo tile,
   quiet grey so it reads as the add affordance, not a photo. */
.dr-ph-add {
  flex: none;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  width: 94%;                        /* matches the photo cells (reduced peek + snap) */
  scroll-snap-align: start;
  height: var(--dr-ph-h);
  background: var(--fill); box-shadow: inset 0 0 0 1px var(--border);
  color: var(--text-soft, #6B6B6B); font-size: 12px; font-weight: 600;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.dr-ph-add .dr-ph-add-ico { display: inline-flex; color: var(--text-soft, #6B6B6B); }
.dr-ph-add .dr-ph-add-ico svg { width: 30px; height: 30px; display: block; }
/* No photo yet → the lone "Add photo" tile spans the FULL card width, matching the
   input fields below it. (The old `+36px` height reserve is GONE — 2026-07-22: it dated
   from when the caption was a row BELOW the photo; the caption became an absolute overlay
   on 2026-07-21, so a real photo cell no longer adds that band, and the reserve was the
   sole cause of the card shrinking 36px on first upload.) */
.dr-ph-add:only-child { width: 100%; height: var(--dr-ph-h); }

/* ============================================================
   CARDS VIEW — INVARIANT CARD HEIGHT + photos fill (2026-07-22).
   Bug it fixes: the open card had no height floor while the red CTA did, so an empty card
   only matched the CTA by the +36 add-tile hack — and the first photo dropped the region to
   --dr-ph-h and shrank the card 36px (measured 510→474 at 1440; same on mobile). Fix: the
   card FLOORS to the CTA's exact height, and the photo region FLEXES to fill whatever vertical
   room is left instead of dictating the height. So empty == full, always, and the photo grows
   to use the space (bigger on laptop, where the card is taller — below).
   ============================================================ */
.dr-defs-step.is-cards .dr-item { min-height: calc(var(--dr-card-chrome) + var(--dr-ph-h)); }
.dr-defs-step.is-cards .dr-photos-wrap { height: var(--dr-ph-h); }   /* FIXED region → the photo below fills it (definite parent), so empty add-tile and a real photo are the exact same height */
.dr-defs-step.is-cards .dr-card-photos,
.dr-defs-step.is-cards .dr-ph-cell,
.dr-defs-step.is-cards .dr-ph,
.dr-defs-step.is-cards .dr-ph-add,
.dr-defs-step.is-cards .dr-ph-add:only-child { height: 100%; }
/* (Desktop cards override this whole block — fit-to-height card + 2-col photo GRID — in the
   ≥861px section further down. This global block is the PHONE cards view: fixed photo height,
   card floored to the CTA, no shrink.) */

/* ============================================================
   PHOTO GRID MODE — per-card header toggle. Flips the in-card photo strip from the
   horizontal rail to a 2-column, vertically-scrolling grid so all attached photos are
   browsable at a glance. The photo area keeps the rail's exact height, so the CARD's
   width / height / padding and the Location·Sub·Severity fields below never move —
   only the strip's insides change. Per-card; resets to the rail on the next re-render.
   (2026-07-19)
   ============================================================ */
.dr-card.is-photogrid .dr-card-photos {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2);
  height: calc(var(--dr-ph-h) + 36px);   /* fallback; JS locks the EXACT carousel height at toggle */
  overflow-x: hidden; overflow-y: auto;
  scroll-snap-type: none; touch-action: pan-y; overscroll-behavior-y: contain;
  align-content: start;
}
/* SQUARE photo tiles (like the outer Grid view), sized to fit inside the card. The note
   lives OVER the bottom of each photo. (2026-07-19) */
.dr-card.is-photogrid .dr-ph-cell { position: relative; width: auto; gap: 0; scroll-snap-align: none; }
.dr-card.is-photogrid .dr-ph { height: auto; aspect-ratio: 1 / 1; }
.dr-card.is-photogrid .dr-ph-add,
.dr-card.is-photogrid .dr-ph-add:only-child { width: auto; height: auto; aspect-ratio: 1 / 1; }
/* (2026-07-21 — the grid's caption overrides were removed: identical to the base rules now
   that the overlay is the default in every view. Two copies of the same treatment is how
   they drift apart.) */
/* No edge-fades in grid mode. */
.dr-card.is-photogrid .dr-photos-wrap::before,
.dr-card.is-photogrid .dr-photos-wrap::after { display: none; }

/* Fields — location, severity dropdown, responsible sub. Labels live INSIDE each
   control as ghost placeholder text (no title rows) to save space. */
.dr-fields { display: flex; flex-direction: column; gap: var(--s3); }
.dr-fields .field { margin: 0; }
/* Ghost placeholder for the selects: grey the closed display until an option is
   picked; the dropdown options themselves stay full-ink. */
.dr-fields select.input.is-placeholder { color: var(--text-soft, #6B6B6B); }
.dr-fields select.input option { color: var(--text); }

/* Text popup — centred modal (Enduro pop-up standard: 0.32 scrim, fade + rise
   16px/0.55s, fixed head + scrolling body, square). Type + read the full wrapped
   text here (a photo caption) so the card itself stays short. */
.dr-note-modal {
  /* Geometry is .vv-overlay's (portal.css) — fixed to the VISIBLE viewport, so this
     centres above the keyboard rather than behind it. */
  z-index: 100;
  display: flex; align-items: center; justify-content: center;
  /* ⚠ SAME ELEMENT AS .vv-overlay, AND THIS FILE LOADS AFTER portal.css — so a bare
     `padding: var(--s5)` here silently wiped the four safe-area paddings .vv-overlay sets.
     The popup is built as `class="vv-overlay dr-note-modal"` (deficiency-report.js:1297):
     one element, two classes, later shorthand wins. Identical trap to .so-modal's, found
     by triple-checking this form on 2026-07-28.
     `max()` not `+`: 20px is the MINIMUM gutter, raised only where an inset demands more.
     The scrim tint below sits on THIS element, so it paints across the padding and the
     inset band stays covered — there is no absolutely-positioned scrim child to go wrong. */
  padding: max(var(--s5), var(--safe-t)) max(var(--s5), var(--safe-r))
           max(var(--s5), var(--safe-b)) max(var(--s5), var(--safe-l));
  background: rgba(0, 0, 0, 0.32);
  animation: dr-note-fade var(--dur) var(--ease-out);
}
.dr-note-panel {
  /* 100%, not 80dvh (2026-07-26). The dvh was aimed at exactly the right problem —
     "this panel holds a textarea, so on a phone it is open WITH the keyboard up" — but
     dvh only ever tracked the address bar; it never shrinks for a keyboard, so on iOS
     the Done button still fell behind the keys. The scrim is now sized to the visible
     viewport, so 100% of its content box (its var(--s5) padding already excluded) is
     both correct and keyboard-aware. */
  width: 100%; max-width: 560px; max-height: 100%;
  display: flex; flex-direction: column; gap: var(--s4);
  padding: var(--s5);
  background: var(--white);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.24);
  animation: dr-note-rise 0.55s var(--ease-out);
}
.dr-note-panel-head { display: flex; align-items: center; justify-content: space-between; }
.dr-note-panel-title { font-size: var(--t-body); font-weight: 700; }
.dr-note-close {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; background: none; border: 0; cursor: pointer;
  color: var(--text-soft, #6B6B6B); -webkit-tap-highlight-color: transparent;
}
/* The single 160px textarea became a BULLET-LINE LIST on 2026-07-29 — `.dr-note-input` no
   longer exists. The list needs the same treatment the photo comment popup needed, for the same
   reason it was needed there: a long list grows past the viewport and takes the Done button off
   screen with it (MEASURED on Android 360 in the photo popup: 25 rows made a 1527px card in a
   720px viewport, Done ~800px below the fold).
   Only the LIST scrolls, so the title row and the Done row can never scroll away.
   `min-height: 0` is what allows a flex child to shrink below its content; the `.repeat`
   wrapper needs it too, or the shrink stops there and never reaches the list.
   The panel ALREADY caps itself at `max-height: 100%` (line ~1247, and its comment explains
   why 100% and not 80dvh) — no second copy of that here. */
.dr-note-repeat { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.dr-note-list {
  list-style: none; margin: 0; padding: 0;
  overflow-y: auto; flex: 1 1 auto; min-height: 0;
  -webkit-overflow-scrolling: touch;
}
.dr-note-panel-actions { display: flex; justify-content: flex-end; }
@keyframes dr-note-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes dr-note-rise { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
html.dr-note-open, html.dr-note-open body { overflow: hidden; }

/* Card actions — a borderless Delete on the LEFT, small black Done on the RIGHT.
   Delete removes the deficiency; the card-head × just collapses it. */
.dr-card-actions {
  display: flex; align-items: center; justify-content: space-between;
}
.dr-card-actions .dr-done-btn {
  min-width: 0; height: 40px; padding: 0 var(--s5);
  background: var(--black); color: var(--white);
  font-size: 13px; font-weight: 600; text-transform: none;   /* match Delete: same size, not all-caps */
}
.dr-card-actions .dr-done-btn:active { background: #000; }
/* Done confirmed — green fill + a leading check glyph (portal success green). The rail
   then glides to the "New deficiency" card. (2026-07-19) */
.dr-card-actions .dr-done-btn.is-done { background: #16A34A; color: var(--white); }
.dr-card-actions .dr-done-btn.is-done:active { background: #12833B; }
.dr-card-actions .dr-done-btn.is-done::before { content: '\2713\00a0\00a0'; font-weight: 700; }
/* Gated Done — a brief shake when the card has no photo AND no field yet. */
@keyframes dr-done-shake { 0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-4px)} 40%,80%{transform:translateX(4px)} }
.dr-card-actions .dr-done-btn.is-shake { animation: dr-done-shake 0.4s var(--ease-out, ease); }

/* CTA press-flash (2026-07-22) — a fast darken-and-back on the red "New deficiency" card each time
   it's clicked, so hammering it to add several in a row visibly registers even though the card stays
   anchored and the new deficiency slides in off to the left. Uses `filter` (not transform) so it
   never fights the coverflow's scale/opacity. JS re-triggers it per click via an offsetWidth reflow. */
@keyframes dr-cta-tap { 0% { filter: brightness(1); } 28% { filter: brightness(0.82); } 100% { filter: brightness(1); } }
.dr-capture.dr-tapped { animation: dr-cta-tap 0.16s var(--ease-out, ease); }
.dr-delete-btn {
  background: none; border: 0; box-shadow: none; cursor: pointer;
  padding: 8px var(--s2);
  color: var(--accent);            /* red text — a destructive action, always reads red */
  font-family: inherit; font-size: 13px; font-weight: 600;
  -webkit-tap-highlight-color: transparent;
  position: relative;              /* containing block for the tap overlay below */
}
/* 44px TAP TARGET, VERTICAL ONLY (2026-07-27). The button paints 32px — hit-tested 32,
   under the floor, on the one control in the card that DESTROYS work. The height is
   stated outright rather than derived from padding: padding is what put it at 32 in the
   first place, and a padding-based target silently re-shrinks whenever the type changes.
   Centred on the button, so the 6px it gains above and below is symmetric; measured
   headroom is 15px up and 121px+ down, so neither edge reaches another control.
   Horizontally it is held to the button's own box (left/right 0) because Done sits
   immediately beside it in the same flex row — bleeding sideways would put a delete
   target under the button that means "keep". */
.dr-delete-btn::before {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
@media (hover: hover) and (pointer: fine) { .dr-delete-btn:hover { color: #C42A2F; } }
.dr-delete-btn:active { color: #C42A2F; }


/* ============================================================
   6. LAPTOP (≥1025px) — the roll-out layer. Snap is hidden (shared
   convention); the library button is the lead action; the list and
   cards get a comfortable measure instead of full width.
   ============================================================ */
@media (min-width: 1025px) {
  .dr-capture { max-width: 420px; }     /* laptop: only Library shows, don't span full width */
  .dr-list { max-width: 760px; }
  /* 2026-07-22 — LIST view spans the full content column (flush to the right gutter),
     matching the step head, the action bar and the Preview report button. The 760/420
     caps above left the list rows + the New-deficiency CTA visibly short of the gutter
     while every other element on the page reached it — reading as a misalignment, not a
     measure. Scoped to List only; Grid keeps its 760 measure and Cards its 296 coverflow. */
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-list,
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-capture { max-width: none; }
  .dr-meta-grid { grid-template-columns: 1fr 1fr; gap: var(--s4) var(--gr-gutter); }
  /* Tile sizing is orientation-driven off --dr-ph-h; no per-width override needed. */
  /* Grid view (2026-07-22, owner): 4 equal columns spanning the FULL content column, flush to the
     left and right gutters (was capped at the 760px measure with auto-fill ~200px tiles, so it sat
     short of the gutter at ~3 tiles). max-width:none lets it reach the gutters like the List view. */
  .dr-defs-step.is-grid .dr-list {
    max-width: none;
    grid-template-columns: repeat(4, 1fr);
  }
}


/* ============================================================
   COMPACT RE-SCALE (2026-07-18) — the open/swipe card runs ~13% tighter so it fits
   the mobile viewport with room. Card stays FULL WIDTH; only vertical size drops:
   smaller field height + font, tighter gaps/padding, shorter actions + caption.
   Scoped to .dr-card so the rest of the portal is untouched.
   ============================================================ */
/* The portal-wide "one column of fields" rule (portal.css, ≥641px) caps a .field at half the
   content column. Inside a deficiency CARD the field must fill the card — the card is already a
   single narrow column and halving it would strand the inputs at ~190px. (2026-07-22) */
.dr-card .field { max-width: none; }
.dr-card { padding: var(--s3); gap: var(--s3); }
.dr-fields { gap: var(--s2); }
.dr-card .input { padding: 10px 12px; font-size: 16px; min-height: 0; }   /* back to near-full field height */
/* Severity select — no dropdown chevron (owner's call 2026-07-19); reclaim the
   reserved right space so it matches the text fields' 12px padding. Still a working select. */
.dr-card select.input { padding-right: 12px; background-image: none; }
/* The severity swatch rule (a square colour chip on the right of the card's Severity field,
   added 2026-07-21) went with that FIELD on 2026-07-29 — the card no longer renders a
   `.dr-sev-sel`, so the rule matched nothing. Severity colour now reads off the D-badge
   alone, which is the only control. */
.dr-card-head { gap: var(--s2); }
.dr-card-actions .dr-done-btn { height: 40px; padding: 0 var(--s3); }   /* ~12% narrower — trimmed --s4→--s3 (2026-07-21) */
.dr-card-actions .dr-delete-btn { padding: 8px var(--s2); }
/* (2026-07-21 — `.dr-ph-cap { min-height: 30px }` removed: it reserved height for the
   old solid caption row; the note is an overlay now and must not add height.) */

/* Nav is now shorter (10px pad + 40px buttons), so the deficiency step reserves a
   smaller bottom band → that reclaimed height goes to the card. */
/* 2026-07-21 — the bottom reserve here is GONE. It held a band clear for the action bar back
   when the bar was pinned and floated over the step. The bar is IN FLOW inside the step now,
   so this padding was pushing it ~92px UP off the bottom of the screen: on an empty
   Deficiencies step the buttons floated in the middle of a white void instead of sitting at
   the bottom the way a short step should. The bar carries its own bottom padding (safe-area /
   24px), so nothing needs reserving on its behalf. */

/* List view: taller rows (bigger photo preview) — every row + the New-deficiency CTA row
   grow to the same height, photo or not. */
.dr-item-row { min-height: 76px; }
.dr-defs-step:not(.is-grid):not(.is-cards) .dr-snap { min-height: 78px; }   /* 2026-07-20 — shortened 15% (was 92px). List row only. */

/* ── DESKTOP LIST ROWS −30% (owner 2026-07-22, LAPTOP/DESKTOP ONLY — mobile is deliberately
   untouched at his instruction). The list is the "snapshot of everything" view, so fitting more
   of it on one screen is the whole point; Grid prioritises the photo and Cards carries the full
   detail, which is the balance we're keeping. 92px → 64px measured (row min-height 76→48 plus the
   unchanged 2×8px padding), taking ~6 visible rows at 1440×900 to ~9. The 76px photo TILE is what
   actually drives the row height, so it has to come down with it or nothing moves. The row stays
   a 64px tap target, well clear of the 44px floor. */
@media (min-width: 861px) {
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-row { min-height: 48px; }
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-thumb-wrap { width: 48px; height: 48px; }
  /* The CTA row inherits the SAME height as the rows it sits among (owner) — it is one of the list's
     rows, not a separate element, so it must never be sized independently again. */
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-snap { min-height: 64px; }
}
/* NOTE: the desktop badge shrink lives AFTER the base List badge rule below, not in the block
   above — both selectors have identical specificity, so source order is the only tie-breaker and
   putting it here would silently lose to it. */

/* List D-badge overlays the tile top-left (grid-style, sized down), so Location + Severity
   own the full meta width. Scoped to List so Grid/Cards keep their own badge sizes. */
.dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-row .dr-num {
  position: absolute; left: 0; top: 0; z-index: 2;
  min-width: 0; height: 22px; padding: 0 6px; font-size: 11px;
}
/* Desktop: badge shrinks with the 48px tile it overlays (see the −30% row block above). Must stay
   AFTER the rule it overrides — identical specificity, so source order decides. */
@media (min-width: 861px) {
  .dr-defs-step:not(.is-grid):not(.is-cards) .dr-item-row .dr-num { height: 18px; padding: 0 5px; font-size: 10px; }
}
/* New-deficiency CTA label — ALL CAPS; sized by --dr-cta-size so it matches the "+"
   (label's prior clamp × 0.8 = 20% smaller, 2026-07-21). */
.dr-defs-step:not(.is-grid):not(.is-cards) .dr-snap .dr-snap-label {
  font-size: var(--dr-cta-size); font-weight: 800; letter-spacing: -0.01em; text-transform: uppercase;
}

/* 2026-07-20 — the `@media (display-mode: browser)` COMPRESSION BLOCK that lived here is GONE.

   It trimmed roughly 106px out of every element on this form — photo cell, card padding, row
   gap, head, fields, footer buttons — whenever the app ran in a browser tab rather than as an
   installed app. The reasoning was sound for a layout that could not scroll: iOS browser chrome
   costs somewhere between 110 and 215pt depending on browser, version, whether the toolbar has
   collapsed, and whether an "open in app" banner is showing, so the card had to be shrunk to
   survive the worst case.

   With the document scrolling, none of that height has to be found. Content that does not fit
   scrolls, in a tab and in the installed app alike, so the form no longer needs to know which
   one it is — and the crew get the full-size card everywhere instead of a squeezed one in the
   browser. The touch targets this block deliberately protected (44px minimum, gloved hands on a
   jobsite) are untouched by the removal: they were never what it shrank. */


/* ============================================================
   DESKTOP MULTI-CARD COVERFLOW (2026-07-21). Phones keep the one-full-card swipe.
   At >=861px, fix each card to its DESIGN width (296px = the 360px phone's content
   column, 360 - 2x32.4) so MULTIPLE full cards show in the full-bleed rail and scroll
   left/right. The coverflow controller still scales/ghosts by distance, so the centre
   3-4 stay in focus and the edges ghost, bleeding the gutters. Math (gap = the rail's
   existing 12px): floor((vw+12)/(296+12)) -> 1280=4, 1440~5, 1920=6. No invented widths.
   .dr-defs-step.is-cards .dr-list overrides the 1025px `.dr-list{max-width:760}` cap by
   specificity, so the rail stays full-bleed. ============================================ */
@media (min-width: 861px) {
  .dr-defs-step.is-cards .dr-list { max-width: none; }
  /* CARD WIDTH — 360px (owner 2026-07-22, "a tad too big"). It had been 410px, sized when the card
     carried ×1.4-scaled content; now that the fields follow the portal's own 38px/12px convention
     the card needs ~258px of chrome, not 294, so 410 was holding space nothing uses.
     COUNTS ARE MEASURED, not carried over from the note this replaced: the rail is FULL-BLEED
     (max-width:none), so cards-in-view is viewport ÷ (card + 12px gap), NOT content-column ÷ card.
     At 1440 that is 3.87 in view, up from 3.41 at 410px; at 3440, 9.25. (The old "996 ÷ 410 = 2.4"
     figure measured the content column, which this rail deliberately does not use.) The red "New
     deficiency" card tracks the same token so the two can never desync. */
  .dr-defs-step.is-cards .dr-item,
  .dr-defs-step.is-cards .dr-capture { flex: 0 0 var(--dr-card-w, 360px); }

  /* FIT-TO-HEIGHT — the card fills from the rail top down to the BACK/PREVIEW bar and never tucks
     under it, so the whole card is on screen and the page never scrolls. --dr-card-fill is measured
     live by fitCards() in JS (nav.top − rail.top); 584px is the pre-measure fallback. The card is a
     flex column so the photo area flexes to fill the space left below the fields.

     CEILING (2026-07-22) — fit-to-height had a floor (360px in fitCards) but NO ceiling, so on a tall
     screen the card kept stretching: measured 410x895 on a 900x1200 tablet, 410x1135 on a 3440x1440
     ultrawide, 410x1855 at 4K. The phone card's own proportion is 1.60 (474px tall at 296px wide), so
     past that the card stops being a card and becomes a ribbon. --dr-card-ratio caps it, DERIVED
     from the card width rather than picked.
     RATIO 1.60 → 1.45 (owner 2026-07-22, "cards feel a tad too big"), alongside width 410 → 360.
     1.60 was the PHONE card's proportion, the right reference while the desktop card was a ×1.4
     blow-up of it. It no longer is — the fields follow the portal's own laptop convention now — so
     the desktop card is its own component and 1.45 matches the chrome it actually has.
     360 × 1.45 = 522px, which becomes BOTH the ceiling and the height at 1440×900, since the
     fit-to-height measurement there (595) is now above the cap rather than below it. */
  .dr-defs-step.is-cards { --dr-card-ratio: 1.45; }
  .dr-defs-step.is-cards .dr-item {
    height: min(var(--dr-card-fill, 584px), calc(var(--dr-card-w, 360px) * var(--dr-card-ratio, 1.45)));
    min-height: 0;
    display: flex; flex-direction: column;
    align-items: stretch;   /* the base cards rule centers the card (align-items:center), which shrank
                               .dr-card to its content width and left ~49px dead space each side — the
                               real cause of the "too much left/right padding". Stretch fills the card. */
  }
  /* The red "New deficiency" CTA is a SIBLING of the cards, not a descendant — it must read the
     same capped height or it desyncs from the card the moment the ceiling bites. */
  .dr-defs-step.is-cards .dr-capture {
    height: min(var(--dr-card-fill, 584px), calc(var(--dr-card-w, 360px) * var(--dr-card-ratio, 1.45)));
    min-height: 0;
  }
  .dr-defs-step.is-cards .dr-item > .dr-card { flex: 1 1 auto; min-height: 0; }

  /* PHOTOS → a 2-COLUMN GRID inside the card (desktop), filling the room below the fields and
     scrolling VERTICALLY inside the card only (never the page). Replaces the horizontal strip;
     the rail⇄grid toggle icon and the strip edge-fades go with it. */
  .dr-defs-step.is-cards .dr-photos-wrap { flex: 1 1 auto; min-height: 120px; height: auto; }
  .dr-defs-step.is-cards .dr-card-photos {
    display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2);
    grid-auto-rows: min-content; align-content: start;
    height: 100%; overflow-x: hidden; overflow-y: auto;
    scroll-snap-type: none; touch-action: pan-y;
  }
  .dr-defs-step.is-cards .dr-ph-cell { width: auto; height: auto; scroll-snap-align: none; }
  .dr-defs-step.is-cards .dr-ph { height: auto; aspect-ratio: 1 / 1; }
  /* SINGLE PHOTO — spans both columns and fills the height left above the add tile, instead of
     sitting in one small square with a hole beside it. `contain` is mandatory here: the portal law
     is that a photo is never cropped, so it letterboxes into the larger box rather than filling it. */
  /* ⚠ ONE PHOTO → ITS TRUE SHAPE, WITHOUT THE CARD LEAVING THE VIEWPORT (owner 2026-07-29:
     "no crop", and then "no viewport exceeding — laptop devices should not have changed").
     THE FIRST ATTEMPT WAS WRONG AND IS RECORDED SO IT IS NOT REPEATED: releasing the item's
     `height` let the card grow to the photo (a 9:16 made it 897px), which fixed the crop but
     broke the rule the fit-to-height layout exists for — the card ran past the viewport and
     cards view started scrolling the page, putting Delete/Done below the fold. Growing the card
     is not the answer.
     WHAT WAS ACTUALLY WRONG: the cell was `height: 100%` of the photo region with the image
     `object-fit: contain`, so a portrait letterboxed inside a landscape-ish box — the empty bars.
     The fix is to give the BOX the photo's aspect and cap it to the room available, so it scales
     DOWN instead of the card scaling up. No bars (box ratio == image ratio), no crop, and the
     card is exactly the height it always was.
     `margin-inline: auto` centres a box narrower than the column once the height cap bites.
     `object-fit: contain` stays as the backstop: if `--ar` is ever missing, the photo letterboxes
     rather than crops, because that law does not bend.
     A SECOND BUG WENT WITH IT: at 336px wide a 3:4 photo computed 448px and OVERFLOWED the fixed
     region, and the caption strip is `position: absolute; bottom: 0` — so it was pushed onto the
     card's action row. MEASURED: `elementFromPoint` at the strip's centre returned
     `.dr-card-actions`, i.e. visible but unclickable, which is why the per-photo comment
     "disappeared". A capped box cannot overflow, so the strip stays inside the photo.
     SCOPE: this block is `min-width: 861px` — tablet and laptop. The phone cards view keeps its
     fixed region and is untouched ("tablet ok but not mobile"). */
  /* SINGLE PHOTO — spans both columns and fills the height left above the add tile, instead of
     sitting in one small square with a hole beside it. `contain` is mandatory here: the portal law
     is that a photo is never cropped, so it letterboxes into the larger box rather than filling it.
     ⚠ REVERTED HERE 2026-07-29 after four attempts to make the box take the photo's aspect. Each
     one traded away something the owner then rejected: growing the card left the viewport; capping
     the box shrank the photo's WIDTH inside the card; and every percentage cap resolved to nothing
     because this chain has no definite height above it. His requirement is the photo keeps its
     full width in the card, with the comment as a fill strip pinned inside the photo's bottom edge
     the way the Weekly Report bento does it — which is a caption-strip change, not a photo-box
     change. Do not re-open the box sizing to solve the caption. */
  .dr-defs-step.is-cards .dr-card-photos.is-single { grid-template-rows: 1fr auto; }
  .dr-defs-step.is-cards .dr-card-photos.is-single .dr-ph-cell { grid-column: 1 / -1; min-height: 0; }
  /* ⚠ NO `height: 100%` HERE (2026-07-29). It stretched the lone photo's figure past the space
     above the fields, and the caption strip is pinned INSIDE the figure's bottom edge — so the
     strip landed under the Delete/Done row and the per-photo comment could not be opened.
     MEASURED: `elementFromPoint` at the strip returned `.dr-card-actions`.
     Without the override the figure keeps the base `height: var(--dr-ph-h)` — a DEFINITE height
     that fits the card, which is also why every percentage cap tried here failed. The photo keeps
     its FULL WIDTH (the owner's requirement — it must not narrow inside the card) and stays
     uncropped via `contain`; it simply does not claim extra height it has nowhere to put. */
  .dr-defs-step.is-cards .dr-card-photos.is-single .dr-ph {
    aspect-ratio: auto;
    height: var(--dr-ph-h);   /* the DEFINITE region height — see the note above */
  }
  .dr-defs-step.is-cards .dr-card-photos.is-single .dr-ph img { width: 100%; height: 100%; object-fit: contain; }
  .dr-defs-step.is-cards .dr-card-photos.is-single .dr-ph-add { grid-column: 1 / -1; aspect-ratio: auto; height: 64px; }
  .dr-defs-step.is-cards .dr-ph-add { width: auto; height: auto; aspect-ratio: 1 / 1; }
  /* Empty card → the lone add box spans both columns AND fills the whole (tall) photo area as one
     big drop zone, instead of sitting small at the top with a void below. A single 1fr row does it;
     once real photos exist the grid reverts to min-content rows (photos flow top-down, scroll). */
  .dr-defs-step.is-cards .dr-card-photos:has(.dr-ph-add:only-child) { grid-auto-rows: 1fr; }
  .dr-defs-step.is-cards .dr-ph-add:only-child {
    grid-column: 1 / -1; width: auto; height: 100%; aspect-ratio: auto; min-height: 120px;
  }
  .dr-defs-step.is-cards .dr-photoview-toggle { display: none !important; }
  .dr-defs-step.is-cards .dr-photos-wrap::before,
  .dr-defs-step.is-cards .dr-photos-wrap::after { display: none; }

  /* SAME AS MOBILE, UNIFORMLY BIGGER (2026-07-22) — one scale factor off the phone card (card is
     410/296 = ~1.4× the phone's), applied to EVERY dimension so the proportions match the phone
     exactly, just larger. (Earlier this inflated padding 2× while font grew 1.4× → the side padding
     blew out; corrected to a single 1.4× so mobile 12px padding → 16px, not 24px.)
     Phone base → ×1.4:  padding/gap 12→16 · field 16px/10·12 → 22px/14·16 · Done 40→56 · badge
     44·28 → 60·38 · time 12→17 · delete 13→18 · add-icon 30→42. */
  /* ── THE ×1.4 CARD SCALE IS RETIRED (owner 2026-07-22) ────────────────────────────────────────
     The desktop card used to scale every part of itself ×1.4 off the phone card. That put its text
     FIELDS at 22px/61px against the portal's own laptop convention of 12px/38px — 83% bigger type
     and 55% taller boxes than every other form the crew uses. A form field is a portal-wide
     component: it has to read as the same control on every screen or the app stops feeling like one
     app. Measured from live weekly / toolbox / orientation fields at 1440 — all three identical at
     12px, padding 10px 12px, 38px tall.
     The overrides are DELETED rather than re-specified, so the card simply inherits the portal's own
     .dr-card rules (padding/gap --s3, head gap --s2, Done 40px, badge 44x28/12px) and can never
     drift from them again. Only three deliberate exceptions remain, each justified below.
     The card KEEPS its footprint — 410px wide, fit-to-height, 1.60 cap — so nothing already
     approved moves; the ~50px reclaimed goes to the photo grid, which is the actual content. */
  .dr-defs-step.is-cards .dr-card .input { font-size: 12px; padding: 10px 12px; }   /* laptop convention (base .dr-card is 16px, the PHONE size) */
  .dr-defs-step.is-cards .dr-card select.input { padding-right: 36px; }             /* chevron gutter, as portal-wide */
  .dr-defs-step.is-cards .dr-card-time { font-size: 12px; }
  /* EXCEPTION 1 — collapse chevron stays 44px. ≥861 includes the 900px tablet, which can be a
     TOUCH device, so this one keeps the 44px tap floor rather than following the mouse-sized chrome. */
  .dr-defs-step.is-cards .dr-card-collapse { width: 44px; height: 44px; }
  /* EXCEPTION 2 — the add-photo drop zone is a target, not a control: it stays generous. */
  .dr-defs-step.is-cards .dr-ph-add { font-size: 16px; }
  .dr-defs-step.is-cards .dr-ph-add .dr-ph-add-ico svg { width: 42px; height: 42px; }

  /* CTA label scales with the wider card (was 30px, sized to the old 296px card). */
  .dr-defs-step.is-cards .dr-snap .dr-snap-label { font-size: 40px; }
  /* (The page-picker `padding-inline: 0` override that lived here — the 2026-07-22 "double-inset
     fix", scoped to ≥861 — is GONE, 2026-07-26. It zeroed a padding that should never have been
     applied above 640 in the first place, so it fixed ≥861 and left 641–860 double-inset. The
     padding is now scoped to ≤640 at its source; see the note beside that rule. Nothing replaces
     this here.) */
}


/* ============================================================
   10. PRINT  (2026-07-27)
   ------------------------------------------------------------
   THIS IS NOT A SECOND REPORT GENERATOR. The deficiency report's real output is the
   jsPDF export, and unlike the other four forms this one has no `.dr-print-only`
   letterhead — building one would duplicate the PDF and immediately drift from it.

   What this block is for: until now deficiency-report.css had NO `@media print` at
   all, so `[data-print-hide]` — which every other form's print block is what actually
   hides — did nothing here. Confirmed under emulated print media: a Cmd-P of this
   page emitted the nav, the page picker, the view switch and the buttons as ink.
   That is the whole defect, and hiding the chrome is the whole fix.

   The rail also comes out of its horizontal scroller. On paper an `overflow-x` box
   prints only the slice that happens to be scrolled into view, so a 12-deficiency
   report would print as one card; `visible` lets the list run down the page.
   ============================================================ */
@media print {

  @page { size: letter; margin: 0.5in; }

  html, body { background: #fff; color: #191A1C; width: 7.5in; }
  .portal { font-size: 10.5pt; line-height: 1.5; }

  /* --- Interactive chrome / screen-only affordances --- */
  [data-print-hide],
  .portal-nav,
  .wizard-nav,
  .dr-view-switch,
  .dr-select-toggle,
  .dr-trash-btn,
  .dr-ctx,
  .dr-capture,
  .dr-card-actions,
  .dr-card-collapse,
  .dr-sev-pick,
  .dr-ph-add,
  .dr-ph-remove,
  .dr-photoview-toggle,
  .btn,
  .btn-icon,
  .btn-link {
    display: none !important;
  }

  /* Let every deficiency reach the page instead of one scrolled-to slice. */
  .dr-list {
    overflow: visible !important;
    scroll-snap-type: none !important;
    flex-direction: column !important;
    transform: none !important;
  }
  .dr-item {
    break-inside: avoid;
    transform: none !important;
    opacity: 1 !important;
  }
}

/* ---- ADD-PHOTOS ICON IN THE CARD HEAD (owner 2026-07-29) ------------------------------------
   Replaces the grey `.dr-ph-add` tile once a photo exists, on EVERY device — the tile now only
   renders as the empty state. Sits beside the collapse chevron; `margin-left: auto` puts it and
   the controls after it on the right without depending on how many items the head holds.
   Hidden until the card actually has a photo (`.has-photos`, set in renderCardPhotos), because
   before that the big drop zone is the way in and two affordances would compete. */
.dr-head-add { display: none; }
.dr-card.has-photos .dr-head-add {
  display: inline-flex; align-items: center; justify-content: center;
  margin-left: auto;
  width: 32px; height: 32px;
  color: var(--text-soft, #6B6B6B);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* 44px tap target without changing the painted box — the technique .repeat-remove uses. */
.dr-card.has-photos .dr-head-add { position: relative; }
.dr-card.has-photos .dr-head-add::before { content: ""; position: absolute; inset: -6px; }
@media (hover: hover) and (pointer: fine) {
  .dr-card.has-photos .dr-head-add:hover { color: var(--text); }
}
