/* Pull list — shared by the crew Pull tab and the manager house drawer.
   Every class is `pl-` prefixed on purpose: this markup is injected INTO two
   different shells, and a bare `.top` or `.add` here would silently restyle
   whichever app it landed in (qa:pull-list pins that every selector opens on
   pl-). The look is the Apple-style contract (boards Main / Tokens, approved
   2026-09-05): one white card on the grey ground, hairline rows, a 24px tick, a
   44px camera square, one accent for every control. NOT ONE COLOUR IS TYPED
   HERE — every --pl-* below is an alias of a --sys-* token in system.css, which
   both shells load first, so the sheet is the same object in both apps by
   construction. Green is a status colour and nothing on this sheet is a status
   today; the alias stays so the next status reaches for it instead of a hex. */
.pl-root {
  --pl-ink: var(--sys-ink);
  --pl-muted: var(--sys-muted);
  --pl-faint: var(--sys-faint);
  --pl-line: var(--sys-line);
  --pl-card: var(--sys-panel);
  --pl-field: var(--sys-fill);
  --pl-fill-2: var(--sys-fill-2);
  --pl-rail: var(--sys-track);
  --pl-press: var(--sys-press-a);
  --pl-accent: var(--sys-accent);
  --pl-green: var(--sys-green);           /* STATUS only — never a button, a link or a tick */
  --pl-orange: var(--sys-orange);
  --pl-orange-soft: var(--sys-orange-soft);
  --pl-red: var(--sys-red-ink);
  --pl-red-soft: var(--sys-red-soft);
  --pl-placeholder: var(--sys-placeholder);
  --pl-shadow: var(--sys-shadow-card);
  --pl-r-card: var(--sys-r-card);
  --pl-r-control: var(--sys-r-control);
  --pl-r-pill: var(--sys-r-pill);
  display: block;
  color: var(--pl-ink);
  -webkit-tap-highlight-color: transparent;
}

/* ---- the card. Counter, bar, rooms, rows and the per-room field are ONE
       white object; the notices sit inside it as strips. Borderless, lifted by
       the hairline shadow; overflow clips the last row's press highlight to the
       corners. Both apps draw exactly this — the manager's drawer drops the
       walls (bottom of this file) because it already sits on a card. ---- */
.pl-sheet {
  background: var(--pl-card);
  border: 0;
  border-radius: var(--pl-r-card);
  box-shadow: var(--pl-shadow);
  overflow: hidden;
  /* clip, where the engine has it: the same corners, but it does not make the
     card a scroll container — and a scroll container is what kept the Undo strip
     below (.pl-notice, position:sticky) from ever sticking in the crew app.
     WebKit before 16 ignores it and keeps `hidden`. 2026-09-17. */
  overflow: clip;
  margin: 0 0 12px;
}

/* ---- progress: the one number the stager checks from across the unit ---- */
.pl-prog { padding: 11px 16px 8px; }
/* 44 tall because the Edit text action is a 44px target; the number's own line
   box is ~34, so the 5px either side is what puts it 16 from the card's top. */
.pl-prog-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; min-height: var(--sys-tap); }
.pl-tools { display: flex; align-items: center; gap: 10px; flex: none; }
.pl-n { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.pl-n b {
  font-size: 28px;
  font-weight: var(--sys-w-bold);
  letter-spacing: -.03em;
  line-height: 1.2;
  color: var(--pl-ink);
  font-variant-numeric: tabular-nums;
}
.pl-n i {
  font-style: normal;
  font-size: var(--sys-t-lead);
  font-weight: var(--sys-w-medium);
  color: var(--pl-muted);
  font-variant-numeric: tabular-nums;
}
/* The save line is a STATE, not a label: it is empty while everything is on
   the server, orange while this phone is holding work, red — and it never
   times out — while a change exists nowhere but this screen. */
.pl-save {
  font-size: var(--sys-t-body);
  font-weight: var(--sys-w-semibold);
  color: var(--pl-muted);
  text-align: right;
  flex: none;
  font-variant-numeric: tabular-nums;
}
.pl-save:empty { display: none; }
.pl-save.waiting { color: var(--pl-orange); }
.pl-save.full { color: var(--pl-red); }
.pl-bar { margin-top: 5px; }
.pl-bar i { display: block; width: 0; transition: width .22s ease; }

/* The sheet STARTS empty. "0 of 0" over a flat bar is a number pretending to
   be progress, so with nothing on it the card says the state in three words. */
.pl-zero { display: none; margin: 0; font-size: var(--sys-t-lead); font-weight: var(--sys-w-medium); color: var(--pl-muted); }
.pl-prog.pl-none .pl-n,
.pl-prog.pl-none .pl-bar { display: none; }
.pl-prog.pl-none .pl-zero { display: block; }

/* ---- Edit: the only door to a Remove, and it is at the TOP of the sheet, as
       far from the tick as this card goes. A text action (system.css
       .sys-btn-text paints it); "Done" is the same word, one weight up. ---- */
/* "Edit" sits on the card's 16px gutter line, like every row under it. That used
   to be done with `margin-right: -8px` against the button's own 8px of text
   padding — and a negative margin is scrollable overflow: in the manager's drawer,
   where .pl-prog's gutters are zeroed (bottom of this file) and there is no padding
   for the 8px to hang in, it made the ROOT 8px wider than its own box. Measured in
   real Chrome 2026-09-09: scrollWidth 334 against a clientWidth of 326 at 390, and
   346 against 338 at 402, in every mode — 8px of sideways rubber-banding inside a
   drawer that is meant to scroll only up and down. The crew never saw it, because
   its card has the padding to absorb it. Dropping the padding reaches the same
   pixel from the INSIDE and overflows nothing; min-width carries the target back
   to 44 (it painted 43.14 wide), and text-align keeps the word itself on the line.
   Nothing here moves on screen. */
.pl-edit { flex: none; padding-right: 0; min-width: var(--sys-tap); text-align: right; }
.pl-edit.on { font-weight: var(--sys-w-semibold); }

/* Editing is a MODE, and it looks like one: the row stops offering a tick, the
   typing fields step out of the way, and every Remove appears at once. */
.pl-root.pl-editing .pl-it { cursor: default; }
.pl-root.pl-editing .pl-it:active { background: none; }
.pl-root.pl-editing .pl-tick { opacity: .35; }
.pl-root.pl-editing .pl-add,
.pl-root.pl-editing .pl-addroom { display: none; }

.pl-rm, .pl-rmroom {
  border: 0;
  background: var(--pl-red-soft);
  color: var(--pl-red);
  border-radius: var(--pl-r-pill);
  padding: 0 12px;
  font: inherit;
  font-size: var(--sys-t-body);
  font-weight: var(--sys-w-semibold);
  flex: none;
  /* Anchors the invisible tap expanders below. */
  position: relative;
}
.pl-rm { min-height: 32px; }
.pl-rm:active, .pl-rmroom:active { opacity: .7; }
.pl-rmroom {
  min-height: 26px;
  /* A 44pt target ACROSS as well as down. The expander below may not grow
     sideways (it would land on the chip a thumb aims with), so the width has to
     live in the box: a 15px glyph in 10px of padding is about 30px, and a target
     that narrow cannot be reached from the side at all. The glyph does not move
     — it centres in the wider box, still on the heading's right — and nothing
     else in the heading shifts, because this is the last item in the row. */
  min-width: var(--sys-tap);
  padding: 0 10px;
  font-size: var(--sys-t-small);
}

/* ---- the two Removes are 44pt targets, and still look small ----
   They delete things, so the INK stays small; a 12-second Undo backs them, so a
   mis-tap costs a tap while a miss costs a stager in gloves three tries. Only
   the target grows, and — the lesson .pl-lib taught — never sideways, because
   an expander that grows toward a neighbour steals the neighbour's taps.
   • .pl-rm sits alone at the right end of a 52px row (in edit mode it REPLACES
     the camera), 8px of row padding above and below its centred 32px box, so
     it spans y10-42. 32 + 6 + 6 = 44, y4-48: inside the row on both sides.
   • .pl-rmroom sits in a room heading padded 14px top / 8px bottom. Its 26px
     box is the tallest thing on that line, so the heading is 14+26+8 = 48 and
     the button spans y14-40. 26 + 10 + 8 = 44, y4-48: it ends exactly at the
     heading's bottom edge, which is the border line above the first row, and
     takes nothing from that row's own target.
   Neither grows left or right at all: 0 keeps them clear of the item name and
   the room's piece count. qa:recovery-wave reads these two lines. */
.pl-rm::after,
.pl-rmroom::after { content: ""; position: absolute; }
.pl-rm::after { inset: -6px 0 -6px 0; }
.pl-rmroom::after { inset: -10px 0 -8px 0; }
/* Name hard left, count and Remove together on the right. */
.pl-roomh em { margin-left: auto; }

/* ---- what just happened, and the way back from it ----
   The dark pill is the app's error vocabulary and it stays; a refusal is not a
   success with a button missing, so it reads red instead. ---- */
.pl-notice {
  display: flex; align-items: center; gap: 10px;
  background: var(--pl-ink);
  color: var(--pl-card);
  border-radius: var(--pl-r-control);
  padding: 8px 8px 8px 12px;
  margin: 8px 16px;
  font-size: var(--sys-t-lead);
  font-weight: var(--sys-w-medium);
  line-height: 1.3;
}
.pl-notice > span { flex: 1; min-width: 0; }
.pl-notice button {
  border: 0; background: var(--pl-card); color: var(--pl-ink);
  border-radius: var(--pl-r-pill); padding: 0 14px; min-height: 36px;
  font: inherit; font-size: var(--sys-t-lead); font-weight: var(--sys-w-semibold);
  flex: none;
  position: relative;
}
.pl-notice button:active { opacity: .8; }
.pl-notice > svg { width: 18px; height: 18px; flex: none; display: block; stroke: currentColor; fill: none; }
.pl-notice.pl-warn { background: var(--pl-red-soft); color: var(--pl-red); font-weight: var(--sys-w-semibold); }
.pl-notice.pl-warn button { background: var(--pl-red); color: var(--pl-card); }
/* The wifi sign-in page: nothing is lost, it has not gone — orange, not red. */
.pl-notice.pl-wifi { background: var(--pl-orange-soft); color: var(--pl-orange); font-weight: var(--sys-w-semibold); }
/* THE UNDO STAYS WHERE THE REMOVE HAPPENED. The strip is drawn at the top of the
   sheet, and Remove is usually tapped far down it: measured after removing the last
   row, Undo sat at y -164 with a 0×0 hit box in both apps, and it expires in 12s —
   a mistaken Remove cost the piece and its photo. Only the undoable strip sticks
   (the red and orange ones are states, not a way back), and only while its own
   card is on screen. 64 = the crew's sticky tab bar (top 6 + 48 tall + its 6px
   ring) + 4. The tab bar pays env(safe-area-inset-top) in its top, so this does
   too, and drops it while the shell has already paid it (html.ni-top).
   2026-09-17. */
.pl-notice:not(.pl-warn):not(.pl-wifi) {
  position: sticky;
  top: calc(64px + var(--sys-safe-t));
  z-index: 3;
}
html.ni-top .pl-notice:not(.pl-warn):not(.pl-wifi) { top: 64px; }

/* ---- the phone has run out of room, and work that stopped trying ----
   Both sit ABOVE the rooms, both carry the same one move out, and neither ever
   goes away on a timer. A colour, an icon, five words (Copy board): red is
   "exists nowhere but this screen", orange is "safe on this phone, not gone
   yet". The ticks stay on screen behind them — nothing here rolls back what she
   did; it says what has not been saved. ---- */
.pl-full,
.pl-stalled {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border: 0;
  border-radius: var(--pl-r-control);
  padding: 8px 8px 8px 12px;
  margin: 8px 16px;
  font-size: var(--sys-t-lead);
  font-weight: var(--sys-w-semibold);
  line-height: 1.3;
}
.pl-full > svg,
.pl-stalled > svg { width: 18px; height: 18px; flex: none; display: block; stroke: currentColor; fill: none; }
.pl-full > span,
.pl-stalled > span { flex: 1; min-width: 140px; }
.pl-full { background: var(--pl-red-soft); color: var(--pl-red); }
.pl-stalled { background: var(--pl-orange-soft); color: var(--pl-orange); }
.pl-full button,
.pl-stalled button {
  border: 0; border-radius: var(--pl-r-pill);
  padding: 0 14px; min-height: 36px;
  font: inherit; font-size: var(--sys-t-lead); font-weight: var(--sys-w-semibold);
  flex: none;
  background: var(--pl-red); color: var(--pl-card);
  position: relative;
}
.pl-stalled button { background: var(--pl-orange); }
.pl-full button:active,
.pl-stalled button:active { opacity: .82; }
/* 36px is what the three strip buttons look like; 44 is what they are. The
   strips are padded 8px, so growing 4px each way stays inside the strip. */
.pl-notice button::after,
.pl-full button::after,
.pl-stalled button::after { content: ""; position: absolute; inset: -4px 0 -4px 0; }

/* ---- rooms: a heading and its rows, inside the one card ---- */
.pl-room { background: transparent; border: 0; margin: 0; }
.pl-roomh {
  margin: 0;
  padding: 14px 16px 8px;
  font-size: var(--sys-t-body);
  font-weight: var(--sys-w-semibold);
  letter-spacing: 0;
  text-transform: none;
  color: var(--pl-muted);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
}
.pl-roomh em {
  font-style: normal;
  font-size: var(--sys-t-body);
  font-weight: var(--sys-w-medium);
  color: var(--pl-muted);
  font-variant-numeric: tabular-nums;
}

/* ---- one row = a tick and a name. The whole row is the target: gloves on, a
       lamp under one arm, a phone in the other hand. ---- */
.pl-it {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 16px;
  border-top: 1px solid var(--pl-line);
  cursor: pointer;
  min-height: 52px;
  box-sizing: border-box;
}
/* ---- the row that slides (2026-09-21) -----------------------------------
   "you can't edit them if you wanna change the text so you have to click in edit
   on top and delete it and add it again but that is not really practical." So the
   two rare things a row needs — its name fixed, its row taken off — live one
   swipe away instead of behind a mode that deletes work to change a word.
   Only the sheet's own rows swipe: the destage record draws its rows as
   .pl-it.pl-itbtn from manager-pull-adapter.js, and those are untouched, which is
   why the flex lives on .pl-slide and not on a changed .pl-it. */
.pl-it.pl-swipeable {
  display: block;
  position: relative;
  overflow: hidden;
  padding: 0;
  cursor: default;
}
.pl-it.pl-swipeable .pl-slide {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 16px;
  min-height: 52px;
  box-sizing: border-box;
  background: var(--pl-card);
  cursor: pointer;
}
/* THE TRAY COMES IN; THE PIECE STAYS PUT. Sliding the whole row the width of two
   buttons is what a Mail message does, and on a Mail message the subject is long
   enough to survive it. Here the row is a tick and one short word — "sofa" —
   which slid clean off the screen, so the thing being renamed or deleted was the
   one thing you could no longer read (seen on a real iPhone, 2026-09-21). The
   tray slides over the right of the row instead, where the camera buttons are,
   and the name never moves. */
.pl-acts {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  /* ABOVE the camera it slides over. The tray comes first in the row and the
     camera's wrap is position:relative, so as the later positioned box the white
     ＋ was painted on top of Delete, which read "De": a tap on the middle of it hit
     the camera, which only closed the tray, and nothing was deleted (UI-149). The
     library button is not positioned, which is why only the camera showed through.
     Closed, the tray is clipped off the row by its overflow, so this never lifts it
     over anything a thumb can reach. Under the sticky Undo strip's 3. */
  z-index: 1;
  display: flex;
  align-items: stretch;
  transform: translateX(100%);
  transition: transform .2s cubic-bezier(.22,.61,.36,1);
  will-change: transform;
}
/* While a thumb is on it the tray follows the finger exactly: no transition, or
   every frame of the drag would be chasing a 200ms animation. */
.pl-it.pl-swipeable.pl-dragging .pl-acts { transition: none; }
.pl-it.pl-swipeable.open .pl-acts { transform: translateX(0); }
.pl-it.pl-swipeable .pl-slide:active { background: var(--pl-press); }
.pl-act {
  border: 0;
  width: 80px;
  font: inherit;
  font-size: var(--sys-t-body);
  font-weight: var(--sys-w-semibold);
  color: #fff;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* Rename is the one she will reach for; Delete is the one that must never be
   mistaken for it, so it sits furthest out and wears the only red on the row. */
.pl-act-edit { background: var(--pl-muted); }
.pl-act-del { background: var(--pl-red); }
.pl-act:active { filter: brightness(.92); }
/* The name is the rename control in Edit mode — the way in for a keyboard and
   for VoiceOver, neither of which can swipe. It is a <button>, so it resets. */
.pl-nmbtn {
  border: 0;
  background: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* The row while its name is being fixed: the field IS the row. */
.pl-renaming .pl-slide {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  min-height: 52px;
  box-sizing: border-box;
}
.pl-rn {
  flex: 1;
  min-width: 0;
  min-height: 44px;
  border: 1px solid var(--pl-line);
  border-radius: var(--sys-r-control);
  background: var(--pl-field);
  padding: 0 12px;
  font: inherit;
  font-size: 16px;   /* anything smaller and iOS zooms the page on focus */
  color: var(--pl-ink);
}
.pl-rn:focus { outline: 2px solid var(--pl-accent); outline-offset: -1px; }
@media (prefers-reduced-motion: reduce) {
  .pl-it.pl-swipeable .pl-acts { transition: none; }
}

.pl-it:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: -2px; }
/* The tick is the focusable checkbox now (pull-list.js rowHtml), not the row: a row
   with role=checkbox made its camera and library buttons unreachable to VoiceOver.
   2026-09-17. */
.pl-tick:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 2px; }
.pl-it:active { background: var(--pl-press); }
/* The circle is system.css's .sys-tick; ticked is the accent with a white check
   drawn in CSS. The ✓ glyph in the markup is for screen readers' benefit and
   is never painted (font-size 0) — a glyph that was transparent-until-done once
   read as "everything's pulled" on a dark card. A CSS check cannot. */
.pl-tick {
  font-size: 0;
  line-height: 1;
  transition: background .12s ease, border-color .12s ease;
}
.pl-it.done .pl-tick { background: var(--pl-accent); border-color: var(--pl-accent); }
.pl-it.done .pl-tick::after {
  content: "";
  width: 5px;
  height: 10px;
  margin-top: -2px;
  border: solid var(--pl-card);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* ---- THE FREE TEXT ON THIS SHEET BREAKS WHEREVER IT HAS TO ----
   A piece name is the one string on this card a person typed, and people type
   model numbers, warehouse codes and names without spaces. A token with no break
   opportunity in it does not wrap — it PAINTS PAST its box, and neither the row
   nor the card is a scroller, so nothing can go and get it. `min-width: 0` is not
   this fix: it lets the flex item shrink, and the glyphs still hang out of the
   smaller box. Measured 2026-09-09 in real Chrome at 390 with a 60-character
   one-token name: the name box reported scrollWidth 479 against clientWidth 294
   in both shells — 185px of the name off the card — and in the MANAGER, where the
   sheet sits in a 326px drawer, the whole root was pushed to 515, which is every
   control on it off the glass. A hyphenated string wraps by itself (a hyphen IS a
   break), which is why the harness deliberately uses one with none. The room
   heading and the note's attribution are the same kind of text and get the same
   rule. Pinned in qa:pull-glass. ---- */
.pl-nm, .pl-roomh, .pl-noteby { overflow-wrap: anywhere; }
.pl-nm { flex: 1; font-size: var(--sys-t-lead); line-height: 1.3; color: var(--pl-ink); min-width: 0; }
.pl-it.done .pl-nm { color: var(--pl-muted); text-decoration: line-through; }
.pl-qty { font-size: var(--sys-t-body); color: var(--pl-muted); font-weight: var(--sys-w-medium); font-variant-numeric: tabular-nums; flex: none; }

/* ---- the camera: system.css's 44px square with the accent "+", and once there
       is a photo it IS the photo. The little square in its bottom-right corner
       is the library — small on purpose: the stager is standing in front of the
       piece, so the shot she is about to take is the common case and the roll
       is the exception. ---- */
.pl-camwrap { position: relative; width: 44px; height: 44px; flex: none; display: block; }
.pl-cam { overflow: hidden; font-family: inherit; font-size: 15px; }
.pl-cam.has { background: var(--pl-fill-2); }
.pl-cam img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pl-camdot { display: flex; color: var(--pl-muted); }
.pl-camdot svg { width: 20px; height: 20px; display: block; stroke: currentColor; fill: none; }
/* ---- the photo cell: the library button, then the camera ----------------
   2026-09-21. The library used to be an 18px ▤ badge in the camera's corner,
   with a tap target measured at 24x21 against the camera's 44x44 seven pixels
   away. A crew member could not hit it — every miss landed on the ＋ and opened
   the CAMERA — and reported that the app could not reach her gallery at all.
   Two real buttons now, side by side, each the 44pt square this app uses
   everywhere: the gallery first, then the camera, which keeps the camera under
   the thumb at the edge of the row where it has always been. (The corner of the
   camera still carries the state badges — saved-on-this-phone, retry — which are
   read, not aimed at, and can share their slot with nothing else now.) */
.pl-cell { display: flex; align-items: center; gap: 8px; flex: none; }
.pl-libbtn { color: var(--pl-muted); }
.pl-libbtn:active { background: var(--pl-fill-2); }
.pl-libbtn svg { width: 21px; height: 21px; }

/* Both apps put a sticky bar above this sheet (the crew's tab pill, the manager's
   House OS tabs) and a bar below it. When the browser scrolls a focused field
   into view it aims for the edge of the scrollport, which is UNDER both of them —
   measured on a 375x667 phone with the keyboard up, the last room's field landed
   behind the tab pill while she was typing in it. This is the browser's own way
   to say "leave room". */
.pl-add input,
.pl-addroom input { scroll-margin-top: 64px; scroll-margin-bottom: 150px; }

/* ---- an upload in flight, ON THE ROW. A modal over the whole sheet is right
       for forty photos and wrong for one: it means standing still in a storage
       unit waiting on a JPEG. This is a hairline UNDER the tile — on top of a
       44px photo it would be unreadable — and the list stays entirely usable
       while it fills. ---- */
.pl-upbar {
  position: absolute; left: 0; right: 0; bottom: -7px;
  height: 4px; border-radius: var(--pl-r-pill);
  background: var(--pl-rail);
  overflow: hidden;
  display: block;
  pointer-events: none;
}
.pl-upbar b {
  display: block; height: 100%; min-width: 4px; width: 0;
  background: var(--pl-accent);
  border-radius: var(--pl-r-pill);
  transition: width .18s linear;
}
.pl-camwrap.up .pl-cam { opacity: .72; }
/* Mid-upload there IS no library button to hide: a picked photo is already on
   the row (pend.blob), so camHtml's `shows` is true and the button is not built.
   The old corner badge needed a rule here; a sibling that does not exist does
   not. (No :has() either — this app still runs on iOS 15.0-15.3, qa:old-webkit.) */

/* ---- a photo this phone is HOLDING. The bytes are on disk, not in a variable
       that dies with the tab — but a waiting photo has to LOOK different from a
       sent one, or "it's on the row" is a promise she finds out about later,
       with the truck already loaded. An orange ring and a small clock in the
       corner where the library button sits: nothing has gone wrong, it just
       hasn't gone yet. (An outline, not a border: it paints above the photo.) ---- */
.pl-camwrap.wait .pl-cam { outline: 1.5px solid var(--pl-orange); outline-offset: -1.5px; opacity: 1; }

.pl-wait {
  position: absolute; right: -3px; bottom: -3px;
  width: 18px; height: 18px; border-radius: 6px;
  border: 0;
  background: var(--pl-orange-soft); color: var(--pl-orange);
  display: flex; align-items: center; justify-content: center;
  font-family: inherit; font-size: 10px; font-style: normal; font-weight: var(--sys-w-bold); line-height: 1;
  pointer-events: none;
}

/* …and when it doesn't land. The picture stays on the row — nothing is lost —
   and the corner button becomes "send it again". Same corner, same expander
   rule as the library badge: it may not grow back over the camera. */
.pl-camwrap.bad .pl-cam { outline: 1.5px solid var(--pl-red); outline-offset: -1.5px; opacity: 1; }
.pl-retry {
  position: absolute; right: -3px; bottom: -3px;
  width: 18px; height: 18px; border-radius: 6px;
  border: 0;
  background: var(--pl-red); color: var(--pl-card);
  padding: 0; margin: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: inherit; font-size: 10px; font-weight: var(--sys-w-bold); line-height: 1;
}
.pl-retry::after { content: ""; position: absolute; inset: -3px -6px -3px -3px; }
.pl-retry:active { opacity: .8; }

/* ---- adding is one text field per room: a quiet fill, no border, 16px, and
       the accent pill beside it (system.css .sys-btn). ---- */
.pl-add { display: flex; gap: 8px; padding: 10px 16px 14px; border-top: 1px solid var(--pl-line); }
.pl-add input,
.pl-addroom input {
  flex: 1; min-width: 0;
  border: 0;
  border-radius: var(--pl-r-control);
  padding: 0 12px;
  min-height: var(--sys-tap);
  box-sizing: border-box;
  font: inherit; font-size: 16px;   /* 16px (--sys-t-field): anything smaller makes iOS zoom the page */
  background: var(--pl-field);
  color: var(--pl-ink);
}
.pl-add input::placeholder,
.pl-addroom input::placeholder { color: var(--pl-placeholder); opacity: 1; }
/* The one focus ring every input in both apps wears: 2px of the accent. */
.pl-add input:focus,
.pl-addroom input:focus { outline: none; box-shadow: 0 0 0 2px var(--pl-accent); }
.pl-add button { flex: none; }

/* ---- and one more field for a room the scope never mentioned. Its own small
       card: the field's fill is the page's grey, so on the bare ground it
       needs the white behind it. ---- */
.pl-addroom {
  display: flex; gap: 8px;
  background: var(--pl-card);
  border-radius: var(--pl-r-card);
  box-shadow: var(--pl-shadow);
  padding: 10px 16px;
  margin: 0 0 12px;
}
.pl-addroom button { flex: none; }

/* ---- the bottom action: a quiet pill while there is pulling left to do, the
       accent once everything is ticked. ---- */
/* THE FOOT NOTE. The margin of the paper sheet: one box, at the end, after the
   rooms and before the finish button — where the owner asked for it. A card like
   every other card (no border, hairline shadow, 18 radius), a 16px field so iOS
   does not zoom the whole sheet when a thumb lands in it, and a line under it
   naming whoever wrote it last, because one shared box needs to say whose words
   these are. 2026-09-07. */
.pl-notebox{margin:14px 0 0;padding:14px;border-radius:var(--sys-r-card);background:var(--sys-panel);box-shadow:var(--sys-shadow-card)}
.pl-notebox label{display:block;margin-bottom:8px;font-weight:var(--sys-w-semibold);font-size:var(--sys-t-body);color:var(--sys-muted)}
.pl-notebox textarea{display:block;width:100%;box-sizing:border-box;min-height:88px;resize:vertical;
  border:0;border-radius:var(--sys-r-control);background:var(--sys-fill);padding:12px;
  font-family:inherit;font-weight:var(--sys-w-regular);font-size:var(--sys-t-field);line-height:1.45;color:var(--sys-ink);outline:none}
.pl-notebox textarea::placeholder{color:var(--sys-placeholder)}
.pl-notebox textarea:focus{box-shadow:0 0 0 2px var(--sys-accent)}
.pl-noteby{margin:8px 2px 0;font-size:var(--sys-t-caption);color:var(--sys-muted)}
.pl-noteby[hidden]{display:none}

.pl-foot { padding: 4px 0 2px; }
.pl-go {
  width: 100%;
  border: 0; border-radius: var(--pl-r-pill);
  min-height: 48px;
  padding: 0 18px;
  font: inherit; font-size: var(--sys-t-lead); font-weight: var(--sys-w-semibold);
  background: var(--pl-fill-2); color: var(--pl-muted);
  transition: background .18s ease, color .18s ease;
}
.pl-go.on { background: var(--pl-accent); color: var(--pl-card); }
.pl-go[disabled] { opacity: .55; }
/* Drawn only while the sheet is finished and this viewer may mark it (b2). */
.pl-foot[hidden] { display: none; }

/* The sheet is tuned for a 390px phone; the narrowest ones (375) give the add
   row a little less air so the field keeps its width beside the pill. */
@media (max-width: 390px) {
  .pl-add, .pl-addroom { gap: 6px; }
  .pl-add button, .pl-addroom button { padding-left: 14px; padding-right: 14px; }
  .pl-full > span,
  .pl-stalled > span { min-width: 120px; }
}

/* There WAS a @media (prefers-color-scheme: dark) block here, and it was the only
   dark mode anywhere near this sheet. Neither shell that renders it has one, so
   on a phone set to dark the pull sheet turned its cards dark while the app
   around it stayed white. Half-dark is worse than either half. If dark mode
   comes back, it has to come back for the SHELL first — a component that goes
   dark on its own just makes a hole in a light screen. */

/* Inside a manager House-OS card the sheet already sits on a card. Drop the
   second layer of walls rather than nesting boxes inside boxes, and pull the
   16px gutters back to the card's own edge. */
.os2-card .pl-root .pl-sheet,
.os2-card .pl-root .pl-addroom {
  background: transparent;
  box-shadow: none;
  border-radius: 0;
  margin-bottom: 0;
}
.os2-card .pl-root .pl-sheet { overflow: visible; }
.os2-card .pl-root .pl-prog { padding-left: 0; padding-right: 0; }
.os2-card .pl-root .pl-it.pl-swipeable .pl-slide,
.os2-card .pl-root .pl-renaming .pl-slide,
.os2-card .pl-root .pl-it,
.os2-card .pl-root .pl-add,
.os2-card .pl-root .pl-roomh { padding-left: 0; padding-right: 0; }
.os2-card .pl-root .pl-notice,
.os2-card .pl-root .pl-full,
.os2-card .pl-root .pl-stalled { margin-left: 0; margin-right: 0; }
/* The sticky Undo strip (above), in the manager's house drawer: the pinned title
   bar there carries the back pill, the address and the tab bar, and ends at 121
   (measured at 390, 2026-09-17) — so the crew's 64 would park the Undo under it.
   The title bar pays env(safe-area-inset-top) and drops it under html.ni-top
   (bento.css), and so does this. */
.os2-card .pl-root .pl-notice:not(.pl-warn):not(.pl-wifi) { top: calc(125px + var(--sys-safe-t)); }
html.ni-top .os2-card .pl-root .pl-notice:not(.pl-warn):not(.pl-wifi) { top: 125px; }
.os2-card .pl-root .pl-addroom { padding: 12px 0 0; border-top: 1px solid var(--pl-line); }

/* ---- the sheet after staging day: a record, not a worksheet ----
   The Pull tab used to disappear the day the status flipped to Active, which is
   the day AFTER the stage — so six weeks later, on destage, the crew loaded the
   furniture back with no list of what had gone in. Past staging both apps now
   render this: the same rooms, the same names, the same counts, and the ticks
   exactly as the crew left them — read-only, because the sheet's numbers all
   mean "pulled FROM storage" and letting anyone untick a row on destage day
   would quietly rewrite the only record of what is actually in the house. */
.pl-record .pl-it { cursor: default; }
.pl-record .pl-it:active { background: none; }
/* …EXCEPT during the destage pass, where the same rows carry the SECOND column
   (`returned`) and are tapped as the furniture comes back onto the truck. Rendered
   as a real <button> then, so it has to look and behave like the live sheet's rows
   again — a control that cannot be told apart from a label is a control nobody
   presses. Everything about `pulled` stays untouched underneath it. */
.pl-record .pl-itbtn {
  cursor: pointer;
  width: 100%;
  background: none;
  border-left: 0; border-right: 0; border-bottom: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.pl-record .pl-itbtn:active { background: var(--pl-press); }
/* Said on the row and not only in the summary: on destage day "nobody ever
   ticked this one" is the difference between a missing piece and a mis-count.
   A status, so it wears the status chip — orange: watch this one. */
.pl-recnever {
  margin-left: auto; flex: 0 0 auto;
  padding: 4px 9px;
  border-radius: var(--pl-r-pill);
  background: var(--pl-orange-soft); color: var(--pl-orange);
  font-size: var(--sys-t-caption); font-weight: var(--sys-w-semibold); line-height: 1.2;
  white-space: nowrap;
}
.pl-recsum {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  flex-wrap: wrap;
  padding: 16px 16px 10px;
}
.pl-recsum b {
  font-size: 28px;
  font-weight: var(--sys-w-bold);
  letter-spacing: -.03em;
  line-height: 1.2;
  color: var(--pl-ink);
  font-variant-numeric: tabular-nums;
}
.pl-recsum span { font-size: var(--sys-t-lead); color: var(--pl-muted); font-weight: var(--sys-w-medium); font-variant-numeric: tabular-nums; }
.os2-card .pl-root .pl-recsum { padding-left: 0; padding-right: 0; }

/* ---- THE PHOTO, FULL SCREEN ----------------------------------------------
   Why this exists: a photo on this sheet used to be a thing you could only
   destroy. The 44px tile on a filled row was the camera, its corner badge was
   the library, and both of them REPLACED — so the tap a person makes to look at
   the sofa was the tap that overwrote Lauren's picture of it. The owner asked
   for a view mode and an edit mode; what he needed was for the tap to mean the
   obvious thing, with the rare actions one level down. This is that level.

   It is a sibling of nothing on this sheet: pull-list.js appends it to
   document.body, because paint() rebuilds the container on every refresh and
   because the manager's drawer keeps a transform (bento.css, animation fill-mode
   `both`), which would make a position:fixed child a prisoner of the drawer.

   ONE SWITCH FOR ONE STATE. Visibility is `hidden` and this single display rule
   — no companion .open class, the way #mdLightbox and #cmViewer both do it,
   because two switches for one state is how an element that reads as gone in JS
   stays on the screen. system.css's [hidden]{display:none!important} is what
   closes it, so nothing here may ever write display:…!important. ---- */
/* THE ALIASES, REPEATED HERE ON PURPOSE. Every --pl-* in this file is declared
   on .pl-root, which is the mount's own container — and this element is appended
   to document.body, outside it, so it inherits none of them. Custom
   properties fail at computed-value time, silently: var(--pl-r-pill) resolved to
   nothing on the ✕ and it has been drawing as a square. Same aliases, same
   --sys-* sources, so the layer is still one object with the sheet. */
.pl-viewer {
  --pl-ink: var(--sys-ink);
  --pl-muted: var(--sys-muted);
  --pl-card: var(--sys-panel);
  --pl-field: var(--sys-fill);
  --pl-red: var(--sys-red-ink);
  /* THE ACCENT, added 2026-09-09 with the check inside the photo — the first
     thing on this layer to need a colour rather than a shape. It went in as
     `var(--pl-accent)` and measured EMPTY on the viewer: the ticked button had no
     background at all (rgba(0,0,0,0)) and its check drew white-on-white, because
     a custom property that fails at computed-value time takes the whole
     declaration with it, silently, and there is nothing on screen to notice.
     Exactly what this block's comment above was written about, recurring the
     moment somebody reached for a token that was not in it. qa:pull-list now
     derives the set from the rules instead of trusting this list. */
  --pl-accent: var(--sys-accent);
  --pl-r-card: var(--sys-r-card);
  --pl-r-pill: var(--sys-r-pill);
}
.pl-viewer {
  position: fixed; inset: 0;
  /* 500: over the crew nav (40) and the manager's house drawer (60), and UNDER
     both apps' toasts (crew 600, manager's legacy 200000) — so opts.toast(), the
     surface this sheet says every refusal on, still reads over an open photo. */
  z-index: var(--sys-z-lightbox, 500);
  display: flex; flex-direction: column;
  background: rgba(10, 10, 9, .96);
}
.pl-vtop {
  position: absolute; top: 0; left: var(--sys-safe-l); right: var(--sys-safe-r); z-index: 3;
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
  padding-top: calc(12px + var(--sys-safe-t));
}
/* Dropped ONLY while the shell has already paid the top inset (html.ni-top, set live
   by public/native-inset.js). This was `html.native-app`, which is on in every
   scroll state — and at the top of a page the shell pays nothing, so the ✕ and the
   counter sat at y 12–56, under the clock, where iOS reads a tap as scroll-to-top:
   the one visible way out of the photo could not be pressed (simulator, crew Pull,
   2026-09-17). With a house open (body locked) the class is on and the ✕ still
   sits 12pt inside a layer that already starts below the status bar. 2026-09-17. */
html.ni-top .pl-vtop { padding-top: 12px; }
.pl-vclose {
  width: var(--sys-tap); height: var(--sys-tap);
  border: 0; border-radius: var(--pl-r-pill);
  background: rgba(255, 255, 255, .16); color: #fff;
  font: inherit; font-size: 20px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  -webkit-tap-highlight-color: transparent;
}
.pl-vclose:active { background: rgba(255, 255, 255, .3); }
.pl-vcount {
  margin-left: auto;
  color: rgba(255, 255, 255, .85);
  font-size: var(--sys-t-body); font-weight: var(--sys-w-bold);
  font-variant-numeric: tabular-nums;
}
/* touch-action:none — the STAGE owns every gesture in here (the swipe between
   pieces and MediaClient's shared pinch-zoom), and it is also what stops iOS
   page-zooming the whole app when two fingers land on a photograph. */
.pl-vstage { flex: 1; position: relative; overflow: hidden; min-height: 0; touch-action: none; }
.pl-vslide {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 56px 10px 10px; box-sizing: border-box;
}
/* pointer-events:none is attachViewerZoom's own contract, written down here
   because it is invisible: every gesture has to land on the stage. Without this
   line pinch silently does nothing and every automated check stays green — the
   exact shape that left the inventory crop dead on 2026-09-06. */
.pl-vimg {
  max-width: 100%; max-height: 100%;
  object-fit: contain; border-radius: 8px;
  pointer-events: none; -webkit-user-drag: none; user-select: none;
}
/* The honest sentence, for the states that have no picture to show: a photo
   stored on the server that this phone has never downloaded, and a file that is
   really gone. Never a spinner — in a storage unit a spinner is every photo. */
.pl-vword {
  margin: 0; max-width: 300px; text-align: center;
  color: rgba(255, 255, 255, .86);
  font-size: var(--sys-t-lead); line-height: 1.45;
}
.pl-vnav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 4;
  width: 46px; height: 46px;
  border: 0; border-radius: 50%;
  background: rgba(0, 0, 0, .42); color: #fff;
  font: inherit; font-size: 26px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.pl-vprev { left: 8px; }
.pl-vnext { right: 8px; }
.pl-vcap { padding: 14px 18px 4px; text-align: center; color: #fff; }
/* …and it BREAKS, like the row behind it. Since the check landed under it, this
   caption is the only thing telling him WHICH piece he is about to write to, and
   a name typed as one token ran off both edges here while the row wrapped fine. */
.pl-vname { display: block; font-size: var(--sys-t-lead); font-weight: var(--sys-w-semibold); line-height: 1.3; overflow-wrap: anywhere; }
.pl-vroom { display: block; margin-top: 2px; font-size: var(--sys-t-body); color: rgba(255, 255, 255, .62); }
.pl-vsay { display: block; margin-top: 6px; font-style: normal; font-size: var(--sys-t-caption); color: rgba(255, 255, 255, .72); line-height: 1.4; }
/* ---- THE CHECK INSIDE THE PHOTO: the one COMMON action on this layer ----
   Everything below it is a rare action behind a deliberate reach; this one is the
   job. So it is full width, it is the accent when it has been done, and it says
   the word next to the circle — a lone circle on a black screen is a state, not
   an offer, and a mover in gloves is the person least able to guess.
   It reuses system.css's .sys-tick for the circle so the ticked look is the same
   object the row draws, painted here off this button's own .done. */
.pl-viewer .pl-vtick {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: calc(100% - 28px);
  /* 20px of clear air under it, because the button directly below is "Take a new
     photo": measured at 375, the gap was 12px between the most-tapped control on
     this layer and the one that opens the camera over the photograph he is
     reading. In gloves that is a coin flip. The line that used to sit between them
     went on 2026-10-04 (Mario's text pass), so the air is its own: 8px here and
     the action row's 12px of padding. */
  margin: 10px 14px 8px;
  min-height: var(--sys-tap);
  border: 0; border-radius: var(--pl-r-pill);
  /* A WHITE PILL, not another grey one. At rest this was rgba(255,255,255,.16) —
     the same fill, radius and weight as .pl-vact — so the bottom of the screen
     was four identical grey pills and the only thing marking the job's button as
     the job was that it was wider. The common action must be the loudest thing on
     the layer; the rare ones stay quiet behind their deliberate reach. */
  background: #fff; color: #1D1D1F;
  font: inherit; font-size: var(--sys-t-lead); font-weight: var(--sys-w-semibold);
  -webkit-tap-highlight-color: transparent;
}
.pl-viewer .pl-vtick:active { opacity: .82; }
/* Done is the accent, the same green-free blue the row's tick fills with, so the
   two readings of "this piece is pulled" cannot drift apart. */
/* …and the WORD turns white with it. Only the background used to change, so
   "Pulled" stayed #1D1D1F on #0071E3 — 3.58:1, the least readable word on the
   screen and the one that answers "did it take?". White on the accent is 4.70:1.
   The circle's ring and fill are already white below. 2026-09-17. */
.pl-viewer .pl-vtick.done { background: var(--pl-accent); color: #fff; }
.pl-viewer .pl-vtick.done:active { background: var(--pl-accent); opacity: .82; }
/* The circle: empty at rest with a white ring, filled white with the accent's own
   check when done — on this layer the ground is black, so the fill inverts.
   .sys-tick carries the 24px box; the check itself is an ::after, a rotated
   border, exactly as .pl-tick draws it on the row — the ✓ in the markup is a
   fallback that font-size:0 hides, the same trick and for the same reason. Two
   drawings of one state are how the two readings drift apart. */
.pl-viewer .pl-vtickdot {
  flex: none;
  font-size: 0;
  line-height: 1;
  border-color: rgba(29, 29, 31, .32);
  background: transparent;
  transition: background .12s ease, border-color .12s ease;
}
.pl-viewer .pl-vtick.done .pl-vtickdot { border-color: #fff; background: #fff; }
.pl-viewer .pl-vtick.done .pl-vtickdot::after {
  content: "";
  width: 5px;
  height: 10px;
  margin-top: -2px;
  border: solid var(--pl-accent);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.pl-viewer .pl-vtickword { line-height: 1; }

/* The rare actions, at the bottom, behind a deliberate reach. Replace offers
   BOTH pickers because iOS cannot open the camera roll directly.
   Written as a DESCENDANT of .pl-viewer, not bare: this bar pays the bottom inset
   itself, and it may only do that because everything above it is inside a
   position:fixed layer that the shell's own body padding cannot reach. Saying so
   in the selector is what tells qa:layout-contract's double-payment scan the
   truth instead of making it guess. */
.pl-viewer .pl-vacts {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  padding: 12px 14px;
  padding-bottom: calc(14px + env(safe-area-inset-bottom));
}
/* The bottom edge's companion, same rule as the ✕ above: while the shell has
   already put the page above the home indicator (html.ni-bottom — scrolled to the
   end, or a house open), env() here paid it a second time and lifted Replace and
   the check 34pt off the bottom over a black band (measured 48px of padding in
   both states, 2026-09-17). */
html.ni-bottom .pl-viewer .pl-vacts { padding-bottom: 14px; }
.pl-viewer .pl-vact {
  min-height: var(--sys-tap); padding: 0 16px;
  border: 0; border-radius: var(--pl-r-pill);
  background: rgba(255, 255, 255, .16); color: #fff;
  font: inherit; font-size: var(--sys-t-body); font-weight: var(--sys-w-semibold);
  -webkit-tap-highlight-color: transparent;
}
.pl-viewer .pl-vact:active { background: rgba(255, 255, 255, .3); }
/* A REMOTE PHOTO WHOSE FILE WOULD NOT LOAD. media-client.js retries once with a
   cache-buster and then flags the <img> `data-media-error="1"`; between that flag
   and pull-list.js answering the same event there is a frame in which the browser
   would draw its own broken-image glyph over a black screen. There was no rule
   for the flag anywhere in this file, so that glyph was the only thing this
   screen had to say about a dead photo. Not !important: [hidden] must still win. */
.pl-vimg[data-media-error="1"] { display: none; }

/* Belt and braces, the way .pl-noteby already carries it: these all set display,
   and a rule that sets display beats the browser's own low-specificity [hidden].
   system.css's !important covers it today; this survives a shell that drops it. */
.pl-viewer[hidden],
/* Written with the same .pl-viewer ancestor its display rule carries: as bare
   `.pl-vtick[hidden]` the two were both (0,2,0) and this one won on source order
   alone, which is not what "belt and braces" means. */
.pl-viewer .pl-vtick[hidden],
.pl-vimg[hidden],
.pl-vword[hidden],
.pl-vnav[hidden],
.pl-vroom[hidden],
.pl-vsay[hidden],
.pl-vact[hidden] { display: none; }

/* ===== "Photo on Lauren's phone" (2026-10-23) =====
   A pull-list photo whose row exists and whose bytes are still on the stager's
   phone. On every OTHER phone the tile used to be a blank ＋; now the cell says
   whose phone, in the caption size, beside a phone glyph in the same 44pt square
   a photo would fill. Reads, not aimed at: the square opens the viewer, which says
   the same sentence full screen. */
.pl-onphone {
  align-self: center;
  max-width: 96px;
  margin-right: 6px;
  font-size: var(--sys-t-caption, 11px);
  line-height: 1.2;
  color: var(--sys-muted, #6b6b70);
  text-align: right;
  overflow-wrap: anywhere;
}
.pl-camwrap.onphone .pl-cam { background: var(--sys-fill, rgba(0, 0, 0, .06)); }
.pl-camwrap.onphone .pl-camdot { color: var(--sys-muted, #6b6b70); }
