/* ---------------------------------------------------------------------------
   admin.css — ImageBooth V2 admin

   Component CSS lifted from the locked exports
   (reference/locked-designs/job-register.html and admin-new-job.html).

   Deliberate differences from the exports, all confirmed:
     1. Fonts are self-hosted from public/assets/fonts. The Typekit, Google
        Fonts and Fontshare links are removed.
     2. Theme scoping moved from the artboard's .frame.dark / .frame.light to
        [data-theme], matching the three-way System / Light / Dark control the
        exports draw in the sidebar. Tokens live in studiohill-tokens.css.
     3. .frame (a fixed 1800x1200 artboard) becomes .app, a real viewport shell.
     4. Six font weights the closed Studio Hill font set does not include were
        normalised to the nearest supplied weight rather than synthesised.
        See FONT WEIGHTS below.
     5. Real form controls, state styling, empty states and reduced-motion
        support are authored here. The exports are static and carry none.
   --------------------------------------------------------------------------- */

@import url("studiohill-tokens.css");

/* --- Self-hosted Studio Hill faces ------------------------------------------
   TASK 012E — where each face is allowed:
     General Sans        every operational interface. Buttons, forms, tables,
                         navigation, contacts, emails, settings, statuses.
                         If an operator reads it to do their job, it is this.
     Cormorant Garamond  large editorial headings only, and the client gallery.
     Termina             Studio Hill MARKS only — the wordmark and the monogram
                         badge. Never interface text. It is a display face and
                         an admin set in it reads as a magazine rather than as
                         software.

   Termina is still declared below because those marks still use it, and
   removing the faces would break the logo.

   The active set is closed: Termina 400/500/800/900, Cormorant Garamond
   300/400, General Sans 400/500. Nothing else is declared, so the browser is
   never asked to fake a weight or an italic that Studio Hill has not licensed.
   ---------------------------------------------------------------------------- */
@font-face{font-family:'termina';src:url("../fonts/Termina-Regular.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'termina';src:url("../fonts/Termina-Medium.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap}
@font-face{font-family:'termina';src:url("../fonts/Termina-Heavy.woff2") format("woff2");font-weight:800;font-style:normal;font-display:swap}
@font-face{font-family:'termina';src:url("../fonts/Termina-Black.woff2") format("woff2");font-weight:900;font-style:normal;font-display:swap}
@font-face{font-family:'Cormorant Garamond';src:url("../fonts/CormorantGaramond-Light.woff2") format("woff2");font-weight:300;font-style:normal;font-display:swap}
@font-face{font-family:'Cormorant Garamond';src:url("../fonts/CormorantGaramond-Regular.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'General Sans';src:url("../fonts/GeneralSans-Regular.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'General Sans';src:url("../fonts/GeneralSans-Medium.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap}
/* SEMIBOLD AND BOLD, supplied with the approved Dashboard design.
   General Sans previously shipped 400/500 only, which is why earlier passes had
   to route every request for weight onto Medium. The design calls for 600 on
   nav items and row titles and 700 on the New Job button and status labels, and
   both faces are now real files rather than a browser-synthesised smear.
   OpenType rather than woff2 because that is the format supplied; the handoff's
   own prototype loads these two the same way. */
@font-face{font-family:'General Sans';src:url("../fonts/GeneralSans-Semibold.otf") format("opentype");font-weight:600;font-style:normal;font-display:swap}
@font-face{font-family:'General Sans';src:url("../fonts/GeneralSans-Bold.otf") format("opentype");font-weight:700;font-style:normal;font-display:swap}
/* CORMORANT MEDIUM (500) — the card headings in the approved design.
   Cormorant has no bold, which an earlier pass discovered the hard way when a
   `font-weight:600` request produced a synthesised fake. 500 is a real supplied
   face, so the editorial headings can carry weight honestly. */
@font-face{font-family:'Cormorant Garamond';src:url("../fonts/CormorantGaramond-Medium.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap}

/* FONT WEIGHTS — normalised to the supplied set, applied at extraction:
     .sb__brand .wm em   300 -> 400   (no Termina Light)
     .av                 700 -> 500   (no General Sans Bold)
     .hd__btn            600 -> 500   (no Termina Semibold)
     .res .cav           600 -> 500   (no General Sans Semibold)
     .clientcard .cav    600 -> 500   (no General Sans Semibold)
     .primary            600 -> 500   (no Termina Semibold)
   Remaining weights in this file are 300 (Cormorant Light), 400, 500 and
   900 (Termina Black), all of which are supplied. */

html{background:var(--bg)}
body{font-family:var(--body);background:var(--bg);color:var(--linen)}

/* The application shell. Replaces the export's fixed 1800x1200 .frame.
   Width and grid are unchanged; only the ground beneath it is richer. */
/* THE PAGE GROUND IS PAINTED IN THE WORKSPACE BOX, NOT THE VIEWPORT BOX.
   ---------------------------------------------------------------------------
   This is the whole reason the Job Register's light background did not match
   the Dashboard's, and it is not a colour difference — it is the same gradient
   sampled over two different rectangles.

   The Dashboard painted `--page-ground` on `.ws.dash`: a box 1380x1000
   starting at x=220. Every other screen inherited it from `.app`: a box
   1600x1000 starting at x=0. A 165-degree gradient resolves to a different
   colour at every point when its box is 220px wider and shifted, so the two
   grounds could never agree no matter what the token said.

   Sizing the background to the main column fixes it at the source: one
   declaration, one box, and the workspace ground is now identical on every
   screen including the Dashboard. Under the sidebar there is only `--bg`,
   which the opaque sidebar covers.

   `background-size` is stated before `background-image` would need it because
   the shorthand `background` above resets these — the longhands must follow. */
.app{height:100vh;background:var(--bg);color:var(--linen);
  background-image:var(--page-ground);
  background-size:calc(100% - var(--sidebar-w)) 100%;
  background-position:var(--sidebar-w) 0;
  background-repeat:no-repeat;
  font-size:var(--fs-lg-plus);-webkit-font-smoothing:antialiased;display:grid;
  grid-template-columns:var(--sidebar-w) 1fr;
  /* THE ROW IS CAPPED AT THE VIEWPORT, NOT SIZED BY ITS CONTENT.
     A grid row is auto-sized by its tallest item, so a sidebar wanting 821px
     made the row 821px even though `.app` is `height:100vh` — and with
     `overflow:hidden` here, the 61px past the fold was not below anything, it
     was gone, with no scrollbar in the document to reach it.
     `minmax(0,1fr)` caps the row so both children get exactly the viewport and
     scroll inside themselves. `.main` already did that; the sidebar now can. */
  grid-template-rows:minmax(0,1fr);
  overflow:hidden;position:relative;
  isolation:isolate}

/* Atmosphere.
   ---------------------------------------------------------------------------
   Reflected ambient light entering from the left and right edges, with the
   centre left deep charcoal. Two very wide, very low-opacity ellipses — not a
   radial blob and not a colour field. In light mode both stops are transparent,
   so this rule costs nothing and the light theme stays restrained by decision.

   Sits behind content on its own layer and takes no pointer events, so it can
   never interfere with a control. */
.app::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:
    radial-gradient(115% 88% at -12% 16%,var(--atmos-left),transparent 58%),
    radial-gradient(105% 82% at 112% 84%,var(--atmos-right),transparent 56%),
    linear-gradient(180deg,var(--atmos-floor),transparent 42%);
  opacity:var(--atmos-strength)}

/* ===== from job-register.html ===== */
*{box-sizing:border-box;margin:0;padding:0}
[data-theme="dark"] .kpi,[data-theme="dark"] .panel,[data-theme="dark"] .card{background-image:linear-gradient(160deg,var(--edge-hi),rgba(255,255,255,0) 58%)}
.eyebrow,.sec,.ni,.blk__hd h2,.tab,.st,.kpi__l,.btn-sec,.filt,.dtab,.k{font-family:var(--body)}
a{color:inherit;text-decoration:none}
a:hover{color:var(--ink)}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;letter-spacing:inherit}
/* Default icon size. Every component rule below is more specific and
   overrides it; this stops an icon in a container the export never used
   from rendering at its intrinsic size. */
.icon{stroke:currentColor;fill:none;stroke-width:1.25;width:12px;height:12px;flex-shrink:0}
:is(.hd__btn,.btn-sec,.kpi,.tab,.filt,.row,.dtab,.qa,.gbtn,.glink,button):focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* sidebar */
/* Sidebar. Width, sections, wordmark, counts and theme control are all
   unchanged — only its presence is stronger: a deeper ground than the page so
   it reads as a distinct architectural plane, and a firmer edge. */
.sb{background:var(--sidebar-bg);border-right:1px solid rgba(240,238,232,.13);
  box-shadow:1px 0 0 rgba(0,0,0,.65),6px 0 22px -14px rgba(0,0,0,.8);
  height:100%;display:flex;flex-direction:column;
  padding:26px 0 20px;position:relative;z-index:2}
/* The wordmark is a link home. `display:block` and the inherited colour keep
   it looking exactly as it did as a div — the only visible difference is the
   cursor, and a faint lift on hover so it reads as pressable. */
.sb__brand{display:block;padding:0 24px 26px;text-decoration:none;
  border-bottom:.5px solid rgba(240,238,232,.09);
  transition:opacity 140ms var(--ease)}
.sb__brand:hover{opacity:.78}
.sb__brand:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;
  border-radius:2px}
/* The wordmark, and one of only four places Termina survives in this file. It
   is the Studio Hill mark rather than interface text — see the note above
   `--sans` in studiohill-tokens.css. */
.sb__brand .wm{font-weight:900;font-size:var(--fs-wordmark);letter-spacing:.02em;color:#F0EEE8;font-family:var(--sans)}
.sb__brand .wm em{font-style:normal;color:#8A8A80;font-weight:400}
.sb__brand .tag{display:block;margin-top:6px;font-size:var(--fs-micro);letter-spacing:.3em;text-transform:uppercase;color:#8A8A80}
/* THE NAV IS THE PART THAT SCROLLS; NOTHING ELSE MAY BE PUSHED OFF.
   ---------------------------------------------------------------------------
   `.app` is `height:100vh; overflow:hidden`, so anything taller than the
   sidebar's own box is not merely below the fold — it is unreachable, with no
   scrollbar anywhere to get to it.

   Measured on a 760px-tall viewport, which is an ordinary 1440x900 laptop once
   the browser chrome is taken off: the sidebar wanted 821px and the sign-out
   row sat at y=764. Not clipped-looking, not awkward — gone, and unpressable.

   `min-height:0` is the load-bearing half. A flex child will not shrink below
   its content without it, so `flex:1` alone lets the list grow and shove the
   footer out of the box rather than scrolling inside it. */
.sb nav{flex:1 1 auto;min-height:0;overflow-y:auto;
  padding:20px 0;display:flex;flex-direction:column}
/* Section headings sit a little further from the items beneath them, so the
   groups read as groups without a rule between them. */
.sb .sec{padding:18px 24px 8px;font-size:var(--fs-micro);letter-spacing:.3em;
  text-transform:uppercase;color:#6E6E63}
.ni{display:flex;align-items:center;gap:12px;padding:10px 24px;font-size:var(--fs-md);letter-spacing:.22em;text-transform:uppercase;color:#B4B0A8;position:relative;transition:color 140ms var(--ease)}
.ni svg{width:13px;height:13px;flex-shrink:0}
/* Active navigation: a lit edge and a slightly raised ground. The glow is a
   micro-effect on a 2px rule — the ceiling for illumination anywhere in the
   product. Never on text, never a halo. */
.ni.on{color:#F0EEE8;
  background:linear-gradient(90deg,var(--accent-dim),rgba(255,255,255,.03) 40%,rgba(0,0,0,0) 78%)}
.ni.on::before{content:"";position:absolute;left:0;top:5px;bottom:5px;width:3px;
  border-radius:0 2px 2px 0;background:var(--accent);box-shadow:var(--accent-glow)}
.ni.on svg{stroke:var(--accent)}
.ni:hover{color:#F0EEE8}
.ni:not(.on):not(.is-disabled):hover{background:rgba(240,238,232,.035)}
.ni .bdg{margin-left:auto;font-family:var(--mono);font-size:var(--fs-sm);color:var(--accent);background:var(--accent-dim);padding:2px 6px}
/* A badge whose count is ALREADY LATE says so. My Tasks counts overdue rather
   than open, so it earns a different tone from the neutral accent every other
   nav badge uses. The sidebar is shared chrome, so this is one modifier rather
   than a second badge component. */
.ni .bdg--hot{color:var(--hot);background:var(--hot-dim)}
.sb__theme{padding:16px 24px 0}
.tm{font-size:var(--fs-micro);letter-spacing:.24em;text-transform:uppercase;color:#7A7A6E;margin-bottom:8px;display:block}
.tm__seg{display:grid;grid-template-columns:1fr 1fr 1fr;background:#0F0F0D;border:.5px solid rgba(240,238,232,.09);border-radius:var(--chip-radius);padding:2px;gap:2px}
.tm__seg button{font-size:var(--fs-2xs);letter-spacing:.1em;text-transform:uppercase;color:#8A8A80;padding:6px 0;border-radius:var(--chip-radius)}
.tm__seg button.on{background:#F0EEE8;color:#0A0A0A}
.sb__user{border-top:.5px solid rgba(240,238,232,.09);padding:16px 24px 0;display:flex;align-items:center;gap:11px;margin-top:16px}
.av{width:30px;height:30px;background:#F0EEE8;color:#0A0A0A;display:flex;align-items:center;justify-content:center;font-size:var(--fs-md);font-weight:500;flex-shrink:0;border-radius:50%}
.sb__user .nm{font-size:var(--fs-base);color:#F0EEE8;font-weight:500}
.sb__user .rl{font-size:var(--fs-2xs);letter-spacing:.22em;text-transform:uppercase;color:#8A8A80;margin-top:2px}

/* The name takes the row's width. Ellipsised rather than wrapped, because a
   two-line name would push the role out of alignment with the avatar. */
.sb__who{min-width:0;flex:1}
.sb__user .nm,.sb__user .rl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Sign out, on its own row beneath the name — see the note in the template for
   why it is not beside it. Borrows `.ni`'s geometry so it sits in the sidebar's
   existing vocabulary; only the things a <button> does not inherit are set. */
.sb__out{margin-top:10px}
.sb__outbtn{width:100%;border:0;background:none;cursor:pointer;
  font-family:var(--body);text-align:left}
.sb__outbtn:hover{color:#F0EEE8}
/* main */
/* `isolation` so the bloom's negative z-index stays ABOVE the page ground
   rather than disappearing behind it — the same trick `.ws.dash` used when
   the bloom lived there. */
.main{min-width:0;height:100%;overflow:hidden;position:relative;isolation:isolate}
.ws{padding:26px 28px 22px;height:100%;display:flex;flex-direction:column;gap:14px;min-height:0}
.hd{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;flex-shrink:0}
.hd h1,.gws__hd h1{font-family:var(--serif);font-weight:400;font-size:34px;color:var(--ink);letter-spacing:.005em}
.hd .sub{margin-top:6px;font-size:var(--fs-base);color:var(--slate2);letter-spacing:.01em}
.hd__right{display:flex;align-items:center;gap:10px}
/* THE SAME FIELD AS THE DASHBOARD'S. It is already the same markup — the
   Dashboard renders `.hd__search` and adds `.dash__search` for the glass —
   so the two only differed in material. Now they do not. */
.hd__search{display:flex;align-items:center;gap:8px;background:var(--surface-card);
  border:1px solid var(--edge-card);border-radius:var(--card-radius);
  -webkit-backdrop-filter:blur(var(--dash-blur));
  backdrop-filter:blur(var(--dash-blur));
  padding:0 18px;min-height:var(--dash-ctl-h);width:240px}
.hd__search svg{width:13px;height:13px;stroke:var(--slate2);flex-shrink:0}
.hd__search input{flex:1;background:none;border:0;outline:0;color:var(--ink);font-size:var(--fs-md-plus)}
.hd__search input::placeholder{color:var(--slate)}
.btn-sec{display:flex;align-items:center;gap:8px;background:var(--panel);color:var(--ink);border:.5px solid var(--line);border-radius:var(--row-radius);padding:10px 16px;font-size:var(--fs-sm-plus);letter-spacing:.01em;font-weight:500}
.btn-sec svg{width:12px;height:12px}
.btn-sec:active{transform:scale(.97)}
.hd__btn{display:flex;align-items:center;justify-content:center;gap:8px;background:var(--btn-primary);color:var(--btn-primary-ink);border-radius:var(--card-radius);padding:0 22px;min-height:var(--dash-ctl-h);font-size:var(--fs-lg);letter-spacing:.01em;font-weight:600}
.hd__btn svg{width:12px;height:12px}
.hd__btn:not([disabled]):active{transform:scale(.97)}
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;flex-shrink:0}
.kpi{background:var(--panel);border:.5px solid var(--line);border-radius:var(--card-radius);padding:16px 20px;box-shadow:var(--shadow)}
.kpi__top{display:flex;align-items:baseline;justify-content:space-between}
.kpi__n{font-family:var(--serif);font-size:30px;color:var(--ink);font-weight:400;line-height:1}
.kpi__top svg{width:14px;height:14px}
.kpi:nth-child(1) svg{stroke:var(--slate2)}
.kpi:nth-child(2) svg{stroke:var(--warn)}
.kpi:nth-child(3) svg{stroke:var(--hot)}
.kpi:nth-child(4) svg{stroke:var(--ok)}
/* The lit state is declared with the rest of the KPI material further down —
   a single-class rule here could never beat the `.kpi` rule that follows it. */
/* .18em, which is `.stat__l`'s tracking to the digit. It was .12em — close
   enough to look intentional and far enough to look like a different label. */
.kpi__l{margin-top:6px;font-size:var(--fs-sm);letter-spacing:.18em;text-transform:uppercase;color:var(--slate)}
/* tabs + filters */
.regbar{display:flex;align-items:center;justify-content:space-between;gap:20px;flex-shrink:0;flex-wrap:wrap}
.tabs{display:flex;gap:24px;border-bottom:.5px solid var(--line)}
.tab{padding:0 0 12px;font-size:var(--fs-sm);letter-spacing:.22em;text-transform:uppercase;color:var(--slate);border-bottom:2px solid transparent;margin-bottom:-.5px;display:flex;align-items:center;gap:6px;transition:color 140ms var(--ease),border-color 140ms var(--ease)}
.tab:hover{color:var(--linen)}
.tab.on{color:var(--ink);border-bottom-color:var(--accent)}
.tab .c{font-family:var(--mono);font-size:var(--fs-xs);color:var(--slate2)}
.tab.on .c{color:var(--ink)}
.filters{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.filt{display:flex;align-items:center;gap:6px;background:var(--panel2);border:.5px solid var(--line);border-radius:var(--pill-radius);padding:7px 13px;font-size:var(--fs-sm);letter-spacing:.08em;color:var(--linen);white-space:nowrap;transition:background 140ms var(--ease),border-color 140ms var(--ease),transform 120ms var(--ease)}
.filt:hover{background:var(--panel-hi)}
.filt:active{transform:scale(.96)}
.filt svg{width:9px;height:9px;stroke:var(--slate2)}
.filt.clear{color:var(--slate2)}
.export-wrap{position:relative}
/* table card */
.card{background:var(--panel2);border-radius:var(--card-radius);border:.5px solid var(--line);box-shadow:var(--shadow);display:flex;flex-direction:column;overflow:hidden;flex-shrink:0}
.tfoot{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-top:.5px solid var(--line);flex-shrink:0}
.tfoot .cnt{font-size:var(--fs-sm);color:var(--slate2);letter-spacing:.05em}
.tfoot .pg{display:flex;gap:6px}
.tfoot .pg button{font-size:var(--fs-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--slate2);padding:6px 11px;border-radius:var(--chip-radius);border:.5px solid var(--line);transition:color 140ms var(--ease),border-color 140ms var(--ease)}
.tfoot .pg button.on{color:var(--ink);border-color:var(--line2)}
.tfoot .pg button:disabled{opacity:.4;cursor:default}
table{width:100%;border-collapse:collapse}

/* The header band.
   ---------------------------------------------------------------------------
   Definition comes from structure, not weight: a rule above and a firmer rule
   below give the band edges, a slightly recessed surface separates it from the
   rows, and the labels are set apart by case and tracking so they read as
   labels rather than as small body text.

   They were set in Termina until Task 012E. A table header is operational
   content — the thing it labels is a client's name or a job number — so it is
   General Sans now, like everything else an operator reads.

   Type scale is untouched — still --fs-2xs, the smallest existing step. The
   surface is --panel2, which the band already used; only its borders, padding
   and label family change. No new colour, and both themes come from the same
   tokens as before. */
thead{position:sticky;top:0;background:var(--panel2);z-index:1;
  box-shadow:inset 0 1px 0 var(--line),inset 0 -1px 0 var(--line2)}
th{text-align:left;font-family:var(--body);font-size:var(--fs-2xs);letter-spacing:.2em;
  text-transform:uppercase;color:var(--slate2);font-weight:500;
  padding:16px 16px 14px;border-bottom:0}
td{padding:14px 16px;border-bottom:.5px solid var(--line);font-size:var(--fs-base);vertical-align:middle}

/* The first row sits against the header rule rather than doubling it. */
tbody tr:first-child td{border-top:0}
tbody{display:block}
thead,tbody tr{display:table;width:100%;table-layout:fixed}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-thumb{background:var(--line2);border-radius:4px}
tr.row{cursor:pointer;transition:background 140ms var(--ease),box-shadow 140ms var(--ease)}
tr.row:hover{background:var(--panel-hi)}
tr.row:active{background:var(--panel-hi)}
tr.row.sel{background:var(--panel-hi);box-shadow:inset 2px 0 0 var(--accent)}
.jthumb{width:34px;height:34px;border-radius:var(--chip-radius);background:var(--panel-hi);flex-shrink:0;display:flex;align-items:center;justify-content:center;color:var(--slate2)}
.jthumb svg{width:14px;height:14px}
.jcell{display:flex;align-items:center;gap:11px}
/* 600, matching `.attn__top` — the Dashboard's own row title, at the same
   13.5px. A Job's name is the thing being clicked on both screens. */
.jt{color:var(--ink);font-weight:600;font-size:var(--fs-lg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cl{color:var(--slate2);font-size:var(--fs-sm-plus);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cl .mono{font-family:var(--mono);color:var(--slate)}
/* THE STAGE PILL IS A STATUS LABEL, and the Dashboard sets those one way:
   `.attn__why` — the reason a Job needs attention — is uppercase, .1em, 700.
   This was pulled into the sentence-case action-pill rule by Task 012E on the
   argument that uppercase is a display device; that argument holds for a BUTTON
   and it is why `.tab`, `.ni` and the button family are untouched here. A stage
   is not something you press. It is the same object as `.attn__why`, so it is
   set the same way. */
.st{display:inline-flex;align-items:center;gap:6px;font-size:var(--fs-sm);
  letter-spacing:.1em;text-transform:uppercase;font-weight:700}
.st i{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.st.info i{background:var(--info)}
.st.info{color:var(--info)}
.st.warn i{background:var(--warn)}
.st.warn{color:var(--warn)}
.st.ok i{background:var(--ok)}
.st.ok{color:var(--ok)}
.st.neu i{background:var(--slate2)}
.st.neu{color:var(--slate2)}
/* Two more tones on the SAME component, bound to tokens that already exist.
   No colour is introduced here — `--hot` and `--accent` have been in the
   palette since the beginning and are already used elsewhere.

   They earn their place because the derived production status has six states
   and four tones could not keep the two that matter apart: READY FOR QC is the
   one row Josh must not scroll past, and GALLERY READY is a handoff to Jenni
   that must never read as "already sent". Giving the first `--hot` and the
   second `--accent` leaves `--ok` to mean finished, which is what green has
   always meant on this screen. */
.st.hot i{background:var(--hot)}
.st.hot{color:var(--hot)}
.st.accent i{background:var(--accent)}
.st.accent{color:var(--accent)}
.gal{font-size:var(--fs-md);color:var(--linen)}
.gal small{display:block;color:var(--slate2);font-size:var(--fs-xs);margin-top:3px}
.gal.dim{color:var(--slate)}
.fee{color:var(--ink);font-weight:500;text-align:right;font-size:var(--fs-lg)}
.fee small{display:block;font-weight:400;font-size:var(--fs-xs);margin-top:3px;text-align:right;letter-spacing:.03em}
.fee small.ok{color:var(--ok)}
.fee small.info{color:var(--info)}
.fee small.mut{color:var(--slate2)}
.more{width:26px;height:26px;border-radius:var(--chip-radius);display:flex;align-items:center;justify-content:center;color:var(--slate2);transition:background 140ms var(--ease),transform 120ms var(--ease)}
.more:hover{background:var(--panel-hi)}
.more:active{transform:scale(.9)}
.more svg{width:13px;height:13px}
/* popover */
.pop{position:absolute;top:172px;right:28px;width:340px;background:var(--panel);border:.5px solid var(--line2);border-radius:var(--card-radius);box-shadow:0 30px 60px -20px rgba(0,0,0,.5),var(--shadow);padding:20px;z-index:20;transform-origin:top right;animation:popIn 180ms var(--ease) both}
@keyframes popIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}
.pop__hd{font-size:var(--fs-md);letter-spacing:.2em;text-transform:uppercase;color:var(--ink);margin-bottom:14px}
.pop__grp{margin-bottom:16px}
.pop__lbl{font-size:var(--fs-2xs);letter-spacing:.18em;text-transform:uppercase;color:var(--slate2);margin-bottom:8px;display:block}
.seg{display:flex;background:var(--panel2);border:.5px solid var(--line);border-radius:var(--chip-radius);padding:2px;gap:2px}
.seg button{flex:1;font-size:var(--fs-xs);letter-spacing:.08em;text-transform:uppercase;padding:7px 4px;border-radius:var(--chip-radius);color:var(--slate2)}
.seg button.on{background:var(--ink);color:var(--bg)}
.pop__fields{font-size:var(--fs-sm);color:var(--slate2);line-height:1.9;border-top:.5px solid var(--line);border-bottom:.5px solid var(--line);padding:12px 0;margin-bottom:12px}
.pop__fields b{color:var(--linen);font-weight:500}
.pop__note{font-size:var(--fs-xs);color:var(--slate2);line-height:1.5;margin-bottom:16px}
.pop__actions{display:flex;gap:10px;justify-content:flex-end}
.pop .ghost{font-size:var(--fs-sm);letter-spacing:.1em;text-transform:uppercase;color:var(--slate2);padding:9px 6px}
.pop .go{background:var(--btn-primary);color:var(--btn-primary-ink);font-size:var(--fs-sm);letter-spacing:.01em;padding:9px 16px;border-radius:var(--chip-radius);display:flex;align-items:center;gap:6px}
/* drawer */
.scrim{position:absolute;inset:0;background:rgba(7,7,6,.55);z-index:9;animation:scrimIn 260ms var(--ease) both}
@keyframes scrimIn{from{opacity:0}to{opacity:1}}
.drawer{position:absolute;top:0;right:0;bottom:0;width:610px;background:var(--panel);border-left:.5px solid var(--line2);z-index:10;display:flex;flex-direction:column;box-shadow:-30px 0 60px rgba(0,0,0,.3);animation:drawerIn 280ms var(--ease-drawer) both}
@keyframes drawerIn{from{transform:translateX(4%);opacity:0}to{transform:translateX(0);opacity:1}}
.dr__hd{padding:26px 30px 0}
.dr__hd .path{font-size:var(--fs-2xs);letter-spacing:.22em;text-transform:uppercase;color:var(--slate2)}
.dr__hd h3{font-family:var(--serif);font-weight:400;font-size:24px;color:var(--ink);margin:10px 0 16px}
.dr__meta{display:flex;gap:24px;padding-bottom:18px;flex-wrap:wrap}
.dr__meta .m .k{font-size:var(--fs-micro);letter-spacing:.2em;text-transform:uppercase;color:var(--slate2)}
.dr__meta .m .v{font-size:var(--fs-base);color:var(--ink);margin-top:5px}
.dtabs{display:flex;gap:20px;border-top:.5px solid var(--line);padding-top:2px}
.dtab{padding:12px 0 12px;font-size:var(--fs-xs);letter-spacing:.18em;text-transform:uppercase;color:var(--slate);border-bottom:2px solid transparent;margin-bottom:-.5px;transition:color 140ms var(--ease),border-color 140ms var(--ease)}
.dtab:hover{color:var(--linen)}
.dtab.on{color:var(--ink);border-bottom-color:var(--accent)}
.dr__bd{flex:1;overflow-y:auto;padding:22px 30px 30px}
.kv{display:flex;justify-content:space-between;gap:16px;padding:12px 0;border-bottom:.5px solid var(--line);font-size:var(--fs-lg)}
.kv .k{color:var(--slate2);letter-spacing:.03em}
.kv .v{color:var(--ink);text-align:right}
.gsec{margin-top:20px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.gbtn.pri{display:inline-flex;align-items:center;gap:6px;font-size:var(--fs-sm);letter-spacing:.01em;font-weight:600;padding:8px 13px;border-radius:var(--chip-radius);background:var(--btn-primary);color:var(--btn-primary-ink)}
.gbtn.pri svg{width:11px;height:11px}
.gdiv{width:1px;height:14px;background:var(--line2)}
.glink{font-size:var(--fs-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--slate2);transition:color 140ms var(--ease)}
.glink:hover{color:var(--ink)}
.glink.warn{color:var(--hot)}
.gstatus{display:flex;align-items:center;gap:8px;margin-bottom:18px}
.gstatus .dot{width:8px;height:8px;border-radius:50%;background:var(--ok)}
.gstatus .lbl{font-family:var(--serif);font-size:19px;color:var(--ink)}
/* ===== from admin-new-job.html ===== */
.sec,.ni,.st,.lbl,.mhd .id,.rail .k,.sub-hd{font-family:var(--body)}
:is(.chip,.crewpill,.btn,.fld,.link,button):focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* sidebar */
.ni{display:flex;align-items:center;gap:12px;padding:10px 24px;font-size:var(--fs-md);letter-spacing:.22em;text-transform:uppercase;color:#B4B0A8;position:relative}
/* backdrop page */
.ws{padding:26px 28px;height:100%;display:flex;flex-direction:column;gap:16px}
.hd h1{font-family:var(--serif);font-weight:400;font-size:34px;color:var(--ink)}
.hd .sub{margin-top:6px;font-size:var(--fs-base);color:var(--slate2)}
.ghost-card{background:var(--panel2);border:.5px solid var(--line);border-radius:var(--card-radius);flex:1}
.scrim{position:absolute;inset:0;background:rgba(7,7,6,.55);z-index:9;animation:fadeIn 240ms var(--ease) both}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
/* modal */
.modal{position:absolute;z-index:10;top:56px;bottom:56px;left:50%;transform:translateX(-50%);width:1180px;background:var(--bg);border:.5px solid var(--line2);border-radius:var(--card-radius);box-shadow:0 40px 90px -30px rgba(0,0,0,.55);display:flex;flex-direction:column;overflow:hidden;animation:modalIn 260ms var(--ease-drawer) both}
@keyframes modalIn{from{opacity:0;transform:translateX(-50%) translateY(8px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
.mhd{padding:22px 28px 18px;border-bottom:.5px solid var(--line);display:flex;align-items:flex-end;justify-content:space-between;gap:20px;background:var(--panel);flex-shrink:0}
.mhd h2{font-family:var(--serif);font-weight:400;font-size:26px;color:var(--ink)}
.mhd .id{font-size:var(--fs-2xs);letter-spacing:.22em;text-transform:uppercase;color:var(--slate2);margin-bottom:8px}
.mhd .id b{font-family:var(--mono);letter-spacing:.04em;color:var(--linen);font-weight:400}
.mclose{width:28px;height:28px;border-radius:var(--chip-radius);display:flex;align-items:center;justify-content:center;color:var(--slate2);transition:background 140ms var(--ease),transform 120ms var(--ease)}
.mclose:hover{background:var(--panel-hi);color:var(--ink)}
.mclose:active{transform:scale(.92)}
.mclose svg{width:14px;height:14px}
.mbody{flex:1;display:grid;grid-template-columns:1fr 330px;min-height:0}
.form{overflow-y:auto;padding:24px 28px 28px;display:flex;flex-direction:column;gap:18px}
.sect{display:flex;flex-direction:column;gap:12px}
/* `[hidden]` MUST BE RESTATED FOR A SECTION. The `display:flex` above is an
   author rule, and an author `display` beats the user-agent `[hidden]` rule —
   so a section given `hidden` was still drawn. New Job's Source files section
   hides itself for Video and before a job type is chosen, and without this it
   showed the question for every type. The same arrangement `.gws__up[hidden]`
   already uses, scoped to the one element rather than made global. */
.sect[hidden]{display:none}
.sub-hd{display:flex;align-items:center;justify-content:space-between;gap:16px;font-size:var(--fs-2xs);letter-spacing:.26em;text-transform:uppercase;color:var(--slate2);padding-bottom:9px;border-bottom:.5px solid var(--line)}
.sub-hd .link{font-size:var(--fs-2xs);letter-spacing:.14em;color:var(--slate2);transition:color 140ms var(--ease)}
.sub-hd .link:hover{color:var(--ink)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}
.fld{background:var(--panel);border:.5px solid var(--line);border-radius:var(--row-radius);padding:10px 13px;display:flex;flex-direction:column;gap:5px;min-width:0}
.fld .lbl{font-size:var(--fs-micro);letter-spacing:.2em;text-transform:uppercase;color:var(--slate2)}
.fld .val{font-size:var(--fs-lg);color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fld .val.ph{color:var(--slate)}
.fld.search{flex-direction:row;align-items:center;gap:10px;padding:12px 14px}
.fld.search svg{width:13px;height:13px;stroke:var(--slate2);flex-shrink:0}
.fld.area{min-height:74px}
.fld.area .val{white-space:normal;line-height:1.5;font-size:var(--fs-base);color:var(--linen)}
.results{background:var(--panel);border:.5px solid var(--line2);border-radius:var(--row-radius);box-shadow:var(--shadow);overflow:hidden}
.res{display:flex;align-items:center;gap:11px;padding:11px 14px;border-bottom:.5px solid var(--line);cursor:pointer}
.res:last-child{border-bottom:0}
.res:hover{background:var(--panel-hi)}
.res .cav{width:28px;height:28px;border-radius:50%;background:var(--panel2);display:flex;align-items:center;justify-content:center;font-size:var(--fs-sm);font-weight:500;color:var(--ink);flex-shrink:0}
.res .nm{font-size:var(--fs-base);color:var(--ink)}
.res .em{font-size:var(--fs-sm);color:var(--slate2);margin-top:2px}
.res .tagr{margin-left:auto;font-size:var(--fs-2xs);letter-spacing:.14em;text-transform:uppercase;color:var(--slate2);font-family:var(--body)}
.clientcard{background:var(--panel);border:.5px solid var(--line);border-radius:var(--row-radius);padding:14px;display:flex;align-items:center;gap:13px}
.clientcard .cav{width:34px;height:34px;border-radius:50%;background:var(--panel2);display:flex;align-items:center;justify-content:center;font-size:var(--fs-md);font-weight:500;color:var(--ink);flex-shrink:0}
.clientcard .nm{font-size:var(--fs-lg);color:var(--ink);font-weight:500}
.clientcard .em{font-size:var(--fs-sm-plus);color:var(--slate2);margin-top:3px}
.clientcard .rate{margin-left:auto;text-align:right}
.clientcard .rate .k{font-size:var(--fs-micro);letter-spacing:.18em;text-transform:uppercase;color:var(--slate2)}
.clientcard .rate .v{font-size:var(--fs-md);color:var(--ink);margin-top:4px}
.clientcard .rate .v.na{color:var(--warn);font-family:var(--body);font-size:var(--fs-xs);letter-spacing:.16em;text-transform:uppercase}
.clientcard .rate .lnk{display:block;margin-top:5px;font-size:var(--fs-2xs);letter-spacing:.14em;text-transform:uppercase;color:var(--slate2);font-family:var(--body);transition:color 140ms var(--ease)}
.clientcard .rate .lnk:hover{color:var(--ink)}
.chip .pr{font-family:var(--mono);font-size:var(--fs-xs);letter-spacing:.02em;opacity:.62;text-transform:none;margin-left:2px}
.chip.on .pr{opacity:.72}
.chip.tbc .pr{color:var(--warn);opacity:1}
.norate{background:var(--panel2);border:.5px solid var(--line);border-radius:var(--row-radius);padding:14px 15px;display:flex;align-items:center;gap:14px}
.norate .t{font-size:var(--fs-md-plus);color:var(--ink)}
.norate .s{font-size:var(--fs-sm-plus);color:var(--slate2);margin-top:4px}
.norate .acts{margin-left:auto;display:flex;gap:8px;flex-shrink:0}
.chips{display:flex;gap:8px;flex-wrap:wrap}
.chip{font-size:var(--fs-sm);letter-spacing:.1em;text-transform:uppercase;color:var(--slate2);background:var(--panel2);border:.5px solid var(--line);border-radius:var(--pill-radius);padding:8px 14px;transition:background 140ms var(--ease),color 140ms var(--ease),transform 120ms var(--ease)}
.chip:hover{background:var(--panel-hi);color:var(--ink)}
.chip:active{transform:scale(.96)}
.chip.on{background:var(--band);color:var(--band-ink);border-color:transparent}
.crewpill{display:flex;align-items:center;gap:8px;font-size:var(--fs-sm);letter-spacing:.1em;text-transform:uppercase;color:var(--slate2);background:var(--panel2);border:.5px solid var(--line);border-radius:var(--pill-radius);padding:8px 14px;transition:background 140ms var(--ease),color 140ms var(--ease),transform 120ms var(--ease)}
.crewpill i{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.crewpill:hover{background:var(--panel-hi);color:var(--ink)}
.crewpill:active{transform:scale(.96)}
.crewpill.on{background:var(--panel);color:var(--ink);border-color:var(--line2);box-shadow:var(--shadow)}
.crewpill.amber{color:var(--warn);border-color:var(--warn)}
.srow .b.amber{color:var(--warn)}
.hint{display:flex;align-items:center;gap:8px;font-size:var(--fs-sm-plus);color:var(--slate2)}
.hint svg{width:11px;height:11px;stroke:var(--slate2);flex-shrink:0}
.hint.warn{color:var(--warn)}
.hint.warn svg{stroke:var(--warn)}
.hint.ok{color:var(--ok)}
.hint.ok svg{stroke:var(--ok)}
.btn{display:inline-flex;align-items:center;gap:7px;font-size:var(--fs-xs);letter-spacing:.14em;text-transform:uppercase;border:.5px solid var(--line);border-radius:var(--chip-radius);padding:8px 14px;color:var(--linen);transition:background 140ms var(--ease),transform 120ms var(--ease)}
.btn:hover{background:var(--panel-hi);color:var(--ink)}
.btn:active{transform:scale(.97)}
.btn svg{width:11px;height:11px}

/* EVERY action control: one family, one weight, one tracking, and sentence case.
   ---------------------------------------------------------------------------
   This rule used to say "pills and buttons are General Sans" while the rest of
   the admin stayed on Termina. Task 012E finished the job in the other
   direction: Termina is a display face and now appears nowhere in an
   operational interface — four brand marks in this file and nothing else. The
   whole admin is General Sans, so this rule is no longer a correction to the
   surrounding sheet; it is the sheet.

   What it still owns is CASE and TRACKING. Every control here was uppercase at
   .1em, which is how a drawer ended up shouting MAKE PRIMARY CONTACT and
   ADD AS GALLERY RECIPIENT at an operator doing routine work. Uppercase is a
   display device: it costs legibility, doubles the width of an ordinary verb
   phrase, and reads as a fashion masthead rather than as software. Removed
   here, in ONE place, rather than per component — which is the same argument
   the previous version of this comment made about the family.

   Hierarchy is carried by fill, border, colour and size, exactly as before. It
   was never carried by the typeface and it is not carried by the case. */
.btn,.gbtn,.glink,.chip,.crewpill,.rowact,.hd__btn,
.btn-sec,.primary,.subacts button,.pconfirm{
  font-family:var(--body);font-weight:500;letter-spacing:.01em;text-transform:none}
/* THE PRIMARY ACTION IS THE HEAVIER ONE. The Dashboard's single call to action
   is General Sans 700 against 500 everywhere else on the page; that weight
   contrast is how it reads as primary now that nothing is uppercase. Stated
   after the family rule because `.hd__btn` is declared before it. */
/* 700, which is the Dashboard's CTA weight. The primary is the one control
   on a screen that is allowed to be heavier than everything around it. */
.hd__btn:not(.ghost),.primary:not(.dim),.gbtn.pri,.pop .go{font-weight:700}
/* rail */
.rail{border-left:.5px solid var(--line);background:var(--panel);display:flex;flex-direction:column;min-height:0}
.rail__bd{flex:1;overflow-y:auto;padding:22px 24px}
.rail .grp{margin-bottom:20px}
.rail .grp:last-child{margin-bottom:0}
.rail .k{font-size:var(--fs-2xs);letter-spacing:.24em;text-transform:uppercase;color:var(--slate2);padding-bottom:9px;border-bottom:.5px solid var(--line);display:block}
.srow{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:.5px solid var(--line);font-size:var(--fs-md-plus)}
.srow:last-child{border-bottom:0}
.srow .a{color:var(--slate2);flex-shrink:0}
.srow .b{color:var(--ink);text-align:right;min-width:0;overflow:hidden;text-overflow:ellipsis}
.srow .b.ph{color:var(--slate)}
.srow .b.mono{font-family:var(--mono);font-size:var(--fs-sm-plus)}
.wf{display:flex;flex-wrap:wrap;gap:5px;padding-top:11px}
.wfs{font-size:var(--fs-micro);letter-spacing:.1em;text-transform:uppercase;color:var(--slate2);background:var(--panel2);border:.5px solid var(--line);border-radius:var(--pill-radius);padding:5px 9px;font-family:var(--body)}
.calbox{display:flex;align-items:center;gap:11px;padding-top:11px}
.calbox .ico{width:28px;height:28px;border-radius:var(--chip-radius);background:var(--panel2);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.calbox .ico svg{width:13px;height:13px;stroke:var(--slate2)}
.calbox .t{font-size:var(--fs-md-plus);color:var(--ink)}
.calbox .s{font-size:var(--fs-sm);color:var(--ok);margin-top:3px;display:flex;align-items:center;gap:5px}
.calbox .s i{width:5px;height:5px;border-radius:50%;background:var(--ok)}
.rail__ft{border-top:.5px solid var(--line);padding:18px 24px;display:flex;flex-direction:column;gap:10px;flex-shrink:0}
.primary{display:flex;align-items:center;justify-content:center;gap:8px;background:var(--btn-primary);color:var(--btn-primary-ink);border-radius:var(--row-radius);padding:13px 18px;font-size:var(--fs-sm-plus);letter-spacing:.01em;font-weight:600;font-family:var(--body);transition:transform 120ms var(--ease)}
.primary svg{width:12px;height:12px}
.primary:active{transform:scale(.98)}
.primary.dim{background:var(--panel2);color:var(--slate);border:.5px solid var(--line)}
[data-theme="light"] .primary.dim{background:var(--panel2);color:var(--slate)}
.subacts{display:flex;align-items:center;justify-content:space-between}
.subacts button{font-size:var(--fs-sm);letter-spacing:.01em;color:var(--slate2);font-family:var(--body);padding:6px 2px;transition:color 140ms var(--ease)}
.subacts button:hover{color:var(--ink)}
.pconfirm{font-size:var(--fs-sm);letter-spacing:.01em;color:var(--warn);font-family:var(--body)}
/* ===== authored: real form controls =========================================
   The exports render fields as static .fld > .val divs. These rules give real
   inputs the identical typography so the implemented form matches the design.
   ============================================================================ */
.fld input,.fld select,.fld textarea{width:100%;background:none;border:0;outline:0;color:var(--ink);font-family:var(--body);font-size:var(--fs-lg);padding:0}
.fld input::placeholder,.fld textarea::placeholder{color:var(--slate)}
.fld select{cursor:pointer;-webkit-appearance:none;appearance:none}
.fld select option{background:var(--panel);color:var(--ink)}
.fld textarea{resize:vertical;min-height:52px;line-height:1.5;font-size:var(--fs-base);color:var(--linen)}
.fld:focus-within{border-color:var(--line2)}
.fld.invalid{border-color:var(--hot)}
.fld .err{font-size:var(--fs-sm);letter-spacing:.02em;color:var(--hot)}

/* --- date and time fields -------------------------------------------------
   THE CALENDAR IS THE BROWSER'S, NOT A WIDGET.
   A date field already has a popup calendar behind it, and it is the one the
   operating system draws: it knows the locale, the week start, the keyboard
   conventions and the screen reader, and it is the calendar the studio's
   phones and laptops already use everywhere else. Rebuilding it in JavaScript
   would be several hundred lines that work worse in every one of those ways,
   on a screen whose whole premise is that it functions with scripting off.

   Two things stood between that calendar and the operator, and neither was a
   missing widget:

   1. The picker glyph was a 14px target at the far edge of a 300px field, and
      nothing else about the field suggested it opened anything. `admin.js`
      makes the whole field open it — see `initDateFields` — and these rules
      say so with the cursor.
   2. On the dark theme it was drawn near-black on a near-black field, because
      `color-scheme` was never declared. That is fixed in the token file, where
      the palette lives; it also makes the popup panel itself dark, which no
      rule here could have done.
   ------------------------------------------------------------------------- */
.fld input[type="date"],.fld input[type="time"]{cursor:pointer}
.fld input::-webkit-calendar-picker-indicator{
  cursor:pointer;opacity:.45;padding:3px;margin:0 -3px 0 0;
  transition:opacity 140ms var(--ease)}
.fld:hover input::-webkit-calendar-picker-indicator,
.fld:focus-within input::-webkit-calendar-picker-indicator{opacity:1}

/* Chips and crew pills as real radio / checkbox controls, so selection works
   without JavaScript and is keyboard reachable. */
.chipwrap{display:inline-flex;position:relative}
.chipwrap input{position:absolute;width:1px;height:1px;opacity:0;margin:0;pointer-events:none}
/* SELECTED IS THE ACCENT, and it is the same accent in both themes.
   Package, add-on and photographer pills used to disagree about what
   "chosen" looked like: a chip went near-black (`--band`) and a crewpill
   went one shade of panel lighter, which on a laptop in daylight was very
   nearly invisible. One treatment now, built from the accent pair the
   trail control already uses — the tint carries the colour, the solid
   border carries the edge, and `--ink` keeps the label readable because
   both tokens flip with the theme. No new colour is introduced. */
.chipwrap input:checked+.chip,
.chipwrap input:checked+.crewpill{background:var(--accent-dim);color:var(--ink);border-color:var(--accent)}
/* The disabled `Unassigned` placeholder is checked but is not a choice an
   operator made, so it keeps its amber reading rather than borrowing the
   accent and claiming to be a selection. */
.chipwrap input:checked+.crewpill.amber{background:var(--panel2);color:var(--warn);border-color:var(--warn)}
.chipwrap input:focus-visible+.chip,.chipwrap input:focus-visible+.crewpill{outline:2px solid var(--accent);outline-offset:2px}
.chipwrap input:disabled+.chip,.chipwrap input:disabled+.crewpill{opacity:.45;cursor:default}
.chip,.crewpill{cursor:pointer}

/* ===== authored: component states ==========================================
   Restrained tonal hover, subtle press, visible focus, muted disabled,
   dimension-preserving loading, inline restrained validation.
   ============================================================================ */
[disabled],.is-disabled,.ni.is-disabled{opacity:.45;cursor:default;pointer-events:none}
.ni.is-disabled{color:#7A7A6E}
[aria-busy="true"],.is-loading{opacity:.55;pointer-events:none}
.is-loading *{visibility:visible}

.empty{padding:44px 20px;text-align:center;color:var(--slate2)}
.empty .t{font-family:var(--serif);font-size:19px;color:var(--ink)}
.empty .s{font-size:var(--fs-md-plus);margin-top:8px;line-height:1.6}

.flash{display:flex;align-items:center;gap:10px;padding:11px 15px;border-radius:var(--row-radius);border:.5px solid var(--line);background:var(--panel);font-size:var(--fs-md-plus);color:var(--ink);flex-shrink:0}
.flash.err{border-color:var(--hot);color:var(--hot)}
.flash.ok{border-color:var(--ok);color:var(--ok)}
/* A partial Morning Brief — §10. Not an error: the brief is valid, one source
   simply did not answer, and the reader needs to know which. */
.flash.warn{border-color:var(--warn);color:var(--warn)}

.errs{border:.5px solid var(--hot);border-radius:var(--row-radius);padding:12px 15px;background:var(--panel);flex-shrink:0}
.errs .t{font-family:var(--body);font-size:var(--fs-xs);letter-spacing:.2em;text-transform:uppercase;color:var(--hot);margin-bottom:8px}
.errs ul{margin:0;padding-left:16px}
.errs li{font-size:var(--fs-md-plus);color:var(--linen);line-height:1.7}

/* Drawer panels other than Gallery, inside the approved drawer structure. */
.dr__sec{margin-top:22px}
.dr__sec:first-child{margin-top:0}
.dr__sec>.k{font-size:var(--fs-2xs);letter-spacing:.24em;text-transform:uppercase;color:var(--slate2);padding-bottom:9px;border-bottom:.5px solid var(--line);display:block;font-family:var(--body)}
.prose{font-size:var(--fs-lg);line-height:1.7;color:var(--linen);white-space:pre-wrap}
.act{display:flex;gap:12px;padding:11px 0;border-bottom:.5px solid var(--line)}
.act:last-child{border-bottom:0}
.act .when{font-family:var(--mono);font-size:var(--fs-sm);color:var(--slate2);flex-shrink:0;width:112px}
.act .what{font-size:var(--fs-base);color:var(--linen)}
.act .who{font-size:var(--fs-sm);color:var(--slate2);margin-top:3px}
.ptot{display:flex;justify-content:space-between;gap:16px;padding:12px 0;font-size:var(--fs-lg)}
.ptot.grand{border-top:.5px solid var(--line2);margin-top:4px}
.ptot .k{color:var(--slate2)}
.ptot .v{color:var(--ink);font-family:var(--mono)}
.ptot.grand .v{font-size:var(--fs-xl)}
.pnote{margin-top:14px;font-size:var(--fs-sm-plus);color:var(--warn);letter-spacing:.02em}

/* Desktop-first, but comfortable on laptop and tablet-landscape. The exports
   are drawn on a fixed 1800px artboard; these are the only width rules added. */
.modal{max-width:calc(100% - 48px)}
.drawer{max-width:calc(100% - 72px)}
@media (max-width:1320px){
  .hd__search{width:180px}
}
@media (max-width:1100px){
  .app{grid-template-columns:180px 1fr}
  .mbody{grid-template-columns:1fr 290px}
}

/* Motion: subtle and purposeful, and switched off on request. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:1ms !important;animation-iteration-count:1 !important;transition-duration:1ms !important;scroll-behavior:auto !important}
}

/* ===== authored: classes replacing inline style attributes ==================
   The Content-Security-Policy is style-src 'self' with no unsafe-inline, so
   every rule lives here rather than in a style="" attribute. The exports use
   inline widths and colours because they are static artboards.
   ============================================================================ */
.col-job{width:26%}
.col-type{width:12%}
.col-crew{width:15%}
.col-stage{width:12%}
.col-date{width:12%}
.col-gallery{width:12%}
.col-fee{width:11%;text-align:right}

.tfoot .pg a{font-size:var(--fs-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--slate2);padding:6px 11px;border-radius:var(--chip-radius);border:.5px solid var(--line);transition:color 140ms var(--ease),border-color 140ms var(--ease)}
.tfoot .pg a:hover{color:var(--ink)}
.tfoot .pg a.on{color:var(--ink);border-color:var(--line2)}

.dot-warn{background:var(--warn)}
.dot-neutral{background:var(--slate2)}
.stack-sm{margin-top:2px}
.muted{color:var(--slate)}
.hint--tight{padding-top:11px;font-size:var(--fs-sm)}
.hint--fine{font-size:var(--fs-xs)}
.linklike{font-family:var(--body);font-size:var(--fs-xs);letter-spacing:.14em;text-transform:uppercase;color:var(--slate2);transition:color 140ms var(--ease)}
.linklike:hover{color:var(--ink)}

/* Centred single-column pages: sign in, and the error page. */
.solo{min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--bg);padding:24px}
.solo__inner{width:100%;max-width:380px}
.solo__brand{margin-bottom:28px}
.solo__brand .wm{font-family:var(--sans);font-weight:900;font-size:var(--fs-wordmark);letter-spacing:.02em;color:var(--ink)}
.solo__brand .wm em{font-style:normal;color:var(--slate);font-weight:400}
.solo__brand .tag{display:block;margin-top:6px;font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.3em;text-transform:uppercase;color:var(--slate2)}
.solo h1{font-family:var(--serif);font-weight:400;font-size:30px;color:var(--ink);margin-bottom:22px}
.solo form{display:flex;flex-direction:column;gap:12px}
.solo .flash{margin-bottom:16px}
.solo .primary{margin-top:6px}
.solo__narrow{max-width:420px}
.solo__back{margin-top:18px}

/* The register row: the project title is the link. No inline handler, so the
   strict script-src holds. */
.jt a{color:inherit}
.jt a:hover{color:var(--ink);text-decoration:underline}

/* ===== live polish ==========================================================
   Visual and interaction corrections from live testing. No layout was
   redesigned: these correct drift against the locked export and add the
   interaction affordances a static artboard cannot show.
   ============================================================================ */

/* Icons in containers the export never used them in. */
.chip svg{width:11px;height:11px}
.flash svg{width:13px;height:13px}
.errs svg{width:12px;height:12px}

/* --- Register column geometry ------------------------------------------------
   tbody is display:block and every tr is its own display:table, so the header
   and each body row lay out independently. Column widths therefore have to be
   declared on th AND td or the two drift apart. They are set here, once, keyed
   off the table so the no-Fee variant (a caller without jobs.pricing.view) still
   sums to 100%.
   ------------------------------------------------------------------------------ */
/* The Gallery cell now carries the Files action as well, so it takes four
   points from Job and two from Photographer. Still seven columns, still exactly
   100%. */
.jobs-table{width:100%;border-collapse:collapse;table-layout:fixed}
/* The Production column carries longer words than the Stage column it
   replaced — "Raws uploaded" and "Gallery ready" against "Shot" and "Booked" —
   and a status that truncates is worse than no status. Four points come out of
   Job and Type, both of which were already eliding, and the seven columns
   still total exactly 100. */
.jobs-table .col-job{width:22%}
.jobs-table .col-type{width:10%}
.jobs-table .col-crew{width:13%}
.jobs-table .col-stage{width:16%}
.jobs-table .col-date{width:12%}
.jobs-table .col-gallery{width:16%}
.jobs-table .col-fee{width:11%;text-align:right}

/* Without the Fee column the freed 11% is redistributed across the three
   columns that carry variable-length text, rather than left to stretch. */
.jobs-table--no-fee .col-job{width:27%}
.jobs-table--no-fee .col-crew{width:16%}
.jobs-table--no-fee .col-gallery{width:19%}

/* Long values truncate rather than wrapping a row taller than its neighbours,
   which is what keeps the columns reading as a grid across the desktop range. */
.jobs-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.jobs-table td .jcell{min-width:0}
.jobs-table td .jcell>div{min-width:0}

/* --- Whole-row interaction ----------------------------------------------------
   The row is the target, not just the title. The title stays a real anchor so
   the register works without JavaScript and cmd/middle-click still opens a tab.
   ------------------------------------------------------------------------------ */
tr.row[data-row-href]{cursor:pointer}
tr.row:focus{outline:none}
tr.row:focus-visible{box-shadow:inset 2px 0 0 var(--accent),inset 0 0 0 1px var(--line2);background:var(--panel-hi)}
tr.row[data-row-href]:active{background:var(--panel-hi);box-shadow:inset 2px 0 0 var(--line2)}
/* Genuine controls inside a row keep their own behaviour. */
.jobs-table td a,.jobs-table td button{position:relative;z-index:1}

/* --- Page gutters -------------------------------------------------------------
   Header, KPIs, filter bar and table are all direct children of .ws, so they
   already share one gutter. The workspace scrolls instead of clipping, because
   .main is overflow:hidden and a full page of rows is taller than a laptop
   viewport — the artboard never had to deal with that.
   ------------------------------------------------------------------------------ */
.ws{overflow-y:auto;overflow-x:hidden}
.card{min-height:0}

/* ===== Task 002.1: inline operational edit ==================================
   A compact edit state inside the existing drawer. No new layout language:
   it reuses the chip, hint and button components already in the export.
   ============================================================================ */
.editable{border-bottom:1px dashed var(--line2);transition:color 140ms var(--ease),border-color 140ms var(--ease)}
.editable:hover{color:var(--ink);border-bottom-color:var(--slate2)}
.editable:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

.dr__actions{display:flex;justify-content:flex-end;gap:10px;padding-bottom:14px}

.inline-edit{background:var(--panel2);border:.5px solid var(--line);border-radius:var(--row-radius);padding:14px 15px;margin-bottom:20px;display:flex;flex-direction:column;gap:11px;animation:inlineEditIn 160ms var(--ease) both}
@keyframes inlineEditIn{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:translateY(0)}}
.inline-edit__k{font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.2em;text-transform:uppercase;color:var(--slate2)}
.inline-edit__actions{display:flex;align-items:center;gap:14px}

/* Read-only commercial context in the Edit Job form. */
.readonly-note{display:flex;align-items:flex-start;gap:9px;background:var(--panel2);border:.5px solid var(--line);border-radius:var(--row-radius);padding:12px 14px;font-size:var(--fs-sm-plus);color:var(--slate2);line-height:1.6}
.readonly-note b{color:var(--ink);font-weight:500}
.locked{display:inline-flex;align-items:center;gap:6px;font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.16em;text-transform:uppercase;color:var(--slate2)}

/* ===== Task 003: Client List ================================================
   The Client List reuses the Job Register components wholesale; only the column
   geometry differs. Same rule as the register: tbody is display:block and each
   row is its own table, so widths must be declared for th AND td.
   ============================================================================ */
.clients-table{width:100%;border-collapse:collapse;table-layout:fixed}
.clients-table .col-client{width:30%}
.clients-table .col-contact{width:20%}
.clients-table .col-sector{width:14%}
.clients-table .col-jobs{width:10%}
.clients-table .col-ratecard{width:16%}
.clients-table .col-state{width:10%}

/* Without commercial access the Rate Card column is not rendered at all, so its
   16% is redistributed across the two text-bearing columns. */
.clients-table--no-commercial .col-client{width:38%}
.clients-table--no-commercial .col-contact{width:28%}

.clients-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.clients-table td .jcell{min-width:0}
.clients-table td .jcell>div{min-width:0}

/* ============================================================================
   RETOUCH BOARD
   Column geometry only. The board reuses the register's card, tab strip, row
   gesture, status pill, cell typography and drawer wholesale — nothing here
   declares a colour, a font or a spacing value, and no token was added for it.
   Widths are on th AND td for the same reason as the other two tables.
   ============================================================================ */
.retouch-table{width:100%;border-collapse:collapse;table-layout:fixed}
.retouch-table .col-job{width:32%}
.retouch-table .col-route{width:14%}
.retouch-table .col-count{width:14%}
.retouch-table .col-stage{width:16%}
.retouch-table .col-sent{width:12%}
.retouch-table .col-returned{width:12%}

.retouch-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.retouch-table td .jcell{min-width:0}
.retouch-table td .jcell>div{min-width:0}

/* Small forms sitting inline among drawer content. */
.inline-form{display:inline}
.inline-form button{font-family:var(--body)}

/* ---------------------------------------------------------------------------
   Job Register: nested Files action
   ---------------------------------------------------------------------------
   Lives inside the existing Job cell rather than in a new column, so no column
   width changes and the grid stays exactly as locked. `flex-shrink:0` keeps it
   whole while the title beside it truncates.

   Restrained by default and firmer on row hover or its own focus: it must be
   findable without competing with the job title. It is a real anchor, so it is
   keyboard reachable, and the register's click handler already treats any <a>
   as its own control — the drawer does not open behind it. */
/* Delivery state and the Files route share the Gallery cell. Stacked rather
   than inline: at this column width a row would truncate one of them, and the
   status is what the eye scans first. */
.galcell{display:flex;flex-direction:column;align-items:flex-start;gap:5px;min-width:0}
.galcell .gal{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis}
/* Two actions sit side by side and wrap rather than truncate: the column is
   narrow, and a clipped "View galler…" is worse than a second line. The negative
   margin on `.rowact` aligns the FIRST one with the status text above it, so the
   gap here is the visual gap minus that. */
.galcell__acts{display:flex;align-items:center;gap:2px;flex-wrap:wrap}
/* Delivered. The one place in this cell that is not a neutral state, so it takes
   the existing success colour rather than a new one. */
.gal.ok{color:var(--ok)}
.jobs-table td.col-gallery{white-space:normal}

/* `font-family` and `letter-spacing` are deliberately absent: the shared
   pill/action rule above owns them for every control of this kind, and this
   declaration comes after it, so repeating them here would silently win and
   split the source in two. */
.rowact{flex-shrink:0;display:inline-flex;align-items:center;gap:6px;
  padding:4px 8px;margin-left:-8px;border-radius:var(--chip-radius);border:.5px solid transparent;
  font-size:var(--fs-2xs);
  text-transform:uppercase;color:var(--slate2);text-decoration:none;
  transition:color 140ms var(--ease),border-color 140ms var(--ease),background 140ms var(--ease)}
.rowact svg{width:13px;height:13px;flex-shrink:0}
tr.row:hover .rowact{color:var(--ink);border-color:var(--line);background:var(--panel)}
.rowact:hover{color:var(--ink);border-color:var(--line2);background:var(--panel)}
.rowact:focus-visible{outline:2px solid var(--accent);outline-offset:2px;color:var(--ink)}

/* ---------------------------------------------------------------------------
   New Job: search-first Client picker
   ---------------------------------------------------------------------------
   A short panel, not a long dropdown. The results are real links inside the
   existing modal language, so keyboard order and focus rings come free. */
.cpick__search{display:flex;align-items:stretch;gap:8px}
.cpick__search .fld{flex:1 1 auto}
.cpick__search .btn{flex-shrink:0}
.cpick__scope{font-size:var(--fs-sm-plus);color:var(--slate2);padding:8px 2px 2px;line-height:1.5}
.cpick__new{display:inline-flex;align-items:center;gap:8px;margin-top:10px;
  padding:9px 12px;border-radius:var(--row-radius);border:.5px dashed var(--line2);
  font-family:var(--body);font-size:var(--fs-sm);letter-spacing:.14em;
  text-transform:uppercase;color:var(--slate2);text-decoration:none;
  transition:color 140ms var(--ease),border-color 140ms var(--ease)}
.cpick__new svg{width:13px;height:13px}
.cpick__new:hover{color:var(--ink);border-color:var(--accent)}
.cpick__new:focus-visible{outline:2px solid var(--accent);outline-offset:2px;color:var(--ink)}
.res:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}

/* Visually hidden, still announced. */
.vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ---------------------------------------------------------------------------
   Client monogram
   ---------------------------------------------------------------------------
   Initials in a box, sized to the existing .jthumb slot so the row geometry is
   untouched. No stored asset, no external logo, no per-client colour — a hue
   derived from a name is a hash nobody can predict and two clients will
   collide. Every colour here is a token, so light and dark are handled by the
   same rule the rest of the admin uses. */
.mgram{font-family:var(--sans);font-weight:500;letter-spacing:.06em;
  color:var(--slate2);background:var(--panel-hi);border:.5px solid var(--line);
  display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;
  -webkit-user-select:none;user-select:none}
.jthumb.mgram{width:30px;height:30px;border-radius:var(--chip-radius);font-size:var(--fs-md)}
.mgram--lg{width:32px;height:32px;border-radius:var(--chip-radius);font-size:var(--fs-base)}
tr.row:hover .jthumb.mgram{color:var(--ink);border-color:var(--line2)}

/* The drawer title now carries the same badge, so the panel reads as a
   continuation of the row that opened it. */
.dr__title{display:flex;align-items:center;gap:11px}
.dr__title h3{margin:0;min-width:0}

/* ---------------------------------------------------------------------------
   Rate Cards
   ---------------------------------------------------------------------------
   These screens previously had no stylesheet of their own and inherited only
   the generic rules, which is why the item editor read as a tall stack. Widths
   are declared on th *and* td: tbody is display:block and every row is its own
   table, so a width on one alone lets the grid drift. */
.ratecards-table{width:100%;border-collapse:collapse;table-layout:fixed}
.ratecards-table .col-card{width:34%}
.ratecards-table .col-kind{width:20%}
.ratecards-table .col-items{width:11%}
.ratecards-table .col-confirm{width:13%}
.ratecards-table .col-clients{width:11%}
.ratecards-table .col-state{width:11%}
.ratecards-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ratecards-table td .jcell{min-width:0}
.ratecards-table td .jcell>div{min-width:0}
.ratecards-table .col-items,.ratecards-table .col-confirm,.ratecards-table .col-clients{
  font-variant-numeric:tabular-nums}

/* A category and its rows, in the drawer and in the import preview. */
.sec-block{margin-bottom:18px}
.sec-block:last-of-type{margin-bottom:0}
.sec-hd{display:flex;align-items:center;gap:9px;padding:0 0 6px}
.sec-name{font-size:var(--fs-md);letter-spacing:.16em;text-transform:uppercase;
  color:var(--slate2);font-family:var(--body)}
.sec-note{font-size:var(--fs-sm-plus);color:var(--slate);padding:0 0 8px;line-height:1.5}

.rcitems-table{width:100%;border-collapse:collapse;table-layout:fixed}
.rcitems-table .col-item{width:52%}
.rcitems-table .col-type{width:14%}
.rcitems-table .col-price{width:16%}
.rcitems-table .col-code{width:18%}
.rcitems-table .col-act{width:18%}
.rcitems-table th{padding:8px 10px 7px}
.rcitems-table td{padding:9px 10px;vertical-align:top}
.rcitems-table .col-price{text-align:right;font-variant-numeric:tabular-nums}
.rcitems-table .col-price.num{color:var(--ink);font-size:var(--fs-lg)}
.rcitems-table tr.is-archived{opacity:.5}
.rcitems-table .jt{white-space:normal}
.rcitems-table .cl{white-space:normal}

/* Package vs add-on: a quiet chip, so the distinction survives a fast scan
   without shouting over the prices. */
.ptag{font-size:var(--fs-2xs);letter-spacing:.14em;text-transform:uppercase;
  font-family:var(--body);color:var(--slate2);white-space:nowrap}
.ptag--pkg{color:var(--ink)}

/* Technical metadata stays present but subordinate. */
code.subtle{font-family:var(--mono);font-size:var(--fs-xs);color:var(--slate);
  word-break:break-all}
.terms{font-family:var(--mono);font-size:var(--fs-md);line-height:1.7;
  color:var(--linen);white-space:pre-wrap;margin:0}

/* Compact item editor: two fields to a row, the three commercial controls
   grouped because they are read as one decision. */
.rc-additem{border-top:.5px solid var(--line);margin-top:18px;padding-top:16px;
  display:flex;flex-direction:column;gap:10px}
.rc-additem__hd{font-size:var(--fs-md);letter-spacing:.16em;text-transform:uppercase;
  color:var(--slate2);font-family:var(--body)}
.rc-additem__row{display:grid;gap:10px}
.rc-additem__row--pair{grid-template-columns:minmax(0,1fr) minmax(0,1.4fr)}
.rc-additem__row--commercial{grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr) minmax(0,1.2fr);
  align-items:stretch}
.rc-additem .fld{padding:8px 11px;gap:4px}
.rc-additem .fld--modes{border:.5px solid var(--line);background:var(--panel);
  border-radius:var(--row-radius);display:flex;flex-direction:column;gap:4px;margin:0;min-width:0}
.rc-additem .fld--modes .lbl{padding:0}
.rc-additem .fld--pkg{flex-direction:row;align-items:center;gap:8px}
.rc-additem .chk{display:flex;align-items:center;gap:7px;font-size:var(--fs-md-plus);
  color:var(--linen);cursor:pointer;line-height:1.3}
.rc-additem .chk input{width:auto;flex-shrink:0;accent-color:var(--accent);margin:0}
.rc-additem__foot{display:flex;align-items:center;justify-content:space-between;
  gap:14px;flex-wrap:wrap}
.rc-additem__foot .hint{flex:1 1 240px;line-height:1.5;align-items:flex-start}

@media (max-width:900px){
  .rc-additem__row--pair,.rc-additem__row--commercial{grid-template-columns:minmax(0,1fr)}
}

/* Import preview: name and figures lead, technical metadata beneath. */
.preview-card{padding:18px 20px;margin-bottom:16px}
.preview-card__hd{display:flex;align-items:flex-start;justify-content:space-between;
  gap:16px;flex-wrap:wrap;padding-bottom:12px}
.preview-card__title{display:flex;align-items:center;gap:11px;min-width:0}
.preview-card__title .jt{white-space:normal}
.preview-card__counts{display:flex;align-items:center;gap:14px;flex-shrink:0}
.pc{font-size:var(--fs-sm-plus);letter-spacing:.1em;text-transform:uppercase;
  color:var(--slate2);font-family:var(--body)}
.pc b{font-family:var(--body);font-size:var(--fs-xl);letter-spacing:0;
  color:var(--ink);font-weight:500;margin-right:5px;font-variant-numeric:tabular-nums}
.pc--warn b{color:var(--warn)}
.dr__meta--tight{gap:20px;padding-bottom:14px;border-bottom:.5px solid var(--line);
  margin-bottom:14px}
.dr__meta--tight .m .v{font-size:var(--fs-md-plus)}
.dr__meta--tight .m .v.mono{font-family:var(--mono);color:var(--slate2);
  font-size:var(--fs-sm-plus)}
.preview-card .dr__actions{border-top:.5px solid var(--line);margin-top:16px;
  padding-top:14px}

/* ---------------------------------------------------------------------------
   Schedule
   ---------------------------------------------------------------------------
   A sibling of the Job Register, built from the same surfaces, borders, labels
   and easing. No calendar library, no bright event colours, no gradients: a
   Job block is the register's job cell, given a time and a position.

   Semantic colour is functional only. A lane carries a 2px left edge so crew
   ownership is readable at a glance, and Unassigned is the one state given a
   warning tone — because it is the one that needs acting on. */
.sched{display:flex;align-items:flex-start;overflow-x:auto}
.sched__axis{flex:0 0 62px;position:sticky;left:0;z-index:2;background:var(--panel)}
.sched__lanes{display:flex;align-items:flex-start;flex:1 1 auto;min-width:0}
.sched__lane{flex:1 1 210px;min-width:190px;border-left:.5px solid var(--line)}

.sched__lanehd{display:flex;align-items:center;gap:8px;height:42px;padding:0 12px;
  background:var(--panel2);border-bottom:1px solid var(--line2);
  font-family:var(--body);font-size:var(--fs-2xs);letter-spacing:.18em;
  text-transform:uppercase;color:var(--slate2);white-space:nowrap;overflow:hidden}
.sched__lanehd--axis{justify-content:flex-end;border-right:.5px solid var(--line)}
.sched__mgram{width:22px;height:22px;border-radius:var(--chip-radius);font-size:var(--fs-2xs)}
.sched__lanename{overflow:hidden;text-overflow:ellipsis}
.sched__lanecount{margin-left:auto;color:var(--slate);font-variant-numeric:tabular-nums}

.sched__track{position:relative}
.sched__hour{height:calc(var(--sched-slot) * 4);border-bottom:.5px solid var(--line);
  position:relative}
.sched__axis .sched__hour{border-right:.5px solid var(--line);text-align:right;padding:2px 10px 0 0}
.sched__axis .sched__hour span{font-family:var(--body);font-size:var(--fs-2xs);
  letter-spacing:.1em;color:var(--slate)}

/* A JOB BLOCK — THE NAME, AND NOTHING ELSE.
   ---------------------------------------------------------------------------
   A block is as tall as its Job is long, and the shortest one this grid can
   draw is 30px. Four lines of metadata never fitted in 30px; they were ellipsed
   into initials and half-words, and only became readable at 90 minutes — which
   made a card's usefulness a function of how long the shoot was, which is
   nonsense. So the card says the one thing that identifies the Job, and the
   detail moved to the hover preview, where it has the room to be read.

   Nothing is lost. WHEN the Job is, and how long it runs, are already said by
   where the block sits and how tall it is — that is what a calendar is for. In
   the day view WHO is on it is said again by the lane, which is a crew lane
   with a crew name at its head. Everything else — client, address, service,
   and crew in the week view, where lanes are days — is one hover away and is
   written into the block's accessible name for anyone not using a pointer.

   That last part is what keeps the crew colour honest. The rule elsewhere in
   this sheet is that colour is never the only signal, and on a week block the
   coloured edge is now the only signal a sighted operator gets without
   hovering. It is not the only signal there is: the name is in the preview and
   in the accessible name, so the Schedule still reads correctly in monochrome
   and to a screen reader.

   The vertical rhythm is deliberate. Type is 13.5px on a 1.25 line, so a line
   is ~17px; padding is 5px each side, so a 30px block has 20px of content box
   and a 45px block has 35px. One line always fits the first, two always fit
   the second. That is why the clamp below is safe: the line box is never
   taller than the box it sits in, so text is never cut through the middle of
   a letter — it either fits or it ellipses at a line boundary. */
.sblock{position:absolute;left:3px;right:3px;overflow:hidden;
  display:flex;flex-direction:column;justify-content:center;
  padding:5px 9px;border-radius:var(--chip-radius);text-decoration:none;
  background:var(--panel-hi);border:.5px solid var(--line2);
  border-left:2px solid var(--slate2);box-shadow:0 1px 0 var(--line);
  transition:border-color 140ms var(--ease),background 140ms var(--ease),
             transform 140ms var(--ease)}
.sblock:hover{background:var(--panel2);border-color:var(--line2);transform:translateY(-1px)}
.sblock:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* Two lines by default, one on the 30-minute block — the only height in the
   grid that cannot hold two. `ScheduleGrid::MIN_SLOTS` is 2, so `s-h-2` is the
   floor and this single override covers every short case there is. */
.sblock__title{font-size:var(--fs-lg);line-height:1.25;color:var(--ink);font-weight:500;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;
  overflow:hidden;overflow-wrap:anywhere}
.sblock.s-h-2 .sblock__title{-webkit-line-clamp:1;line-clamp:1}

.sblock--clash{border-left-color:var(--warn)}
.sblock--unassigned{border-left-color:var(--warn);border-style:dashed}

/* GOOGLE CALENDAR — READ-ONLY, AND BEHIND THE WORK.
   ---------------------------------------------------------------------------
   Admin only: the contractor grid is never handed one. A Google event is a
   muted dashed outline beneath the Jobs rather than a card — no shadow, no
   crew edge, not a link. A personal calendar's block is hatched and says only
   "Busy — Josh".

   In a Week column a Job that shares its time with an event keeps the wider
   left side (`sblock--shared`) and the event steps right (`sdiary--beside`),
   so the Job stays what the eye lands on and neither hides the other. The
   strip above the grid holds all-day events and anything outside the axis,
   and is the same height in every column so the hour rows stay level. */
.sched__track .sblock{z-index:1}
/* Two classes, deliberately: the `.s-t-*` placement utilities further down
   set `left:0;right:0` on every positioned block and win on source order
   against a single class. */
.sched__track .sblock--shared{right:38%}
.sblock--busy{border-left-color:var(--warn)}
.sdiary{position:absolute;z-index:0;overflow:hidden;
  padding:3px 7px;border-radius:var(--chip-radius);border:.5px dashed var(--line2);
  background:var(--panel);color:var(--slate)}
.sdiary--person{background:repeating-linear-gradient(135deg,var(--line) 0 1px,transparent 1px 7px),var(--panel)}
.sched__track .sdiary--beside{left:calc(62% + 2px)}
.sdiary__t{display:block;font-size:var(--fs-xs);line-height:1.3;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sallday{display:flex;flex-direction:column;gap:2px;padding:3px;overflow:hidden;
  border-bottom:.5px solid var(--line)}
.sallday--r1{height:calc(var(--sched-slot) * 2)}
.sallday--r2{height:calc(var(--sched-slot) * 3.5)}
.sallday--r3{height:calc(var(--sched-slot) * 5)}
.sallday--axis{justify-content:center;align-items:flex-end;padding-right:10px;
  border-right:.5px solid var(--line);font-size:var(--fs-2xs);letter-spacing:.12em;color:var(--slate)}
.sallday__chip{display:block;flex:0 0 auto;padding:1px 6px;border-radius:var(--chip-radius);
  border:.5px dashed var(--line2);font-size:var(--fs-2xs);line-height:1.5;color:var(--slate);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sallday__chip.sdiary--person{background:repeating-linear-gradient(135deg,var(--line) 0 1px,transparent 1px 7px)}
.sallday__more{border-style:none}
.sched__lane--diary .sched__lanehd{color:var(--slate)}

/* The client drawer's Stock Library tab — Task 017.
   ---------------------------------------------------------------------------
   The Gallery Sets panel's rows, with a photograph at the front: a
   collection is identified by what it looks like as much as what it is
   called, and an operator inspecting one is checking the pictures. */
.stock__l,.stock__imgs{list-style:none;margin:0 0 16px;padding:0;display:flex;flex-direction:column;gap:8px}
.stock__i{display:flex;gap:12px;align-items:flex-start;padding:10px;
  border:.5px solid var(--line);border-radius:var(--row-radius);background:var(--panel-hi)}
.stock__i.is-off{opacity:.62}
.stock__hero{display:block;flex:0 0 96px;width:96px;aspect-ratio:3/2;overflow:hidden;
  border-radius:var(--chip-radius);background:var(--panel2)}
.stock__hero img{width:100%;height:100%;object-fit:cover;display:block}
.stock__hero .jthumb{width:100%;height:100%;border-radius:0}
.stock__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
.stock__name{text-decoration:none}
.stock__name:hover{text-decoration:underline}

.stock__img{display:flex;gap:10px;align-items:center;padding:6px 8px 6px 6px;
  border:.5px solid var(--line);border-radius:var(--row-radius)}
.stock__img.is-hero{border-color:var(--line2)}
.stock__thumb{flex:0 0 64px;width:64px;height:48px;overflow:hidden;border-radius:var(--chip-radius);
  background:var(--panel2)}
.stock__thumb img{width:100%;height:100%;object-fit:cover;display:block}
.stock__thumb .jthumb{width:100%;height:100%;border-radius:0}
.stock__imgname{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;
  font-size:var(--fs-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stock__imgacts{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.stock__imgacts form{display:inline-flex;gap:4px}
.gws__selhint{font-size:var(--fs-sm)}

/* THE INSTAGRAM MODULE — Task 018.
   ---------------------------------------------------------------------------
   Three squares, and nothing that says "social media". No gradient, no brand
   mark, no follower count: the existing dashboard surfaces, edges and label
   type, with photographs in them. It is drawn on the admin Dashboard and on a
   client's, so everything here is written once and both inherit it.

   The tiles are square by `aspect-ratio` rather than by a fixed height, so the
   module is whatever width its column gives it and the squares follow. That is
   what lets the same markup sit in a 35% dashboard column and across a client
   page without a second set of sizes. */
.ig{display:flex;flex-direction:column;min-width:0}

/* The heading and the handle stack, the way Today's heading and its range do.
   The handle used to sit at the far right of the heading row, where it read as
   a second, unrelated thing rather than as the name of the account the heading
   is about. */
.ig__hd{display:flex;flex-direction:column;align-items:flex-start;gap:1px;
  margin-bottom:12px}
.ig__t{margin:0;font-family:var(--serif);font-size:var(--dash-fs-heading);
  font-weight:400;color:var(--dash-ink);letter-spacing:.01em}
.ig__handle{max-width:100%;font-size:var(--dash-fs-meta);color:var(--dash-faint);
  text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  transition:color 140ms var(--dash-ease)}
a.ig__handle:hover{color:var(--dash-muted)}
a.ig__handle:focus-visible{outline:2px solid var(--dash-info);outline-offset:2px;border-radius:2px}

/* TIGHT GUTTERS. The photographs are the module, so the space between them is
   a seam rather than a margin — three squares reading as one band. */
.ig__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;
  list-style:none;margin:0;padding:0}
.ig__cell{min-width:0}

/* THE PHOTOGRAPH IS THE TILE.
   ---------------------------------------------------------------------------
   The media fills the square edge to edge. It is positioned to `inset:0`
   rather than sized in the flow, so no padding, margin or inherited image rule
   anywhere else in this sheet can put a frame between the picture and the
   tile's edge — the failure this replaced was exactly that, a pale inset
   around each photograph on a live screen.

   It carries the tile's radius itself as well as being clipped by
   `overflow:hidden`. The clip alone is not dependable on a parent that is
   transformed, and this tile lifts on hover.

   THERE IS NO BORDER. A 1px hairline is a 1px inset, and against a dark
   photograph on a light card it reads as a frame around the picture rather
   than as an edge to it. The hover ring is a `box-shadow` for the same reason:
   it sits outside the box and insets nothing.

   The background is the plate a lazily-loaded image arrives onto, and is
   covered completely the moment it does. */
.ig__tile{position:relative;display:block;aspect-ratio:1/1;overflow:hidden;
  isolation:isolate;
  border-radius:var(--dash-r-row);background:var(--dash-item);
  transition:transform 160ms var(--dash-ease),box-shadow 160ms ease}
.ig__tile img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:center;display:block;border-radius:inherit}
.ig__tile:hover{transform:translateY(-2px);box-shadow:0 0 0 1px var(--dash-info)}

/* THE PRESENTATION CROP.
   ---------------------------------------------------------------------------
   The remaining white was in the media, not the CSS: the studio's live
   sources are 1440x1800, a 4:5 Instagram canvas with a landscape photograph
   sitting in a white band. `cover` on a square shows the middle 80% of that
   canvas, which is the photograph AND a slice of white above and below it.

   So a TALL source is drawn zoomed, about its centre, past the canvas and
   into the photograph. The number is derived, not tuned: a 3:2 frame on a
   4:5 canvas is 1440x960, and a square window that fits inside 960 of the
   1440 is a zoom of exactly 1440 / 960 = 1.5. A 4:3 frame (1080 tall) is
   covered with room to spare; a 16:9 (810) still shows a sliver, and is not a
   shape a stills photographer posts.

   ONLY a tall source. A landscape or square source cannot be carrying a 4:5
   canvas, so it is drawn as it is — zooming those would crop real pictures
   to fix a problem they do not have. Which side of the line a source is on is
   read from the image's own dimensions once it has loaded (see
   `initInstagramCrop` in admin.js): no pixel is examined, nothing is fetched
   and nothing is proxied. Without scripting the tile is plain `cover`.

   The cost falls on a GENUINE 4:5 portrait, which loses the outer 19% of its
   height on each side beyond what `cover` already took. That is the trade,
   made deliberately: the dimensions alone cannot tell a padded landscape from
   a true portrait, and the studio's feed is the former far more often than
   the latter.

   1.5 is the FLOOR, not the value. On the live feed the photograph sits a
   little inside its canvas, so at exactly 1.5 a sliver of white survived at
   the top and bottom of a tile. 1.6 is a window of 900 rows: 30 rows of
   margin at each end of a 960-row frame, which covers the template as it
   is actually exported. The cost to a genuine portrait is a further 2% of
   its height at each end.

   One number, so it is one change if the template's margins change. */
.ig{--ig-tall-zoom:1.6}
.ig__tile[data-crop="tall"] img{transform:scale(var(--ig-tall-zoom))}
.ig__tile:focus-visible{outline:2px solid var(--dash-info);outline-offset:2px}

/* A Reel reads as a Reel — quietly.
   It used to be a solid white disc, which was the loudest thing in the module
   and argued with the metrics below it. Now it speaks the same language they
   do: a white glyph on a small local darkening, so the two marks on a tile
   look like one decision rather than two. */
.ig__kind{position:absolute;top:7px;right:7px;display:flex;align-items:center;
  justify-content:center;width:17px;height:17px;border-radius:50%;
  background:var(--ig-overlay);color:var(--ig-overlay-ink)}
.ig__kind svg{width:9px;height:9px;stroke:currentColor;fill:none;stroke-width:2.2;
  transform:rotate(-90deg)}

/* THE METRICS.
   ---------------------------------------------------------------------------
   Small white text in the bottom-left corner, and NOT a bar. This used to be a
   full-width strip with a gradient running the whole way across the tile,
   which put a grey band under every photograph and made the module look like a
   widget with a caption rather than a picture.

   The fade is now LOCAL: a radial wash anchored to the bottom-left corner that
   is gone by two-thirds of the way across, so it darkens the few square
   centimetres the numbers sit on and nothing else. The text shadow does the
   rest on a bright photograph, so the wash can stay faint.

   The element is only as wide as its content — `right` is not set — so on a
   tile with one metric the fade is smaller again. Drawn only when a metric was
   actually returned; see `InstagramPost`. */
.ig__stats{position:absolute;left:0;bottom:0;display:flex;align-items:center;gap:9px;
  padding:18px 16px 7px 8px;
  font-family:var(--body);font-size:var(--fs-xs);letter-spacing:.03em;
  color:var(--ig-overlay-ink);text-shadow:0 1px 2px var(--ig-overlay-shadow);
  background:radial-gradient(120% 120% at 0% 100%, var(--ig-overlay) 0%, transparent 70%)}
.ig__stat{display:inline-flex;align-items:center;gap:4px;font-variant-numeric:tabular-nums}
.ig__stat svg{width:10px;height:10px;stroke:currentColor;fill:none;stroke-width:2.2}

/* On the admin Dashboard the module IS the card. The heading sits at the top
   and the band of photographs directly beneath it — no filler between them,
   and whatever height Today does not use is left at the foot rather than
   spread through the module as gaps. */
.ig--panel{height:100%}

/* The client's Dashboard: the module is a section on a page rather than a
   card, so it gets the page's own rhythm above it and nothing around it. */
.ptsocial{margin-top:clamp(26px,3vw,38px)}
/* Capped so three squares land at a photographic ~210px rather than the
   ~500px the full page width would give them. This is the last thing on a
   client's Dashboard, not the biggest — the hero is. */
.ptsocial .ig{max-width:680px}
.ptmsg--ig{display:flex;flex-direction:column;align-items:flex-start;gap:12px}

/* On a phone the three squares stay three across: they are small, but a
   3-column strip of photographs still reads, and stacking them would make the
   module the longest thing on a client's Dashboard. */
@media (max-width:520px){
  .ig__grid{gap:7px}
  .ig__stats{padding:12px 5px 4px;gap:6px}
}

/* Crew colour.
   ---------------------------------------------------------------------------
   Keys come from CrewPalette, which is the only place a person maps to a tone.
   The palette itself lives here, so no hex value is ever stored in the
   database. Every value is a token, so light and dark are handled by the same
   rule as the rest of the admin.

   Colour is a 2px edge and a small dot, and it is never the only signal. Since
   the block was reduced to the Job's name it is no longer text ON the card:
   every day lane is labelled with the crew it belongs to, and on every block in
   both views the crew is named in the hover preview and in the accessible name.
   So the Schedule still reads correctly in monochrome and for a colour-blind
   operator, and reads at all for a screen reader. */
.crew--primary{--crew:var(--ink)}
.crew--crimson{--crew:var(--hot)}
.crew--amber{--crew:var(--warn)}
.crew--violet{--crew:var(--crew-violet)}
.crew--teal{--crew:var(--ok)}
.crew--slate{--crew:var(--slate2)}
.crew--dev{--crew:var(--slate)}
.crew--unassigned{--crew:var(--warn)}

.sched__lane{border-top:2px solid transparent}
.sched__lane.crew--primary,.sched__lane.crew--crimson,.sched__lane.crew--amber,
.sched__lane.crew--violet,.sched__lane.crew--teal,.sched__lane.crew--slate,
.sched__lane.crew--dev,.sched__lane.crew--unassigned{border-top-color:var(--crew)}
.sched__lane .sched__lanehd .sched__lanename::before{content:"";display:inline-block;
  width:6px;height:6px;border-radius:50%;background:var(--crew,var(--slate2));
  margin-right:7px;vertical-align:middle}
.sblock.crew--primary,.sblock.crew--crimson,.sblock.crew--amber,.sblock.crew--violet,
.sblock.crew--teal,.sblock.crew--slate,.sblock.crew--dev,
.sblock.crew--unassigned{border-left-color:var(--crew)}

/* Week: a real time grid. Seven day columns share the axis with the Day view,
   so an 8am block sits at the same height in both and the empty afternoon is
   visible rather than collapsed away. */
.sched--week .sched__lane{flex:1 1 0;min-width:120px}
.sweek__col.is-today .sched__lanehd{color:var(--ink);box-shadow:inset 0 -2px 0 var(--accent)}
.sweek__day{display:flex;flex-direction:column;gap:1px;text-decoration:none;color:inherit;min-width:0}
.sweek__dow{font-size:var(--fs-2xs);letter-spacing:.18em}
.sweek__dom{font-size:var(--fs-sm);letter-spacing:.06em;color:var(--slate)}
.sweek__day:hover .sweek__dow{color:var(--ink)}
/* At week width a block shows time, title and client; crew keeps its own line
   until the column is too narrow to read it. */
/* A week column is narrower, so the title steps down a size. The same maths
   still holds: 13px on a 1.25 line is ~16px, against the same 20px and 35px
   content boxes. */
.sched--week .sblock{padding:5px 7px}
.sched--week .sblock__title{font-size:var(--fs-base)}

/* Current time. Server-rendered at request time, static rather than ticking —
   accurate when the page loads, which is what it is for. */
.snow{position:absolute;left:0;right:0;height:0;border-top:1px solid var(--accent);
  z-index:3;pointer-events:none}
.snow::before{content:"";position:absolute;left:0;top:-3px;width:6px;height:6px;
  border-radius:50%;background:var(--accent)}

.schednav{align-items:center;gap:8px}
.schednav__label{font-family:var(--body);font-size:var(--fs-sm);letter-spacing:.12em;
  text-transform:uppercase;color:var(--ink);padding:0 4px;white-space:nowrap}

@media (prefers-reduced-motion:reduce){
  .sblock{transition:none}
  .sblock:hover{transform:none}

  /* Both states, not just the closed one. `.spop[data-open]` re-declares
     `transition` and outweighs a bare `.spop` here, so silencing only the
     resting rule left the opening animation running for exactly the operator
     who asked for no animation. */
  .spop,
  .spop[data-open]{transition:none}
}

@media (max-width:1180px){
  .sched__lane{min-width:168px}
}

/* THE HOVER PREVIEW.
   ---------------------------------------------------------------------------
   Everything the block used to try to say, said once, with room.

   `position:fixed` because the two things that would otherwise clip it are the
   block itself (`overflow:hidden`, and only 30px tall) and the calendar
   (`overflow-x:auto`). Fixed takes it out of both, and script places it against
   the block's viewport rectangle and flips it to the other side when the right
   edge would run off — see `initSchedulePreview`.

   `pointer-events:none` is the important line. The preview is a convenience for
   a mouse, so it must never become a thing the mouse can hit: without this, the
   panel would slide under the cursor, take the hover away from the block that
   opened it, and flicker. Nothing in it is interactive, so nothing is lost.

   It is not display:none when closed — it needs a measurable size to be placed
   before it is shown — so it is made unreachable instead: `visibility:hidden`
   takes it out of the accessibility tree and out of tab order. */
.spop{position:fixed;z-index:40;top:0;left:0;width:268px;max-width:calc(100vw - 24px);
  padding:13px 15px 14px;border-radius:var(--card-radius);
  background:var(--panel2);border:.5px solid var(--line2);
  box-shadow:0 20px 46px -22px rgba(0,0,0,.55);
  pointer-events:none;visibility:hidden;opacity:0;
  transform:translateY(-3px) scale(.985);transform-origin:left center;
  transition:opacity 140ms var(--ease),transform 140ms var(--ease),
             visibility 0s linear 140ms}
.spop[data-side="left"]{transform-origin:right center}
.spop[data-open]{visibility:visible;opacity:1;transform:none;
  transition:opacity 140ms var(--ease),transform 140ms var(--ease),visibility 0s}

.spop__t{font-size:var(--fs-xl);line-height:1.2;color:var(--ink);font-weight:500;margin:0}
.spop__n{font-family:var(--mono);font-size:var(--fs-xs);letter-spacing:.12em;
  text-transform:uppercase;color:var(--slate);margin:4px 0 0}
.spop__l{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;
  margin:12px 0 0;padding:11px 0 0;border-top:.5px solid var(--line)}
.spop__l dt{font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.16em;
  text-transform:uppercase;color:var(--slate2);padding-top:1px}
.spop__l dd{font-size:var(--fs-sm-plus);line-height:1.35;color:var(--ink);margin:0}
.spop__flag{display:block;margin:11px 0 0;font-family:var(--body);
  font-size:var(--fs-micro);letter-spacing:.16em;text-transform:uppercase;
  color:var(--warn)}
/* Quarter-hour position steps. Generated once, ninety-six of each, so a
   block can sit at its real time under `style-src 'self'` and without
   JavaScript. Both axis and lane use --sched-slot, so they cannot drift. */
.s-t-0,
.s-t-1,
.s-t-2,
.s-t-3,
.s-t-4,
.s-t-5,
.s-t-6,
.s-t-7,
.s-t-8,
.s-t-9,
.s-t-10,
.s-t-11,
.s-t-12,
.s-t-13,
.s-t-14,
.s-t-15,
.s-t-16,
.s-t-17,
.s-t-18,
.s-t-19,
.s-t-20,
.s-t-21,
.s-t-22,
.s-t-23,
.s-t-24,
.s-t-25,
.s-t-26,
.s-t-27,
.s-t-28,
.s-t-29,
.s-t-30,
.s-t-31,
.s-t-32,
.s-t-33,
.s-t-34,
.s-t-35,
.s-t-36,
.s-t-37,
.s-t-38,
.s-t-39,
.s-t-40,
.s-t-41,
.s-t-42,
.s-t-43,
.s-t-44,
.s-t-45,
.s-t-46,
.s-t-47,
.s-t-48,
.s-t-49,
.s-t-50,
.s-t-51,
.s-t-52,
.s-t-53,
.s-t-54,
.s-t-55,
.s-t-56,
.s-t-57,
.s-t-58,
.s-t-59,
.s-t-60,
.s-t-61,
.s-t-62,
.s-t-63,
.s-t-64,
.s-t-65,
.s-t-66,
.s-t-67,
.s-t-68,
.s-t-69,
.s-t-70,
.s-t-71,
.s-t-72,
.s-t-73,
.s-t-74,
.s-t-75,
.s-t-76,
.s-t-77,
.s-t-78,
.s-t-79,
.s-t-80,
.s-t-81,
.s-t-82,
.s-t-83,
.s-t-84,
.s-t-85,
.s-t-86,
.s-t-87,
.s-t-88,
.s-t-89,
.s-t-90,
.s-t-91,
.s-t-92,
.s-t-93,
.s-t-94,
.s-t-95{position:absolute;left:0;right:0}
.s-t-0{top:calc(var(--sched-slot) * 0)}
.s-t-1{top:calc(var(--sched-slot) * 1)}
.s-t-2{top:calc(var(--sched-slot) * 2)}
.s-t-3{top:calc(var(--sched-slot) * 3)}
.s-t-4{top:calc(var(--sched-slot) * 4)}
.s-t-5{top:calc(var(--sched-slot) * 5)}
.s-t-6{top:calc(var(--sched-slot) * 6)}
.s-t-7{top:calc(var(--sched-slot) * 7)}
.s-t-8{top:calc(var(--sched-slot) * 8)}
.s-t-9{top:calc(var(--sched-slot) * 9)}
.s-t-10{top:calc(var(--sched-slot) * 10)}
.s-t-11{top:calc(var(--sched-slot) * 11)}
.s-t-12{top:calc(var(--sched-slot) * 12)}
.s-t-13{top:calc(var(--sched-slot) * 13)}
.s-t-14{top:calc(var(--sched-slot) * 14)}
.s-t-15{top:calc(var(--sched-slot) * 15)}
.s-t-16{top:calc(var(--sched-slot) * 16)}
.s-t-17{top:calc(var(--sched-slot) * 17)}
.s-t-18{top:calc(var(--sched-slot) * 18)}
.s-t-19{top:calc(var(--sched-slot) * 19)}
.s-t-20{top:calc(var(--sched-slot) * 20)}
.s-t-21{top:calc(var(--sched-slot) * 21)}
.s-t-22{top:calc(var(--sched-slot) * 22)}
.s-t-23{top:calc(var(--sched-slot) * 23)}
.s-t-24{top:calc(var(--sched-slot) * 24)}
.s-t-25{top:calc(var(--sched-slot) * 25)}
.s-t-26{top:calc(var(--sched-slot) * 26)}
.s-t-27{top:calc(var(--sched-slot) * 27)}
.s-t-28{top:calc(var(--sched-slot) * 28)}
.s-t-29{top:calc(var(--sched-slot) * 29)}
.s-t-30{top:calc(var(--sched-slot) * 30)}
.s-t-31{top:calc(var(--sched-slot) * 31)}
.s-t-32{top:calc(var(--sched-slot) * 32)}
.s-t-33{top:calc(var(--sched-slot) * 33)}
.s-t-34{top:calc(var(--sched-slot) * 34)}
.s-t-35{top:calc(var(--sched-slot) * 35)}
.s-t-36{top:calc(var(--sched-slot) * 36)}
.s-t-37{top:calc(var(--sched-slot) * 37)}
.s-t-38{top:calc(var(--sched-slot) * 38)}
.s-t-39{top:calc(var(--sched-slot) * 39)}
.s-t-40{top:calc(var(--sched-slot) * 40)}
.s-t-41{top:calc(var(--sched-slot) * 41)}
.s-t-42{top:calc(var(--sched-slot) * 42)}
.s-t-43{top:calc(var(--sched-slot) * 43)}
.s-t-44{top:calc(var(--sched-slot) * 44)}
.s-t-45{top:calc(var(--sched-slot) * 45)}
.s-t-46{top:calc(var(--sched-slot) * 46)}
.s-t-47{top:calc(var(--sched-slot) * 47)}
.s-t-48{top:calc(var(--sched-slot) * 48)}
.s-t-49{top:calc(var(--sched-slot) * 49)}
.s-t-50{top:calc(var(--sched-slot) * 50)}
.s-t-51{top:calc(var(--sched-slot) * 51)}
.s-t-52{top:calc(var(--sched-slot) * 52)}
.s-t-53{top:calc(var(--sched-slot) * 53)}
.s-t-54{top:calc(var(--sched-slot) * 54)}
.s-t-55{top:calc(var(--sched-slot) * 55)}
.s-t-56{top:calc(var(--sched-slot) * 56)}
.s-t-57{top:calc(var(--sched-slot) * 57)}
.s-t-58{top:calc(var(--sched-slot) * 58)}
.s-t-59{top:calc(var(--sched-slot) * 59)}
.s-t-60{top:calc(var(--sched-slot) * 60)}
.s-t-61{top:calc(var(--sched-slot) * 61)}
.s-t-62{top:calc(var(--sched-slot) * 62)}
.s-t-63{top:calc(var(--sched-slot) * 63)}
.s-t-64{top:calc(var(--sched-slot) * 64)}
.s-t-65{top:calc(var(--sched-slot) * 65)}
.s-t-66{top:calc(var(--sched-slot) * 66)}
.s-t-67{top:calc(var(--sched-slot) * 67)}
.s-t-68{top:calc(var(--sched-slot) * 68)}
.s-t-69{top:calc(var(--sched-slot) * 69)}
.s-t-70{top:calc(var(--sched-slot) * 70)}
.s-t-71{top:calc(var(--sched-slot) * 71)}
.s-t-72{top:calc(var(--sched-slot) * 72)}
.s-t-73{top:calc(var(--sched-slot) * 73)}
.s-t-74{top:calc(var(--sched-slot) * 74)}
.s-t-75{top:calc(var(--sched-slot) * 75)}
.s-t-76{top:calc(var(--sched-slot) * 76)}
.s-t-77{top:calc(var(--sched-slot) * 77)}
.s-t-78{top:calc(var(--sched-slot) * 78)}
.s-t-79{top:calc(var(--sched-slot) * 79)}
.s-t-80{top:calc(var(--sched-slot) * 80)}
.s-t-81{top:calc(var(--sched-slot) * 81)}
.s-t-82{top:calc(var(--sched-slot) * 82)}
.s-t-83{top:calc(var(--sched-slot) * 83)}
.s-t-84{top:calc(var(--sched-slot) * 84)}
.s-t-85{top:calc(var(--sched-slot) * 85)}
.s-t-86{top:calc(var(--sched-slot) * 86)}
.s-t-87{top:calc(var(--sched-slot) * 87)}
.s-t-88{top:calc(var(--sched-slot) * 88)}
.s-t-89{top:calc(var(--sched-slot) * 89)}
.s-t-90{top:calc(var(--sched-slot) * 90)}
.s-t-91{top:calc(var(--sched-slot) * 91)}
.s-t-92{top:calc(var(--sched-slot) * 92)}
.s-t-93{top:calc(var(--sched-slot) * 93)}
.s-t-94{top:calc(var(--sched-slot) * 94)}
.s-t-95{top:calc(var(--sched-slot) * 95)}
.s-h-1{height:calc(var(--sched-slot) * 1)}
.s-h-2{height:calc(var(--sched-slot) * 2)}
.s-h-3{height:calc(var(--sched-slot) * 3)}
.s-h-4{height:calc(var(--sched-slot) * 4)}
.s-h-5{height:calc(var(--sched-slot) * 5)}
.s-h-6{height:calc(var(--sched-slot) * 6)}
.s-h-7{height:calc(var(--sched-slot) * 7)}
.s-h-8{height:calc(var(--sched-slot) * 8)}
.s-h-9{height:calc(var(--sched-slot) * 9)}
.s-h-10{height:calc(var(--sched-slot) * 10)}
.s-h-11{height:calc(var(--sched-slot) * 11)}
.s-h-12{height:calc(var(--sched-slot) * 12)}
.s-h-13{height:calc(var(--sched-slot) * 13)}
.s-h-14{height:calc(var(--sched-slot) * 14)}
.s-h-15{height:calc(var(--sched-slot) * 15)}
.s-h-16{height:calc(var(--sched-slot) * 16)}
.s-h-17{height:calc(var(--sched-slot) * 17)}
.s-h-18{height:calc(var(--sched-slot) * 18)}
.s-h-19{height:calc(var(--sched-slot) * 19)}
.s-h-20{height:calc(var(--sched-slot) * 20)}
.s-h-21{height:calc(var(--sched-slot) * 21)}
.s-h-22{height:calc(var(--sched-slot) * 22)}
.s-h-23{height:calc(var(--sched-slot) * 23)}
.s-h-24{height:calc(var(--sched-slot) * 24)}
.s-h-25{height:calc(var(--sched-slot) * 25)}
.s-h-26{height:calc(var(--sched-slot) * 26)}
.s-h-27{height:calc(var(--sched-slot) * 27)}
.s-h-28{height:calc(var(--sched-slot) * 28)}
.s-h-29{height:calc(var(--sched-slot) * 29)}
.s-h-30{height:calc(var(--sched-slot) * 30)}
.s-h-31{height:calc(var(--sched-slot) * 31)}
.s-h-32{height:calc(var(--sched-slot) * 32)}
.s-h-33{height:calc(var(--sched-slot) * 33)}
.s-h-34{height:calc(var(--sched-slot) * 34)}
.s-h-35{height:calc(var(--sched-slot) * 35)}
.s-h-36{height:calc(var(--sched-slot) * 36)}
.s-h-37{height:calc(var(--sched-slot) * 37)}
.s-h-38{height:calc(var(--sched-slot) * 38)}
.s-h-39{height:calc(var(--sched-slot) * 39)}
.s-h-40{height:calc(var(--sched-slot) * 40)}
.s-h-41{height:calc(var(--sched-slot) * 41)}
.s-h-42{height:calc(var(--sched-slot) * 42)}
.s-h-43{height:calc(var(--sched-slot) * 43)}
.s-h-44{height:calc(var(--sched-slot) * 44)}
.s-h-45{height:calc(var(--sched-slot) * 45)}
.s-h-46{height:calc(var(--sched-slot) * 46)}
.s-h-47{height:calc(var(--sched-slot) * 47)}
.s-h-48{height:calc(var(--sched-slot) * 48)}
.s-h-49{height:calc(var(--sched-slot) * 49)}
.s-h-50{height:calc(var(--sched-slot) * 50)}
.s-h-51{height:calc(var(--sched-slot) * 51)}
.s-h-52{height:calc(var(--sched-slot) * 52)}
.s-h-53{height:calc(var(--sched-slot) * 53)}
.s-h-54{height:calc(var(--sched-slot) * 54)}
.s-h-55{height:calc(var(--sched-slot) * 55)}
.s-h-56{height:calc(var(--sched-slot) * 56)}
.s-h-57{height:calc(var(--sched-slot) * 57)}
.s-h-58{height:calc(var(--sched-slot) * 58)}
.s-h-59{height:calc(var(--sched-slot) * 59)}
.s-h-60{height:calc(var(--sched-slot) * 60)}
.s-h-61{height:calc(var(--sched-slot) * 61)}
.s-h-62{height:calc(var(--sched-slot) * 62)}
.s-h-63{height:calc(var(--sched-slot) * 63)}
.s-h-64{height:calc(var(--sched-slot) * 64)}
.s-h-65{height:calc(var(--sched-slot) * 65)}
.s-h-66{height:calc(var(--sched-slot) * 66)}
.s-h-67{height:calc(var(--sched-slot) * 67)}
.s-h-68{height:calc(var(--sched-slot) * 68)}
.s-h-69{height:calc(var(--sched-slot) * 69)}
.s-h-70{height:calc(var(--sched-slot) * 70)}
.s-h-71{height:calc(var(--sched-slot) * 71)}
.s-h-72{height:calc(var(--sched-slot) * 72)}
.s-h-73{height:calc(var(--sched-slot) * 73)}
.s-h-74{height:calc(var(--sched-slot) * 74)}
.s-h-75{height:calc(var(--sched-slot) * 75)}
.s-h-76{height:calc(var(--sched-slot) * 76)}
.s-h-77{height:calc(var(--sched-slot) * 77)}
.s-h-78{height:calc(var(--sched-slot) * 78)}
.s-h-79{height:calc(var(--sched-slot) * 79)}
.s-h-80{height:calc(var(--sched-slot) * 80)}
.s-h-81{height:calc(var(--sched-slot) * 81)}
.s-h-82{height:calc(var(--sched-slot) * 82)}
.s-h-83{height:calc(var(--sched-slot) * 83)}
.s-h-84{height:calc(var(--sched-slot) * 84)}
.s-h-85{height:calc(var(--sched-slot) * 85)}
.s-h-86{height:calc(var(--sched-slot) * 86)}
.s-h-87{height:calc(var(--sched-slot) * 87)}
.s-h-88{height:calc(var(--sched-slot) * 88)}
.s-h-89{height:calc(var(--sched-slot) * 89)}
.s-h-90{height:calc(var(--sched-slot) * 90)}
.s-h-91{height:calc(var(--sched-slot) * 91)}
.s-h-92{height:calc(var(--sched-slot) * 92)}
.s-h-93{height:calc(var(--sched-slot) * 93)}
.s-h-94{height:calc(var(--sched-slot) * 94)}
.s-h-95{height:calc(var(--sched-slot) * 95)}
.s-h-96{height:calc(var(--sched-slot) * 96)}

/* ---------------------------------------------------------------------------
   THE REGISTERS STOP BEING TABLES BELOW 720
   ---------------------------------------------------------------------------
   The skin turns both registers into the shared row-card — see its section
   R4 — and a card cannot lay out while its cells still carry the column
   percentages this sheet gives them: a `td` that is a grid item and also
   `width:22%` forces a 22% track, which is how a project title came to wrap
   one character per line.

   THE RESET LIVES IN THIS SHEET because this sheet owns those widths and the
   skin is guarded against touching them — correctly, and the guard is what
   sent it here. Nothing above 720 changes: every declaration below is inside
   the phone and small-tablet step.

   AND IT LIVES ABOVE THE SHARED-PANEL SECTION, because `visual_system` counts
   how often each list surface is named from that section's marker to the end
   of the sheet — the three panels have to be governed identically or they
   drift. A width reset is not a panel rule, and counting it made the Register
   and Clients look like they had drifted when they had not.
   --------------------------------------------------------------------------- */
@media (max-width:720px){
  .jobs-table .col-job,
  .jobs-table .col-type,
  .jobs-table .col-crew,
  .jobs-table .col-stage,
  .jobs-table .col-date,
  .jobs-table .col-gallery,
  .jobs-table .col-fee,
  .col-job,.col-type,.col-crew,.col-stage,.col-date,.col-gallery,.col-fee{width:auto}
  .col-fee{text-align:left}

  /* The Clients register is the same shape and gets the same treatment. */
  .clients-table .col-client,
  .clients-table .col-contact,
  .clients-table .col-sector,
  .clients-table .col-jobs,
  .clients-table .col-ratecard,
  .clients-table .col-state{width:auto}
}

/* ---------------------------------------------------------------------------
   Task 004.5 — shared list panels, header band and KPI depth
   ---------------------------------------------------------------------------
   ONE set of rules for the Job Register, Clients and Rate Cards, so the three
   list surfaces cannot drift apart. Schedule deliberately does NOT consume
   them: its blocks are positioned on a time axis and a row-panel treatment
   would fight the grid.

   The structure is untouched. Column classes, widths, order, `table-layout:
   fixed`, `tbody{display:block}` and the per-row `display:table` all remain
   exactly as locked — which is precisely what makes this possible: every row is
   already its own table element, so it can take a margin and a radius while the
   column grid stays welded to the header.
   --------------------------------------------------------------------------- */

/* The header band stays Studio Black in BOTH themes. In light mode this is the
   deliberate identity echo — black band, cream labels — and it is why the band
   is tokenised separately from the page surfaces. */
.jobs-table thead,
.clients-table thead,
.ratecards-table thead{background:var(--band)}
.jobs-table thead th,
.clients-table thead th,
.ratecards-table thead th{color:var(--band-ink);border-bottom:0}
.jobs-table thead,
.clients-table thead,
.ratecards-table thead{box-shadow:inset 0 -1px 0 var(--band-line)}

/* Rows become individually separated panels. */
.jobs-table tbody,
.clients-table tbody,
.ratecards-table tbody{padding:8px 8px 4px}
.jobs-table tbody tr,
.clients-table tbody tr,
.ratecards-table tbody tr{
  background:var(--surface-row);
  border:1px solid var(--edge-row);
  border-radius:var(--row-radius);
  margin-bottom:var(--row-gap);
  box-shadow:var(--elev-row);
  /* Luminance, border and accent only. Nothing here moves on hover. */
  transition:background 140ms var(--ease),border-color 140ms var(--ease),
             box-shadow 140ms var(--ease)}
.jobs-table tbody td,
.clients-table tbody td,
.ratecards-table tbody td{border-bottom:0}
.jobs-table tbody tr:last-child,
.clients-table tbody tr:last-child,
.ratecards-table tbody tr:last-child{margin-bottom:0}

/* The row radius has to reach the end cells or the corners square off. */
.jobs-table tbody td:first-child,
.clients-table tbody td:first-child,
.ratecards-table tbody td:first-child{
  border-top-left-radius:var(--row-radius);border-bottom-left-radius:var(--row-radius)}
.jobs-table tbody td:last-child,
.clients-table tbody td:last-child,
.ratecards-table tbody td:last-child{
  border-top-right-radius:var(--row-radius);border-bottom-right-radius:var(--row-radius)}

/* Hover, selected and focus states are defined in the 004.5B block below,
   which supersedes the quieter first pass entirely. */

/* The card is now a frame around panels rather than a surface behind rows.
   ---------------------------------------------------------------------------
   Its edge is the Dashboard's panel edge — 1px of `--edge-card`, a light inner
   highlight rather than the 0.5px near-black outline the locked export drew.
   On a translucent surface over a warm ground, a dark outline reads as a
   pencil line around a piece of paper; the Dashboard has none anywhere. */
.card{background:var(--surface-card);border-radius:var(--card-radius);
  border:1px solid var(--edge-card);box-shadow:var(--elev-card);
  -webkit-backdrop-filter:blur(var(--dash-blur));
  backdrop-filter:blur(var(--dash-blur))}

/* KPI depth.
   Predominantly dark charcoal, with an extremely subtle status cast entering
   from one corner. Dimensional, not decorative: no gradient card, no glow, no
   colour field. The existing layout, geometry and icon colours are unchanged. */
/* Geometry only; surface, edge and lit state are stated in the 004.5B block.
   ---------------------------------------------------------------------------
   THE CORNER CAST IS GONE. Each KPI used to carry a faint coloured wash from
   its top-right corner, tinted per position — mint, amber, rose, green. The
   Dashboard's stat card has nothing of the kind: it is a flat translucent
   panel that LIGHTS UP when its number is non-zero, and the colour is in the
   border and the glow rather than smeared inside the surface. Four permanently
   tinted corners and one conditional glow are two different ideas about what a
   summary card is, and only one of them is the reference. */
.kpi{border-radius:var(--card-radius);position:relative;overflow:hidden;
  transition:border-color 140ms var(--ease),box-shadow 140ms var(--ease)}

/* Controls are treated in the 004.5B glass block below. */

@media (prefers-reduced-motion:reduce){
  .jobs-table tbody tr,.clients-table tbody tr,.ratecards-table tbody tr,
  .kpi,.hd__btn,.filt{transition:none}
}

/* ---------------------------------------------------------------------------
   Task 004.5B — final refinement
   ---------------------------------------------------------------------------
   The 004.5 system was correct but reading too quietly in use. This pushes it
   roughly 15–20%: perceptible atmosphere, an unmistakable row hover, rounder
   panels and smoked-glass controls. Nothing structural moves — every rule here
   changes surface, border, radius or illumination only.
   --------------------------------------------------------------------------- */

/* Row hover and selection.
   ---------------------------------------------------------------------------
   HOVER IS THE DASHBOARD'S HOVER. A row there brightens and takes the emphasis
   edge — `--dash-hi-border`, warm gold in light and mint in dark — and that is
   all. This used to be a mint ring plus a soft outer halo, four signals where
   the reference uses two, and in light mode it was the one teal object on a
   warm cream page.

   The accent has NOT left: it still marks SELECTION and FOCUS below, which is
   the distinction the Dashboard draws too. Hover says "this responds";
   selection says "this is the one you opened". Two states, two colours, and
   they are no longer the same colour at two intensities.

   Position never changes — these are table rows, and a row that moves under
   the cursor makes a dense list unusable. */
.jobs-table tbody tr.row:hover,
.clients-table tbody tr.row:hover,
.ratecards-table tbody tr.row:hover{
  background:var(--surface-row-hover);
  border-color:var(--dash-hi-border)}

.jobs-table tbody tr.row.sel,
.clients-table tbody tr.row.sel,
.ratecards-table tbody tr.row.sel{
  background:var(--surface-row-sel);
  border-color:var(--ring-sel);
  box-shadow:var(--halo-sel),inset 3px 0 0 var(--accent)}

/* Keyboard focus stays at least as loud as hover. */
.jobs-table tbody tr.row:focus-visible,
.clients-table tbody tr.row:focus-visible,
.ratecards-table tbody tr.row:focus-visible{
  background:var(--surface-row-hover);
  border-color:var(--ring-sel);
  box-shadow:var(--halo-sel),inset 3px 0 0 var(--accent);
  outline:none}

/* Panels: rounder and further apart, so they read as long premium panels
   rather than table rows with a line between them. */
.jobs-table tbody,
.clients-table tbody,
.ratecards-table tbody{padding:9px 9px 5px}

/* Smoked glass for pills and compact controls.
   Translucent layered surface with an internal highlight. Smoked, never
   frosted white; the blur is deliberately small so it reads as material and
   not as an effect. In light mode --glass-blur is none and the tokens resolve
   to a cleaner opaque surface instead. */
.filt,
.tfoot .pg button,
.hd__btn.ghost,
.btn:not(.primary){
  background:var(--glass);
  border:1px solid var(--glass-line);
  border-radius:var(--pill-radius);
  /* NO INTERNAL HIGHLIGHT. These carried a top-down white sheen; the
     Dashboard's pill is a flat tint with an edge, and the sheen was the
     difference between "the same pill" and "nearly the same pill". */
  -webkit-backdrop-filter:var(--glass-blur);
  backdrop-filter:var(--glass-blur);
  transition:background 140ms var(--ease),border-color 140ms var(--ease),
             color 140ms var(--ease),box-shadow 140ms var(--ease)}
.filt{padding:8px 15px}
.tfoot .pg button{padding:7px 13px;border-radius:var(--pill-radius)}

.filt:hover,
.tfoot .pg button:hover,
.hd__btn.ghost:not([disabled]):hover,
.btn:not(.primary):hover{
  background:var(--glass-hover);border-color:var(--glass-line-hover);color:var(--ink)}

/* The ghost variant's own ink.
   ---------------------------------------------------------------------------
   It never had any: `.hd__btn` sets `color:var(--bg)` for the FILLED button,
   `.hd__btn.ghost` replaced the background with glass and left the colour
   alone — so every ghost header button in the admin has been drawing the page
   background colour on top of a translucent surface. Dark mode rendered near
   black on near black; light mode rendered cream on white. It was invisible in
   both, which is why nobody read it as a colour bug.

   Stated here rather than folded into the group above, because `.filt` and
   `.btn` already carry their own foreground and must keep it. */
.hd__btn.ghost{color:var(--ink)}

.filt.on,
.tfoot .pg button.on{
  border-color:var(--ring-hover);color:var(--ink);box-shadow:var(--halo-hover)}

/* THE PRIMARY ACTION, RECONCILED TO THE DASHBOARD.
   ---------------------------------------------------------------------------
   This used to give the primary a mint EDGE and a glass highlight over a
   near-black fill, while the Dashboard's one call to action was a warm
   near-black slab with a warm drop shadow and no edge at all. Held side by
   side they read as two different buttons doing the same job.

   The fill, ink, edge and shadow now come from `--btn-primary*`, which the
   Dashboard's own `--dash-btn*` aliases — so there is exactly one primary
   button treatment in the application, and mint is still never a solid fill:
   in dark it is a 20%-alpha wash with a mint edge, which is the Dashboard's. */
.hd__btn:not(.ghost),
.btn.primary,
.primary:not(.dim),
.gbtn.pri,
.pop .go{
  border:1px solid var(--btn-primary-border);
  box-shadow:var(--btn-primary-shadow)}

/* HEADER BUTTONS ARE SQUARED, not lozenges.
   ---------------------------------------------------------------------------
   The two rules above hand `--pill-radius` (999px) to everything in the button
   family, which is right for a filter chip and wrong for a header action: New
   Job was a 36px-tall lozenge sitting directly above a 16px-radius card and
   beside a 10px-radius search field, and read as a different species from both.

   10px is the search field's own radius — the two sit side by side in the same
   header and now share a corner. This is the last word on `.hd__btn`'s radius
   because it comes after both rules above; the small `.btn` / `.filt` chips are
   untouched and stay pills, which is what they should be. */
/* AND THE BUTTON BESIDE IT TAKES THE SAME CORNER — which is the Dashboard's
   call-to-action corner, `--card-radius`. The earlier note here argued for the
   search field's 10px because the two sat side by side and had to agree; that
   argument is unchanged, only the value it lands on has moved to the one the
   reference states. */
.hd__btn,
.hd__btn.ghost,
.hd__btn:not(.ghost),
.hd__share>summary.hd__btn{border-radius:var(--card-radius)}
/* AND IT HOVERS THE WAY THE DASHBOARD'S DOES: it lifts, and its shadow
   deepens under it. Nothing changes colour.
   ---------------------------------------------------------------------------
   The rule that stood here replaced the button's background-color with
   `--accent-dim` and set its text to `--ink` — so on hover a black button with
   cream text became a pale mint button with dark text. That is not a hover
   state, it is a different button, and it was a legibility trap waiting for
   the day the two tokens moved closer together. */
.hd__btn:not(.ghost):not([disabled]):hover,
.btn.primary:hover,
.primary:not(.dim):hover,
.gbtn.pri:hover,
.pop .go:hover{
  transform:translateY(-2px);box-shadow:var(--dash-glass-shadow-hi)}
.hd__btn:not(.ghost):not([disabled]):active,
.btn.primary:active,
.primary:not(.dim):active,
.gbtn.pri:active,
.pop .go:active{transform:scale(.97)}

@media (prefers-reduced-motion:reduce){
  .hd__btn:not(.ghost):not([disabled]):hover,
  .btn.primary:hover,
  .primary:not(.dim):hover,
  .gbtn.pri:hover,
  .pop .go:hover{transform:none}
}

/* KPI material.
   THE KPI CARD IS THE DASHBOARD'S STAT CARD. Same translucent surface, same
   backdrop blur, same light edge, same warm shadow — and the same lit state,
   which is the part that was actually broken: the alert rule set a warn border
   and a glow near the top of this sheet, and this rule sat after it at equal
   specificity, so for as long as both existed the alert never lit. Awaiting
   Action could hold three jobs and look exactly like a card holding none.
   Stated together here so there is one last word rather than a race. */
.kpi{background:var(--surface-card);border:1px solid var(--edge-card);
  box-shadow:var(--elev-card);
  -webkit-backdrop-filter:blur(var(--dash-blur));
  backdrop-filter:blur(var(--dash-blur))}
.kpi.kpi--alert{border-color:var(--dash-warn-border);
  box-shadow:var(--dash-warn-glow),var(--elev-card)}
.kpi:hover{border-color:var(--line2);box-shadow:var(--elev-card),var(--halo-hover)}
.kpi.kpi--alert:hover{border-color:var(--dash-warn-border);
  box-shadow:var(--dash-warn-glow),var(--elev-card),var(--halo-hover)}

@media (prefers-reduced-motion:reduce){
  .filt,.tfoot .pg button,.hd__btn,.btn,.kpi{transition:none}
}

/* ---------------------------------------------------------------------------
   Task 004.5B repair — rows must round as one continuous panel
   ---------------------------------------------------------------------------
   THE ACTUAL CAUSE, not the symptom.

   `border-radius` has no effect on a table element while `border-collapse:
   collapse` is in force — that is the CSS spec, not a browser quirk. And
   `border-collapse` is an *inherited* property, so every row (each of which is
   `display:table`, the mechanic that keeps columns welded to the header) was
   inheriting `collapse` from its parent table and silently discarding its own
   radius. The same applied to the first/last cell corners.

   So the panel painted a rectangle while the surrounding `.card` — an ordinary
   div — rounded correctly, which is exactly the "rounded outer, rectangular
   inner" the review saw. The hover ring traced a rectangle for the same reason.

   Raising the radius would have changed nothing.

   The fix is to separate the borders and remove the spacing, which restores
   radius support without altering geometry: `border-spacing:0` leaves cells
   exactly where `collapse` had them, `table-layout:fixed` and every column
   width are untouched, and the doubled cell borders `separate` would normally
   reintroduce are already zeroed on these three tables.

   `.rcitems-table` is deliberately excluded: it lives inside the Rate Card
   drawer as a data table, not as a list-panel surface. */
.jobs-table,
.clients-table,
.ratecards-table{border-collapse:separate;border-spacing:0}
.jobs-table thead,.jobs-table tbody,.jobs-table tr,
.clients-table thead,.clients-table tbody,.clients-table tr,
.ratecards-table thead,.ratecards-table tbody,.ratecards-table tr{
  border-collapse:separate;border-spacing:0}

/* With radius now honoured, the cells must not paint a square over the rounded
   parent. They carry no background of their own; the end cells simply repeat
   the row radius so a background applied to one later cannot square a corner. */
.jobs-table tbody td,
.clients-table tbody td,
.ratecards-table tbody td{background:transparent}

/* The header band keeps square top corners inside the card, and the card's own
   overflow clips them — the band is a full-bleed surface, not a panel. */
.jobs-table thead th,
.clients-table thead th,
.ratecards-table thead th{border-radius:0}

/* Hover, selected and focus illumination.
   All three are box-shadows on the row itself, so they inherit the row's
   border-radius by construction — there is no second radius value anywhere to
   drift out of step with --row-radius. That is deliberate: an `outline` would
   trace the border box and reintroduce the rectangle, which is what the
   original global `.row:focus-visible` rule was doing before the more specific
   rule below turned it off. */
.jobs-table tbody tr.row,
.clients-table tbody tr.row,
.ratecards-table tbody tr.row{outline:none}
.jobs-table tbody tr.row:focus-visible,
.clients-table tbody tr.row:focus-visible,
.ratecards-table tbody tr.row:focus-visible{outline:none}

/* The card already carries `overflow:hidden` from the locked export, so it
   clips anything reaching its edge. No second rule is needed. */

/* ---------------------------------------------------------------------------
   Job drawer — Files tab (Task 006)

   No new tokens and no new visual language. This is the locked 004.5B system
   applied to a list that happens to hold files: the same --row-radius the Job
   Register panels use, the same --glass smoked surface as the compact
   controls, the same mint --accent on hover, the same type ramp.

   Deliberately NOT a file manager. No grid of large tiles, no breadcrumb, no
   folder tree — a Job's files are a short operational list read at the same
   density as everything else in the drawer, and the thumbnail is small because
   it is an aid to recognition rather than the point of the screen.
   --------------------------------------------------------------------------- */
.filelist{margin-top:16px;display:flex;flex-direction:column;gap:var(--row-gap)}
.fileitem{display:flex;align-items:center;gap:13px;padding:10px 12px;
  background:var(--surface-row);border:.5px solid var(--edge-row);
  border-radius:var(--row-radius);box-shadow:var(--elev-row);
  transition:border-color 140ms var(--ease),box-shadow 140ms var(--ease)}
.fileitem:hover{border-color:var(--ring-hover);box-shadow:var(--halo-hover)}

/* Fixed square, so a portrait and a landscape frame line up down the list. */
.fileitem__thumb{width:42px;height:42px;flex-shrink:0;border-radius:var(--row-radius);
  overflow:hidden;display:flex;align-items:center;justify-content:center;
  background:var(--glass);border:.5px solid var(--glass-line);
  backdrop-filter:var(--glass-blur)}
.fileitem__thumb img{width:100%;height:100%;object-fit:cover;display:block}
.fileitem__thumb.is-plain svg{width:15px;height:15px;stroke:var(--slate2);fill:none}

.fileitem__body{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
.fileitem__name{font-size:var(--fs-md-plus);color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fileitem__meta{font-family:var(--body);font-size:var(--fs-2xs);letter-spacing:.12em;
  text-transform:uppercase;color:var(--slate2)}
.fileitem__note{font-size:var(--fs-xs);color:var(--slate)}

/* Add files. Same geometry as .inline-edit, because it is the same idea: a
   compact server-rendered form inside the drawer that never leaves context. */
.fileadd{margin-top:22px;background:var(--panel2);border:.5px solid var(--line);
  border-radius:var(--row-radius);padding:14px 15px;display:flex;flex-direction:column;gap:11px}
.fileadd__k{font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.2em;
  text-transform:uppercase;color:var(--slate2)}
.fileadd__row{display:flex;gap:10px;align-items:stretch;flex-wrap:wrap}
.fileadd__input{flex:1 1 240px;font-size:var(--fs-md);color:var(--ink);
  flex-direction:row;align-items:center}
.fileadd__role{display:flex;flex-direction:column;gap:5px}
.fileadd__role .fld{padding:8px 11px;font-size:var(--fs-md);color:var(--ink);
  font-family:var(--body)}
.fileadd__actions{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.fileadd__actions .hint{flex:1 1 220px;line-height:1.5;align-items:flex-start}

/* ---------------------------------------------------------------------------
   Files tab layout: one intake card, two action cards, one list.
   ---------------------------------------------------------------------------
   The tab had grown into a column of stacked forms, each with a paragraph
   explaining itself, and the files — the thing an operator opened the tab to
   see — had been pushed below all of it. The actions are now cards: source
   intake leads because it is where a job's files actually arrive, finals and
   supporting files sit side by side because they are peers, and the list sits
   under them where it can be read.

   No new token, no new colour. The intake card is the same --glass smoked
   surface as the thumbnails and the compact controls; the action cards are the
   same --panel2 the add-file form already used. The only new geometry is a
   two-column grid.
   --------------------------------------------------------------------------- */
.fcard{background:var(--panel2);border:.5px solid var(--line);border-radius:var(--row-radius);
  padding:14px 15px;display:flex;flex-direction:column;gap:11px;min-width:0}
.fcard__hd{display:flex;align-items:center;justify-content:space-between;gap:12px}
.fcard__t{font-family:var(--body);font-size:var(--fs-2xs);letter-spacing:.24em;
  text-transform:uppercase;color:var(--slate2)}
.fcard__t svg{width:11px;height:11px;vertical-align:-1px;margin-right:7px}

/* The lead card. Raised a step above the two beneath it, because a job's
   source files arrive here and nowhere else. */
.fcard--intake{background:var(--glass);border-color:var(--glass-line);
  backdrop-filter:var(--glass-blur);box-shadow:var(--elev-row);padding:16px 17px}
.fcard--intake .fcard__t{color:var(--ink)}

/* One line, ellipsised. A long intake folder name must not wrap the card. */
.fcard__folder{font-size:var(--fs-md-plus);color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* ---------------------------------------------------------------------------
   VIDEO FILES

   A third card beneath the Final/Other pair rather than beside them. It is a
   list rather than an upload form — the bytes arrive over SFTP — so it reads
   as a register of what is on the server, not as somewhere to put something.
   --------------------------------------------------------------------------- */
.fcard__meta{font-size:var(--fs-2xs);color:var(--slate2);letter-spacing:.08em}
.fcard__act{margin-top:2px}
.fcard__acts{display:flex;flex-wrap:wrap;gap:8px}
.fcard--video{margin-top:12px}

.vidlist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.vidlist__i{border:.5px solid var(--line);border-radius:var(--row-radius);
  background:var(--panel);padding:11px 12px}
.vidlist__f{display:flex;flex-direction:column;gap:9px}
.vidlist__hd{display:flex;align-items:center;gap:10px}
/* The title is the field an operator actually edits, so it takes the room. */
.vidlist__title{flex:1 1 auto;min-width:0}

/* One badge, and the states are not decorative: `warn` means the expiry sweep
   is refusing to delete a file that is past retention because the archive is
   not confirmed. That is correct behaviour which is invisible anywhere else,
   so it is the loudest thing on the row. */
.vidlist__badge{font-size:var(--fs-2xs);letter-spacing:.1em;text-transform:uppercase;
  white-space:nowrap;padding:3px 8px;border-radius:999px;border:.5px solid var(--line);
  color:var(--slate2)}
.vidlist__badge--live{color:var(--ink)}
.vidlist__badge--ok{color:var(--ok);border-color:currentColor}
.vidlist__badge--warn{color:var(--warn);border-color:currentColor}
.vidlist__badge--gone{color:var(--slate2);opacity:.75}

.vidlist__meta{display:flex;flex-wrap:wrap;gap:12px;font-size:var(--fs-2xs);color:var(--slate2)}
.vidlist__row{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end}
.vidlist__k{display:flex;flex-direction:column;gap:4px;font-size:var(--fs-2xs);color:var(--slate2)}
.vidlist__pos{width:72px}
.vidlist__chk{display:flex;align-items:center;gap:7px;font-size:var(--fs-2xs);color:var(--slate2)}
.vidlist__acts{display:flex;gap:8px;flex-wrap:wrap}

@media (max-width:640px){
  .vidlist__hd{flex-wrap:wrap}
  .vidlist__row{flex-direction:column;align-items:stretch}
}

/* Two peers, side by side, collapsing on a narrow viewport. */
/* Stretch, not start: two peers should read as two peers, and one card an inch
   taller than the other because it carries a select does not. */
.fgrid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}
.fgrid .fileadd{height:100%}
.fgrid .fileadd__actions{margin-top:auto}
@media (max-width:900px){
  .fgrid{grid-template-columns:minmax(0,1fr)}
}

/* Inside a card the form is the card: no second border, no second background. */
.fcard .fileadd{margin-top:0;background:none;border:0;border-radius:0;padding:0;gap:10px}
.fcard .fileadd__row{gap:8px}
.fcard .fileadd__input{flex:1 1 100%}

/* Development only, and it should look it: present, legible, not competing. */
.fcard--dev{margin-top:26px;opacity:.55;transition:opacity 140ms var(--ease)}
.fcard--dev:hover,.fcard--dev:focus-within{opacity:1}

@media (prefers-reduced-motion:reduce){
  .fcard--dev{transition:none}
}

/* The two intake actions are the same size.
   ---------------------------------------------------------------------------
   Primary is carried by contrast — a filled `.gbtn pri` against a glass `.btn`
   — and not by making one of them bigger. They read as a pair of things you
   might do, which is what they are: one sends a contractor to the folder, the
   other asks whether anything has landed. */
.fcard--intake .fileadd__actions .gbtn,
.fcard--intake .fileadd__actions .btn{
  padding:8px 13px;border-radius:var(--pill-radius);
  font-size:var(--fs-sm);letter-spacing:.1em;gap:6px;line-height:1.25}
.fcard--intake .fileadd__actions .gbtn svg,
.fcard--intake .fileadd__actions .btn svg{width:11px;height:11px}

/* `.btn` carries a 1px border and `.gbtn` does not, which left the filled pill
   2px shorter than the glass one. A transparent border of the same width makes
   the two boxes identical without giving the filled pill an edge. */
.fcard--intake .fileadd__actions .gbtn{border:1px solid transparent}

/* ---------------------------------------------------------------------------
   Job files summary — a count, not a file browser
   --------------------------------------------------------------------------- */
/* One line: how many, and the way in. The format breakdown and the stage
   totals that used to sit here were answering a question nobody opened the
   drawer to ask; the count is the glance, and View files is the rest. */
.fsum{display:flex;align-items:center;justify-content:space-between;
  gap:14px;flex-wrap:wrap;margin-top:14px}
.fsum__total{font-family:var(--body);font-size:var(--fs-2xs);letter-spacing:.16em;
  text-transform:uppercase;color:var(--slate2)}
.fsum__total b{font-family:var(--body);font-size:var(--fs-lg);letter-spacing:0;
  font-weight:500;color:var(--ink);margin-right:5px}
.fsum__act{display:flex}

/* ---------------------------------------------------------------------------
   Retouch notes
   --------------------------------------------------------------------------- */
.rnotes__row{display:flex;gap:10px;flex-wrap:wrap}
.rnotes__row .fileadd__role{flex:1 1 110px}
.rnotes__check{align-self:flex-start}
/* The sky instruction's labels are sentences rather than words — "New blue
   skies in all images" — so its control needs a basis wide enough to show the
   chosen one rather than an ellipsis. Same flex idiom as the row it sits in;
   no new geometry. */
.rnotes__wide{flex:1 1 240px}
.rnotes__text{min-height:64px}

/* A textarea used AS the field, rather than inside one.
   ---------------------------------------------------------------------------
   `.fld textarea` (line ~403) styles a control *wrapped* by a field, and it
   sets the colour. A textarea carrying `.fld` itself matches neither that rule
   nor `.fileadd__role .fld`, and form controls do not inherit `color` from
   their parent — so it fell back to the user agent's black on a dark panel.

   Both bare textareas in the drawer had it: this one and the retouch revision
   note. Fixed on the shape rather than on the one instance, because scoping it
   to `.rnotes__text` would knowingly leave the identical bug next door. */
textarea.fld{color:var(--linen);font-family:var(--body);font-size:var(--fs-base);
  line-height:1.5;resize:vertical}
textarea.fld::placeholder{color:var(--slate)}

/* New blue skies, ON.
   ---------------------------------------------------------------------------
   The generic checked pill is a quiet state change — right for picking crew off
   a list of names, too quiet for a single switch that changes what a retoucher
   does to every frame. So the indicator becomes a red status light and the pill
   takes a restrained red edge: enough to read as live across the room, not so
   much that it reads as an error.

   `--hot` is the existing red, so this adds no token, and it resolves per
   theme. The glow is mixed from the same token rather than hard-coded.

   OFF is untouched — `.dot-neutral` still supplies the grey — and focus is
   untouched too: the `:focus-visible` outline rule above is a separate
   property from `box-shadow`, so both render together. */
.chipwrap input:checked+.crewpill.alert{color:var(--hot);border-color:var(--hot);
  box-shadow:0 0 0 1px color-mix(in srgb,var(--hot) 35%,transparent),
             0 0 10px color-mix(in srgb,var(--hot) 22%,transparent)}
.chipwrap input:checked+.crewpill.alert i{background:var(--hot);
  box-shadow:0 0 6px color-mix(in srgb,var(--hot) 70%,transparent)}

/* ---------------------------------------------------------------------------
   Task 008A — Single Gallery admin

   The thumbnail grid is the screen. Everything else is deliberately quiet: the
   counts are one line of the same micro-caps used across the drawer, the two
   per-image controls stay out of the way until the pointer or the keyboard
   reaches them, and the only permanent marks on a frame are the two that carry
   meaning — Hero and Hidden.

   No new token is introduced. Hero uses the operational accent, because it is a
   state; the band colour behind the badges is the existing header band, which is
   near-black in both themes and therefore legible over any photograph.
   --------------------------------------------------------------------------- */

/* The counts, and the two actions that apply to the whole gallery. */
.galhd{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;
  padding-bottom:11px;border-bottom:.5px solid var(--line)}
.galhd__c{font-family:var(--body);font-size:var(--fs-2xs);letter-spacing:.2em;
  text-transform:uppercase;color:var(--slate2)}
.galhd__c b{font-family:var(--body);font-size:var(--fs-lg);letter-spacing:0;
  font-weight:500;color:var(--ink)}
.galhd__sep{padding:0 5px;color:var(--line2)}
/* Wraps, because the drawer is a proportion of the viewport and the two labels
   together are wider than it gets on a narrow screen. */
.galhd__a{display:flex;align-items:center;gap:8px;flex-wrap:wrap}

/* The action form carries the CSRF token and the Job for every tile control.
   It has no visible content and must take no space. */
.galact{display:none}

.galform{margin:0}

/* Auto-fill rather than a fixed column count: the drawer is a fixed width today,
   but the same grid has to survive a wider one without a media query per size. */
.galgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(108px,1fr));
  gap:10px;margin-top:16px;list-style:none;padding:0;align-items:start}
.galgrid__i{display:flex;flex-direction:column;gap:6px;min-width:0}
/* The frame and everything overlaid on it. Badges, the select tick, the action
   pair and the drag grip are siblings of the frame rather than children of it —
   a button inside a link is invalid, and it made Hero and Hide impossible to
   press without also opening the viewer — so they position against this box,
   which is exactly the size of the picture and nothing else. */
.galgrid__box{position:relative;display:block}

/* The frame. Width-driven: the column decides how wide, the photograph decides
   how tall.
   ---------------------------------------------------------------------------
   This used to be a fixed 4:3 box with `object-fit:cover`, which made every
   frame the same shape by cropping whatever did not fit — so a portrait read as
   a landscape with its top and bottom cut off, which is exactly the wrong thing
   to show somebody reviewing a delivery. Now the image lays out at its own
   intrinsic ratio inside a fixed-width column, and the tile is as tall as the
   picture is.

   Rows therefore do not band to a single height, and that is the point: an
   admin review grid should show the shoot, not a tidy wall of identical
   rectangles. `align-items:start` on the grid keeps each tile its own height.

   The ratio comes from the image itself because ImageBooth does not record one:
   `assets.width` and `assets.height` exist in the schema and have never been
   written. Reserving space from stored dimensions would be the better fix and
   belongs with the derivative pipeline, which is its own task. `min-height`
   holds a little space until the header arrives so the grid does not start at
   zero and jump. */
.galgrid__fr{position:relative;display:block;overflow:hidden;min-height:74px;
  border-radius:var(--row-radius);background:var(--panel2);border:.5px solid var(--edge-row);
  box-shadow:var(--elev-row);
  transition:border-color 140ms var(--ease),box-shadow 140ms var(--ease),opacity 140ms var(--ease)}
.galgrid__i:hover .galgrid__fr,.galgrid__i:focus-within .galgrid__fr{
  border-color:var(--ring-hover);box-shadow:var(--halo-hover)}
/* No `object-fit`, no crop, no forced height. Portrait reads as portrait,
   landscape as landscape, square as square. */
.galgrid__fr img{width:100%;height:auto;display:block}

/* A final with no drawable bytes has no intrinsic ratio to derive a height
   from, so this one keeps a box — otherwise it would collapse to its min. */
.galgrid__fr--plain{aspect-ratio:4/3}

.galgrid__ph{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.galgrid__ph svg{width:17px;height:17px;stroke:var(--slate);fill:none}

/* Hero and Hidden. Permanent, because they are facts about the image rather than
   actions available on it. */
.galgrid__badge{position:absolute;left:6px;top:6px;display:inline-flex;align-items:center;gap:4px;
  font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.16em;font-weight:500;
  text-transform:uppercase;padding:3px 7px;border-radius:var(--pill-radius);
  background:var(--band);color:var(--accent)}
.galgrid__badge svg{width:9px;height:9px;stroke:var(--accent);fill:none}
.galgrid__badge--off{left:auto;right:6px;color:var(--band-ink)}

.galgrid__i.is-hidden .galgrid__fr img,.galgrid__i.is-hidden .galgrid__ph{opacity:.28}
.galgrid__i.is-hidden .galgrid__name{color:var(--slate)}

/* Two controls, revealed on hover or on keyboard focus. They stay in the DOM and
   stay focusable when invisible, so tabbing into one brings it into view. */
.galgrid__acts{position:absolute;right:6px;bottom:6px;display:flex;gap:5px;
  opacity:0;transform:translateY(3px);
  transition:opacity 140ms var(--ease),transform 140ms var(--ease)}
.galgrid__i:hover .galgrid__acts,.galgrid__i:focus-within .galgrid__acts{opacity:1;transform:none}
.galgrid__act{display:inline-flex;align-items:center;justify-content:center;
  width:25px;height:25px;border-radius:var(--chip-radius);color:var(--linen);
  background:var(--glass);border:.5px solid var(--glass-line);
  -webkit-backdrop-filter:var(--glass-blur);backdrop-filter:var(--glass-blur);
  transition:background 140ms var(--ease),color 140ms var(--ease),border-color 140ms var(--ease)}
.galgrid__act svg{width:12px;height:12px;stroke:currentColor;fill:none}
.galgrid__act:hover{background:var(--glass-hover);border-color:var(--glass-line-hover);color:var(--ink)}
.galgrid__act:active{transform:scale(.94)}
.galgrid__act.on{background:var(--ink);color:var(--bg);border-color:transparent}
.galgrid__act:focus-visible{outline:2px solid var(--accent);outline-offset:2px;opacity:1}

/* The drag affordance. A mark, not a button: the whole tile is the drag target,
   and this says so without adding a third control to every frame. */
.galgrid__grip{position:absolute;left:6px;bottom:6px;display:flex;opacity:0;
  transition:opacity 140ms var(--ease)}
.galgrid__grip svg{width:13px;height:13px;stroke:var(--band-ink);fill:none}
.galgrid__i:hover .galgrid__grip{opacity:.7}

.galgrid__cap{display:flex;align-items:baseline;gap:6px;min-width:0}
.galgrid__n{font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.12em;
  color:var(--slate);flex-shrink:0}
.galgrid__name{font-size:var(--fs-2xs);color:var(--slate2);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

.galsave{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:16px}

/* Set by the script, so the grab cursor only appears where dragging works. */
.galgrid.is-draggable .galgrid__fr{cursor:grab}
.galgrid__i.is-dragging .galgrid__fr{opacity:.35;border-color:var(--ring-sel)}

@media (prefers-reduced-motion:reduce){
  .galgrid__fr,.galgrid__acts,.galgrid__act,.galgrid__grip{transition:none}
}

/* ---------------------------------------------------------------------------
   Task 008A — admin gallery preview

   What a client would be shown, for somebody who is already signed in. It is
   deliberately plainer than the admin grid: no controls, no badges, no hover
   states, because the point is to see the arrangement rather than to work on it.
   --------------------------------------------------------------------------- */
/* Prominent, not a billboard. Without the cap a 3:2 hero across a 1240px
   workspace is 827px tall, which pushes every other image below the fold and
   makes the preview a picture of one photograph rather than of a gallery. */
.galprev__hero{margin-bottom:14px}
/* An explicit height rather than a max-height: with `aspect-ratio` still in
   force a capped height shrinks the WIDTH to preserve the ratio, which left the
   hero floating at half the workspace. Overriding the ratio keeps it full width
   and lets object-fit crop the frame. */
.galprev__hero .galprev__fr{aspect-ratio:auto;height:clamp(260px,46vh,460px)}
.galprev__fr{position:relative;display:block;overflow:hidden;border-radius:var(--card-radius);
  background:var(--panel2);border:.5px solid var(--edge-row);aspect-ratio:3/2}
.galprev__fr img{width:100%;height:100%;object-fit:cover;display:block}
.galprev__ph{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.galprev__ph svg{width:22px;height:22px;stroke:var(--slate);fill:none}
.galprev__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));
  gap:12px;list-style:none;padding:0}
.galprev__grid .galprev__fr{aspect-ratio:4/3;border-radius:var(--row-radius)}

/* ---------------------------------------------------------------------------
   Task 008B — the Single Gallery workspace

   A page, not a drawer. Identity across the top, the grid as the dominant
   surface, the settings rail beside it — and both columns scrolling on their
   own so the rail stays put on a gallery of two hundred frames.

   No new token. The rail's inert rows use the same muted treatment the rest of
   the admin uses for "nothing here yet", so the difference between a setting
   that works and one that is coming is visible without a badge on each row.
   --------------------------------------------------------------------------- */

/* The page owns its own scrolling, so the two columns can scroll separately. */
.gws{overflow:hidden;gap:0}

.gws__hd{display:flex;align-items:flex-end;justify-content:space-between;
  gap:24px;flex-shrink:0;flex-wrap:wrap}
.gws__id{min-width:0}
.gws__hd .path{font-size:var(--fs-2xs);letter-spacing:.22em;text-transform:uppercase;
  color:var(--slate2);display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.gws__hd .path a{color:var(--slate2);transition:color 140ms var(--ease)}
.gws__hd .path a:hover{color:var(--ink)}
.gws__hd .path span{color:var(--line2)}
.gws__hd h1{margin-top:9px;display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
/* The place, beside the project rather than under it: together they are how a
   photographer says which job this is. */
.gws__place{font-family:var(--serif);font-size:.56em;color:var(--slate2)}
.gws__acts{display:flex;align-items:center;gap:8px;flex-wrap:wrap}

/* Structure for client delivery, which does not exist. Genuinely `disabled`
   rather than dressed to look it, so nothing can be clicked and assistive
   technology is told the same thing the eye is. */
/* Disabled, and still readable.
   ---------------------------------------------------------------------------
   This was `opacity:.38`, which is the lazy way to say unavailable: it fades
   the label, the icon and the border together until the control is a smudge,
   and on a dark ground it took Share and Publish below the point where anybody
   could tell what they said. A control that will exist soon should be legible
   now — an operator needs to see that Publish is coming, not squint at where
   it might be.

   So: no opacity at all. A quiet surface, a real border and a muted-but-solid
   label, in tokens that resolve on both themes — `--slate2` against `--panel2`
   is the same pairing the admin uses for every other piece of secondary text.

   `pointer-events:none` keeps it genuinely inert, the hover and active rules
   above exclude `[disabled]` so nothing lights up under the cursor, and the
   `disabled` attribute itself keeps it out of the tab order and announces the
   state. Nothing here enables anything. */
.hd__btn[disabled]{
  cursor:default;pointer-events:none;
  /* `opacity:1` is the whole point of this rule, not a leftover. The global
     `[disabled]{opacity:.45}` fades the label, the icon AND the border together,
     and it applies here too because this selector overrides colour without
     mentioning opacity. Opting out explicitly is what lets the muted colour
     below be the only thing saying "unavailable" — one signal, chosen, at a
     legible strength, instead of two stacked until the control disappears. */
  opacity:1;
  background:var(--panel2);background-image:none;
  /* The firmer of the two edge tokens. `--line2` left the pill shape barely
     drawn once the fill stopped doing the work, and the brief asks for a clear
     border in both themes — this is the same edge a hovered glass control
     uses, so it reads as a real pill rather than a ghost of one. */
  border:1px solid var(--glass-line-hover);
  color:var(--slate2);
  box-shadow:none}

.gws__meta{padding:16px 0 14px;margin-bottom:0;border-bottom:.5px solid var(--line);
  flex-shrink:0}

/* The rail's width.
   ---------------------------------------------------------------------------
   Was a flat 292px, which was too narrow for what the rail now carries: rows
   like "Submitted · 4 images selected" plus a View selection link were being
   ellipsed to "Submitted · 4 images select...", and a settings rail that hides
   the setting is not doing its job.

   The ceiling is measured rather than guessed. At a 1440 viewport the header's
   action row — Preview client view, Share, Publish — runs 1050→1412, and both
   it and the rail are flush to the same right edge, so 362px puts the rail's
   left edge under the P of "Preview client view". 364 is that, rounded to an
   even number.

   `clamp` rather than a fixed value so the grid still gets the room between
   the 1120px stacking breakpoint and a wide desktop: the rail gives width back
   as the window narrows, and the main column is `minmax(0,1fr)` either way. */
.gws__body{flex:1;min-height:0;display:grid;
  grid-template-columns:minmax(0,1fr) clamp(300px,25vw,364px);gap:20px;padding-top:16px}
.gws__main{min-width:0;min-height:0;display:flex;flex-direction:column;gap:12px}

.gws__bar{display:flex;align-items:center;justify-content:space-between;
  gap:14px;flex-wrap:wrap;flex-shrink:0}
.gws__counts{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
/* Compact: these are counts, not filters, so they read smaller than a control. */
.gws__counts .chip{padding:5px 11px;font-size:var(--fs-2xs)}
.gws__barcts{display:flex;align-items:center;gap:10px;flex-wrap:wrap}

/* The grid scrolls; the header, the counts and the rail do not. */
.gws__scroll{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding-right:4px}

/* Bigger frames than the drawer's: this is the working surface, and a
   photograph an operator is judging should not be a postage stamp. */
/* Packed columns, not banded rows.
   ---------------------------------------------------------------------------
   Ordinary grid rows are as tall as the tallest item in them, so one portrait
   left a band of dead space beside every landscape next to it. Instead each
   tile spans a whole number of very short implicit rows — its own height in
   2px units, measured by the script once the image reports its shape — and the
   row gap is folded into that span rather than applied between rows.

   Auto-placement then packs each tile into the first column it fits in at or
   after the cursor, which is masonry, and it does it **in DOM order**: the
   flow is left-to-right, top-to-bottom, and item five is still after item four.
   That is the whole reason for choosing this over CSS `columns`, which packs
   perfectly and flows top-to-bottom per column — unreadable in a grid whose
   primary interaction is dragging one image in front of another. */
.galgrid--ws{grid-template-columns:repeat(auto-fill,minmax(190px,1fr));
  gap:14px;margin-top:0}

/* The packed layout is switched on by the script, once it has measured. Without
   it the grid stays ordinary rows — banded and a little gappy, but correct and
   never overlapping, which a 4px row would be if nothing had set the spans. */
.galgrid--ws.is-packed{grid-auto-rows:2px;row-gap:0}
.galgrid--ws .galgrid__name{font-size:var(--fs-xs)}

/* Add photos. The file input carries its own label through the button beside
   it, so the pair reads as one control. */
.gws__add{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
/* The file input is hidden from sight, not from the keyboard: clipped rather
   than `display:none`, so it stays focusable and the label still activates it.
   The ring on the label is what a keyboard user sees. */
/* Add photos, at the Sort button's size.
   ---------------------------------------------------------------------------
   It is an `.hd__btn` — the header's 36px pill at 10/18 padding — sitting in a
   toolbar of 31px `.btn` pills, and next to Sort it read as oversized. These
   four lines bring the geometry and the type down to `.btn` exactly.

   What it does NOT take is `.btn`'s translucent fill: this is the one control
   on the bar that adds work to the job, and the solid ink pill is how it says
   so. Same size as its neighbours, still obviously the action. */
.gws__pick{position:relative;cursor:pointer;
  padding:8px 14px;gap:7px;font-size:var(--fs-xs);
  /* Add photos borrows `.hd__btn` for its solid fill, not for its shape. It
     stands in the gallery TOOLBAR beside Sort, so it takes the toolbar's pill
     rather than the header's squared corner — the same rule the rest of the
     admin now follows: header actions are squared, bar chips are pills. */
  border-radius:var(--pill-radius)}
.gws__pick svg{width:11px;height:11px}
/* Locked while a transfer is running. `pointer-events` rather than the disabled
   attribute, because a <label> cannot be disabled — and the label is the
   control here. */
.gws__pick.is-busy,.gws__send.is-busy{opacity:.5;pointer-events:none}
.gws__pick input[type="file"]{position:absolute;width:1px;height:1px;padding:0;margin:0;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
.gws__pick:focus-within{outline:2px solid var(--accent);outline-offset:2px;color:var(--ink)}
.gws__add--empty{justify-content:center;margin-top:18px}

.gws__empty{padding:52px 20px 44px}
.gws__emptyact{display:flex;justify-content:center}
.gws__emptylink{margin-top:16px}

/* --- the settings rail ---------------------------------------------------- */
/* A PANEL, so it takes the panel edge. It already shared `--surface-card` and
   `--elev-card` with the Job Register's card; only its border was still the
   0.5px row hairline, which on a translucent surface read as a seam rather
   than an edge. Padding, gap and every rail rhythm below are untouched. */
.gws__rail{min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:22px;
  background:var(--surface-card);border:1px solid var(--edge-card);
  border-radius:var(--card-radius);box-shadow:var(--elev-card);padding:18px 18px 22px}
.gws__grp{display:flex;flex-direction:column;gap:0}
/* The group heading sits on the same rhythm as the rows under it, so the space
   above the first setting matches the space between every pair after it. */
.gws__grp>.k{font-size:var(--fs-2xs);letter-spacing:.24em;text-transform:uppercase;
  color:var(--slate2);padding-bottom:var(--rail-row-pad);
  border-bottom:.5px solid var(--line);
  display:block;font-family:var(--body)}

/* One padding value, from the token, on every row in the rail — Status through
   Delivery. The compaction removed rows; this is where the room they freed
   went. A rail that is shorter AND tighter would read as squeezed, which is the
   opposite of the point: fewer things, further apart. */
.gws__row{display:flex;align-items:baseline;justify-content:space-between;
  gap:12px;padding:var(--rail-row-pad) 0;
  border-bottom:.5px solid var(--line);min-width:0}
.gws__row:last-child{border-bottom:0}
.gws__rk{font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.16em;
  text-transform:uppercase;color:var(--slate2);flex-shrink:0}
/* A one-line consequence under a rail row — Task 012H. Used where a setting
   changes what somebody OUTSIDE this screen sees, and the reassurance about
   what it does not change is worth the two lines. */
.gws__rnote{margin:2px 0 8px;font-size:var(--fs-xs);color:var(--slate2);line-height:1.5}
/* A rail value wraps rather than truncates.
   ---------------------------------------------------------------------------
   `nowrap` + ellipsis was the wrong default here. It suits a filename, which
   is one unbreakable token nobody reads in full; it does not suit a status
   like "Submitted · 4 images selected", where the ellipsis eats the number —
   the only part an operator was looking for. A wider rail makes that rare; a
   second line makes it impossible.

   The filename rows opt back in with `--trunc` below. */
.gws__rv{font-size:var(--fs-md);color:var(--ink);text-align:right;min-width:0;
  white-space:normal;overflow-wrap:anywhere}
/* One line, ellipsed: for a value that is a filename. */
.gws__rv--trunc{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  overflow-wrap:normal}
/* Not yet built. Dimmed as a group rather than badged row by row — the
   paragraph above them says why once. */
.gws__row.is-soon .gws__rv{color:var(--slate)}
.gws__row.is-soon{opacity:.62}

.gws__note{font-size:var(--fs-xs);line-height:1.6;color:var(--slate2);
  padding:8px 0 10px;margin:0}

/* Activity entries take the rail's rhythm too. They are not settings rows, but
   they are in the same column under the same dividers, and two spacings in one
   card reads as a mistake rather than as a distinction. */
.gws__act{padding:var(--rail-row-pad) 0;border-bottom:.5px solid var(--line)}
.gws__act:last-child{border-bottom:0}
.gws__actt{font-size:var(--fs-sm-plus);color:var(--ink);line-height:1.45}
.gws__actm{font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.14em;
  text-transform:uppercase;color:var(--slate);margin-top:4px}

/* The rail drops beneath the grid before it gets too narrow to read. */
@media (max-width:1120px){
  .gws{overflow-y:auto}
  .gws__body{grid-template-columns:minmax(0,1fr);min-height:0}
  .gws__scroll{overflow:visible;min-height:0}
  .gws__rail{overflow:visible}
}

/* ---------------------------------------------------------------------------
   Task 008B UX pass — uploading, dropping, viewing, selecting

   Four behaviours, all of them affordances over markup that already works. The
   upload is still one form post; the viewer is still a link to the existing
   preview route; selection is still nothing but a class on a tile.
   --------------------------------------------------------------------------- */

/* --- uploading ------------------------------------------------------------ */

/* `[hidden]` first, and it matters: an author `display` beats the user-agent
   rule the attribute relies on, so a bare `display:flex` here would show the
   band permanently. Every element in this block that the script toggles is
   guarded the same way. */
.gws__up[hidden],.gws__uplist[hidden],.gws__selbar[hidden],.gws__jsonly[hidden],.gws__send[hidden]{display:none}

/* The upload queue's panel — Task 010B. Grid rather than flex, because the
   failure list spans the full width beneath the bar and the text. */
.gws__up{display:grid;grid-template-columns:auto 1fr;align-items:center;
  gap:6px 14px;flex-shrink:0;
  padding:12px 15px;margin-bottom:12px;border-radius:var(--row-radius);
  background:var(--panel2);border:.5px solid var(--line2)}
.gws__upt{font-size:var(--fs-md);color:var(--ink);min-width:0}
.gws__upt b{font-weight:500}
.gws__ups{display:block;font-size:var(--fs-xs);color:var(--slate2);margin-top:3px}

/* Determinate now, and it has earned the right to be: each file is its own
   request, so the browser counts bytes on the wire and this is that count. It
   is never advanced by anything nobody measured — a file still waiting in the
   queue contributes nothing until it starts. */
.gws__upbar{position:relative;width:64px;height:3px;flex-shrink:0;overflow:hidden;
  border-radius:var(--pill-radius);background:var(--line2)}
.gws__upbar i{position:absolute;inset:0 auto 0 0;width:0;border-radius:var(--pill-radius);
  background:var(--accent);transition:width .2s var(--ease)}
@media (prefers-reduced-motion:reduce){
  .gws__upbar i{transition:none}
}

/* One row per failed file: what it was called, why it stopped, and the one
   button that does something about it. Nothing else in the batch is affected,
   which is the whole reason the queue exists. */
.gws__uplist{grid-column:1 / -1;list-style:none;margin:8px 0 0;padding:8px 0 0;
  display:flex;flex-direction:column;gap:6px;border-top:.5px solid var(--line2)}
.gws__uprow{display:flex;align-items:center;gap:10px;font-size:var(--fs-sm)}
.gws__upname{font-family:var(--body);font-weight:500;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:38%}
.gws__upwhy{flex:1;min-width:0;color:var(--slate2)}

/* --- dropping ------------------------------------------------------------- */

/* The note sits over the column and only appears once files are actually over
   it, so nothing on the page moves and the grid is never displaced. Positioned
   against `.gws__main` rather than the scrolling area, so it stays put over the
   visible column instead of scrolling away with the content. */
.gws__main{position:relative}
.gws__dropnote{position:absolute;inset:0;z-index:3;display:flex;
  align-items:center;justify-content:center;gap:10px;
  border-radius:var(--card-radius);border:1px dashed var(--ring-sel);
  /* Translucent, so the grid stays visible underneath. An opaque panel made
     the drop target unmistakable and also blanked the page, which is a poor
     trade when the operator is mid-gesture and wants to see where the images
     are going. */
  background:color-mix(in srgb,var(--accent) 12%,color-mix(in srgb,var(--panel2) 82%,transparent));
  -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);
  font-family:var(--body);font-size:var(--fs-md);font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--ink);
  opacity:0;pointer-events:none;transition:opacity 120ms var(--ease)}
.gws__dropnote svg{width:14px;height:14px}
.gws__main.is-dropping .gws__dropnote{opacity:1}

/* --- the viewer ----------------------------------------------------------- */

/* A neutral, near-black stage in both themes: a photograph is judged against
   something quiet, and cream would tint everything on it. */
/* No `display` on the base rule. `dialog:not([open]){display:none}` comes from
   the user agent at element specificity, so a `.lb{display:flex}` here outranks
   it and a CLOSED dialog renders as a full-page overlay on top of the gallery.
   It is laid out only when it is open. */
.lb{width:min(94vw,1500px);height:min(92vh,1000px);max-width:none;max-height:none;
  /* Restored explicitly: a dialog is centred by the user agent's `margin:auto`,
     and the global reset at the top of this file zeroes every margin — so
     without this the viewer sits hard against the top-left corner. */
  margin:auto;
  padding:0;border:0;border-radius:var(--card-radius);overflow:hidden;
  background:var(--band);color:var(--band-ink);
  flex-direction:column}
.lb::backdrop{background:rgba(0,0,0,.72)}
.lb[open]{display:flex;animation:lbIn 140ms var(--ease) both}
@keyframes lbIn{from{opacity:0;transform:scale(.99)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.lb[open]{animation:none}}

.lb__hd{display:flex;align-items:center;justify-content:space-between;gap:16px;
  padding:13px 16px;border-bottom:.5px solid var(--band-line);flex-shrink:0}
.lb__id{display:flex;align-items:baseline;gap:12px;min-width:0}
.lb__name{font-size:var(--fs-md-plus);color:var(--band-ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lb__pos{font-family:var(--body);font-size:var(--fs-micro);letter-spacing:.18em;
  text-transform:uppercase;color:var(--slate);flex-shrink:0}
.lb__x{display:inline-flex;align-items:center;justify-content:center;
  width:30px;height:30px;border-radius:var(--row-radius);flex-shrink:0;color:var(--band-ink);
  border:.5px solid var(--band-line);transition:background 140ms var(--ease)}
.lb__x svg{width:13px;height:13px}
.lb__x:hover{background:rgba(255,255,255,.08)}

.lb__stage{flex:1;min-height:0;position:relative;display:flex;
  align-items:center;justify-content:center;padding:18px}
/* The frame is the viewport; the photograph keeps its own shape inside it. */
.lb__img{max-width:100%;max-height:100%;width:auto;height:auto;
  object-fit:contain;display:block;border-radius:var(--chip-radius)}

.lb__nav{position:absolute;top:50%;transform:translateY(-50%);
  display:inline-flex;align-items:center;justify-content:center;
  width:42px;height:42px;border-radius:50%;color:var(--band-ink);
  background:rgba(0,0,0,.42);border:.5px solid var(--band-line);
  transition:background 140ms var(--ease)}
.lb__nav:hover{background:rgba(0,0,0,.66)}
.lb__nav svg{width:16px;height:16px}
.lb__nav--prev{left:14px;transform:translateY(-50%) rotate(90deg)}
.lb__nav--next{right:14px;transform:translateY(-50%) rotate(-90deg)}
.lb__x:focus-visible,.lb__nav:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* --- multi-select for bulk actions — Task 012G ----------------------------- */

/* There is no select MODE. The checkbox is on the tile, shown on hover or once
   anything is selected, and it is a real input rather than a decorated span —
   so it is keyboard reachable, announces its state, and needs no mode to enter.
   The old `body.is-selecting` rules survive only as the SELECTED look. */
.galgrid__check{position:absolute;left:8px;top:8px;z-index:2;
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  opacity:0;transition:opacity 140ms var(--ease)}
.galgrid__i:hover .galgrid__check,
.galgrid__check:focus-within,
body.is-selecting .galgrid__check{opacity:1}
.galgrid__check input{position:absolute;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
.galgrid__tick{display:flex;align-items:center;justify-content:center;
  width:19px;height:19px;border-radius:var(--chip-radius);
  background:var(--glass);border:1px solid var(--glass-line-hover);
  -webkit-backdrop-filter:var(--glass-blur);backdrop-filter:var(--glass-blur)}
.galgrid__tick svg{width:11px;height:11px;stroke:transparent;fill:none}
.galgrid__check input:checked + .galgrid__tick{background:var(--accent);border-color:transparent}
.galgrid__check input:checked + .galgrid__tick svg{stroke:var(--accent-ink)}
.galgrid__check input:focus-visible + .galgrid__tick{outline:2px solid var(--accent);outline-offset:2px}

.galgrid__i.is-selected .galgrid__fr{border-color:var(--ring-sel);box-shadow:var(--halo-sel)}
/* Hero, Hide and Remove are real writes and must stay reachable while
   something is selected. */
body.is-selecting .galgrid__acts{opacity:1;transform:none}
/* The per-image overflow — Task 012H. Hero stays on the tile; Hide and Remove
   moved in here, because three buttons on every frame of a two-hundred image
   gallery is six hundred controls competing with the photographs. */
.galgrid__menu{position:relative;display:inline-flex}
.galgrid__menu>summary{list-style:none;cursor:pointer}
.galgrid__menu>summary::-webkit-details-marker{display:none}
.galgrid__menuitems{position:absolute;right:0;bottom:calc(100% + 6px);z-index:30;
  display:flex;flex-direction:column;min-width:190px;padding:5px;
  background:var(--panel);border:.5px solid var(--line2);border-radius:var(--row-radius);
  box-shadow:var(--shadow)}
.galgrid__menuitem{display:flex;align-items:center;gap:9px;padding:8px 10px;
  border-radius:var(--chip-radius);text-align:left;white-space:nowrap;
  font-family:var(--body);font-size:var(--fs-sm);color:var(--ink);
  transition:background 140ms var(--ease)}
.galgrid__menuitem svg{width:13px;height:13px;stroke:currentColor;fill:none;flex-shrink:0}
.galgrid__menuitem:hover{background:var(--panel-hi)}
.galgrid__menuitem:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.galgrid__menuitem--warn:hover{color:var(--warn)}
/* An open menu keeps its tile's actions up even when the pointer has moved off
   the picture to reach them. */
.galgrid__i:has(.galgrid__menu[open]) .galgrid__acts{opacity:1;transform:none}

.gws__selbar{display:flex;align-items:center;justify-content:space-between;
  gap:14px;flex-wrap:wrap;flex-shrink:0;margin-top:12px;
  padding:11px 15px;border-radius:var(--row-radius);
  background:var(--panel2);border:.5px solid var(--line2)}
.gws__selrm:hover{color:var(--warn);border-color:var(--warn)}

/* --- gallery sets, admin -------------------------------------------------- */

/* The Sets panel. A band above the grid rather than a rail row, because naming,
   ordering and filing are work an operator does WHILE looking at the
   photographs — and because rename and description need room to type in.

   Every control is a real form post; the Edit and Add panels are `<details>`,
   so the whole surface works with scripting off. */
.gsets{flex-shrink:0;margin:0 0 14px;padding:14px 16px 12px;
  border-radius:var(--row-radius);
  background:var(--panel2);border:.5px solid var(--line2)}
.gsets__h{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 16px}
.gsets__n{margin:0;max-width:74ch;font-size:var(--fs-sm);line-height:1.55;
  color:var(--slate2)}

.gsets__l{list-style:none;margin:12px 0 0;padding:0;
  display:flex;flex-direction:column;gap:2px}
.gsets__i{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 18px;
  justify-content:space-between;
  padding:9px 10px;border-radius:var(--row-radius)}
.gsets__i:hover{background:var(--panel)}
/* A set the client is not shown. Quietened rather than struck through: it is
   still a real set holding real photographs, and an operator has to be able to
   read what is in it. */
.gsets__i.is-off{opacity:.62}
/* Three things on the row, in a fixed reading order however it wraps: what the
   set is called and how big it is; what an operator can do to it; and then, on
   its own line, the sentence the client will read. The description is the one
   that must never share a line — it is a sentence, and a sentence squeezed
   between a count and a row of controls reads as neither. */
.gsets__id{display:flex;flex-wrap:wrap;align-items:baseline;gap:10px;min-width:0;order:1}
.gsets__k{font-size:var(--fs-base);color:var(--ink)}
/* The CLIENT's count — the same figure their tab carries. */
.gsets__c{font-family:var(--serif);font-size:var(--fs-lg);color:var(--ink)}
/* Images in the set the client is NOT shown, said separately rather than
   folded into the count, so neither number is ambiguous. */
.gsets__w,.gsets__d{font-size:var(--fs-micro);letter-spacing:.16em;
  text-transform:uppercase;color:var(--slate2)}
.gsets__d--off{color:var(--warn)}
/* `min-width` rather than `max-width` for the measure, and that distinction is
   the whole reason this line breaks: a 70ch cap let a short description fit
   beside the controls, so one set read as a paragraph on its own line and the
   next read as a caption tacked onto a button. The box is the full row; the
   reading measure comes from padding instead. */
.gsets__desc{flex:0 0 100%;min-width:100%;order:3;margin:4px 0 0;
  padding-right:min(30%,320px);
  font-size:var(--fs-sm);line-height:1.5;color:var(--slate2)}
.gsets__acts{order:2;display:flex;align-items:center;gap:14px;flex-wrap:nowrap;
  margin-left:auto;flex-shrink:0}
.gsets__acts form{display:flex;align-items:center;gap:6px;flex-shrink:0}

.gsets__ed{position:relative}
.gsets__ed>summary{list-style:none;cursor:pointer}
.gsets__ed>summary::-webkit-details-marker{display:none}
.gsets__form{position:absolute;right:0;top:calc(100% + 6px);z-index:20;
  display:flex;flex-direction:column;gap:10px;
  width:min(330px,80vw);padding:13px;
  background:var(--panel);border:.5px solid var(--line2);
  border-radius:var(--row-radius);box-shadow:var(--shadow)}
.gsets__form form{display:flex;flex-direction:column;gap:6px}
.gsets__lb{font-size:var(--fs-micro);letter-spacing:.16em;text-transform:uppercase;
  color:var(--slate2)}
.gsets__opt{letter-spacing:.16em;color:var(--line2)}
.gsets__in{width:100%;padding:9px 11px;border-radius:var(--row-radius);
  border:.5px solid var(--line2);background:var(--bg);
  font-family:var(--body);font-size:var(--fs-sm);color:var(--ink)}
.gsets__in:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.gsets__go{display:flex;justify-content:flex-end;margin-top:4px}
/* Why a delete is refused, drawn where Delete would have been. Hiding the
   control would teach an operator that sets cannot be deleted at all. */
.gsets__no{margin:0;font-size:var(--fs-sm);line-height:1.5;color:var(--slate2)}
.gsets__rm:hover{color:var(--warn);border-color:var(--warn)}

/* The Add panel opens leftward at the end of the band, where its summary sits. */
.gsets__add{display:flex;align-items:center;gap:14px;margin-top:12px}
.gsets__add .gsets__form{left:0;right:auto}

/* Move to set, in the bulk bar. */
.gws__selset{display:inline-flex;align-items:center}
.gws__selsel{width:auto;min-width:150px;cursor:pointer}

/* SORT, replacing Clear order. A `<details>` holding four submits, so the menu
   works with scripting off. */
.gws__sort{position:relative}
.gws__sort>summary{list-style:none;cursor:pointer}
.gws__sort>summary::-webkit-details-marker{display:none}
.gws__sortmenu{position:absolute;right:0;top:calc(100% + 6px);z-index:20;
  display:flex;flex-direction:column;min-width:230px;padding:6px;
  background:var(--panel);border:.5px solid var(--line2);border-radius:var(--row-radius);
  box-shadow:var(--shadow)}
.gws__sortopt{padding:9px 11px;border-radius:var(--chip-radius);text-align:left;
  font-family:var(--body);font-size:var(--fs-sm);color:var(--ink);
  transition:background 140ms var(--ease)}
.gws__sortopt:hover{background:var(--panel-hi)}
.gws__sortopt:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.gws__sortnote{margin:4px 0 2px;padding:0 11px;
  font-size:var(--fs-2xs);color:var(--slate2);line-height:1.5}

/* Removed from this gallery — the proof that removal is not deletion. */
.gws__gone{flex-shrink:0;margin-top:12px}
.gws__gone>summary{list-style:none;cursor:pointer}
.gws__gone>summary::-webkit-details-marker{display:none}
.gws__gones{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding:10px 0;border-top:.5px solid var(--line);
  font-size:var(--fs-sm);color:var(--slate2);transition:color 140ms var(--ease)}
.gws__gones:hover{color:var(--ink)}
.gws__gonec{font-size:var(--fs-xs);color:var(--slate);font-variant-numeric:tabular-nums}
.gws__gonen{margin:0 0 8px;font-size:var(--fs-xs);color:var(--slate2);line-height:1.5}
.gws__gonelist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}
.gws__goneitem{display:flex;align-items:center;gap:11px;padding:6px 0}
.gws__gonethumb{width:38px;height:38px;flex-shrink:0;border-radius:var(--chip-radius);
  object-fit:cover;background:var(--panel2)}
.gws__gonethumb--none{display:flex;align-items:center;justify-content:center}
.gws__gonethumb--none svg{width:14px;height:14px;stroke:var(--slate);fill:none}
.gws__gonename{flex:1;min-width:0;font-size:var(--fs-sm);color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gws__selct{font-family:var(--body);font-size:var(--fs-2xs);letter-spacing:.16em;
  text-transform:uppercase;color:var(--slate2)}
.gws__selct b{font-family:var(--body);font-size:var(--fs-lg);letter-spacing:0;
  font-weight:500;color:var(--ink)}
.gws__selacts{display:flex;align-items:center;gap:12px;flex-wrap:wrap}

/* ---------------------------------------------------------------------------
   Task 008B — grid density

   How many photographs an operator wants on screen at once while reviewing a
   delivery. `auto-fill` with a minimum does the work: the requested density is
   a preferred column WIDTH, so a narrow viewport quietly drops to fewer columns
   rather than crushing the tiles, and nothing ever goes below a usable size.

   At the workspace's grid width these land on roughly 4 / 6 / 8 columns.
   --------------------------------------------------------------------------- */
.gws__dens{display:flex;align-items:center;gap:4px;padding:3px;border-radius:var(--pill-radius);
  background:var(--panel2);border:.5px solid var(--line)}
.gws__dens .chip{padding:4px 10px;font-size:var(--fs-micro);letter-spacing:.14em;
  border:0;background:none}
.gws__dens .chip.on{background:var(--ink);color:var(--bg)}
.gws__dens[hidden]{display:none}

/* Each density asks for an exact column count, and gives it up rather than
   crush the tiles.
   ---------------------------------------------------------------------------
   The minimum track is `max(floor, one Nth of the row)`. While an Nth is wider
   than the floor, the minimum IS an Nth and `auto-fill` packs exactly N. Once
   the viewport narrows enough that an Nth would be smaller than the floor, the
   floor takes over and auto-fill packs fewer columns instead.
   
   So the count is 4 / 6 / 8 on a desktop and steps down on its own on a laptop
   or a split screen, with no breakpoint to maintain and no tile ever squeezed
   below a size an operator can judge a photograph at. The `- 1px` absorbs
   sub-pixel rounding, which otherwise drops the last column. */
.galgrid--ws[data-density="large"]{
  grid-template-columns:repeat(auto-fill,minmax(max(200px,calc((100% - 3 * 14px) / 4 - 1px)),1fr))}
.galgrid--ws[data-density="medium"]{
  grid-template-columns:repeat(auto-fill,minmax(max(138px,calc((100% - 5 * 14px) / 6 - 1px)),1fr))}
.galgrid--ws[data-density="small"]{
  grid-template-columns:repeat(auto-fill,minmax(max(104px,calc((100% - 7 * 14px) / 8 - 1px)),1fr))}

/* The caption has less room to work with as the columns narrow — but a
   FILENAME IS CONTENT, not a label, and the alignment pass put a floor under
   content. It steps down one notch rather than two; the Gallery's grid,
   spacing and density behaviour are untouched. */
.galgrid--ws[data-density="small"] .galgrid__name{font-size:var(--fs-2xs)}
.galgrid--ws[data-density="small"] .galgrid__act{width:22px;height:22px}
.galgrid--ws[data-density="small"] .galgrid__act svg{width:10px;height:10px}

/* --- Gallery publication rows (Task 011, admin rail) ----------------------- */

/* The gallery address: present, selectable, not shown.
   ---------------------------------------------------------------------------
   The rail used to print the whole `/g/<token>` URL in mono over two wrapped
   lines, and the row stacked to give it the width. Nobody reads a token; they
   press Copy. So the row is an ordinary one-line row again — GALLERY URL on the
   left, Copy on the right — and the address is clipped out of sight.

   Clipped, NOT `display:none`. `admin.js` selects this node so ⌘C works where
   the Clipboard API is unavailable (a plain-http deployment), and a range
   cannot be laid over a box that was never generated. One pixel, hidden
   overflow and no wrapping keeps it out of the layout while leaving it a real
   box with real text in it. No `clip-path` and no `display:none` for exactly
   that reason — the point of this rule is that the text stays selectable. */
.gws__url{position:absolute;width:1px;height:1px;overflow:hidden;
  white-space:nowrap;pointer-events:none}
.gws__inline{display:flex;align-items:baseline;gap:10px;justify-content:flex-end}
.gws__pubform{display:contents}

/* --- share, recipients and delivery (Task 012) ---------------------------- */

/* SHARE. A `<details>` in the header, like the client gallery's download menu
   and for the same reason: it opens, closes and takes the keyboard with no
   script at all. Only rendered when the gallery is published — an unpublished
   one has no client link to share. */
.hd__share{position:relative}
.hd__share>summary{list-style:none;cursor:pointer}
.hd__share>summary::-webkit-details-marker{display:none}
.hd__sharemenu{position:absolute;right:0;top:calc(100% + 7px);z-index:20;
  width:min(272px,74vw);padding:6px;
  background:var(--panel);border:.5px solid var(--line2);
  border-radius:var(--row-radius);box-shadow:var(--shadow);
  display:flex;flex-direction:column;gap:2px}
.hd__shareopt{display:flex;align-items:flex-start;gap:10px;width:100%;
  padding:9px 11px;border-radius:var(--chip-radius);border:0;background:none;cursor:pointer;
  text-align:left;color:var(--ink);
  transition:background 120ms var(--ease)}
.hd__shareopt:hover{background:var(--panel2)}
.hd__shareopt svg{flex:none;width:14px;height:14px;margin-top:2px;
  stroke:currentColor;fill:none}
.hd__shareopt>span{display:flex;flex-direction:column;gap:2px;min-width:0}
.hd__sharek{font-family:var(--body);font-size:var(--fs-sm);font-weight:500;
  letter-spacing:.14em;text-transform:uppercase}
.hd__shared{font-family:var(--body);font-size:var(--fs-xs);color:var(--slate2);
  letter-spacing:.01em}

/* A rail row whose value needs the full width — the recipient list. The default
   row is a right-aligned single line, which a list of people is not. */
.gws__row--stack{flex-direction:column;align-items:stretch;gap:8px}
.gws__rv--wide{text-align:left;width:100%;overflow:visible;white-space:normal;
  display:flex;flex-direction:column;gap:8px}
.gws__none{font-size:var(--fs-sm);color:var(--slate)}

/* ONE RECIPIENT. Compact by default: two lines of identity, a state chip, and
   the actions underneath. Everything else — the reason it failed, the edit
   fields — appears only when there is something to show. */
.gwr{display:flex;flex-direction:column;gap:5px;padding:9px 10px;
  background:var(--panel2);border-radius:var(--chip-radius)}
.gwr__top{display:flex;align-items:baseline;gap:8px;justify-content:space-between}
.gwr__who{display:flex;flex-direction:column;gap:1px;min-width:0}
.gwr__n{font-size:var(--fs-sm);color:var(--ink);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.gwr__e{font-size:var(--fs-xs);color:var(--slate2);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}

/* The state chip. Colour follows the words rather than replacing them — "Failed"
   is legible without seeing the colour, which is the whole requirement. */
.gwr__s{flex:none;font-size:var(--fs-2xs);letter-spacing:.16em;
  text-transform:uppercase;white-space:nowrap}
.gwr__s--wait{color:var(--slate)}
.gwr__s--ok{color:var(--ok)}
.gwr__s--bad{color:var(--hot)}
.gwr__why{font-size:var(--fs-xs);color:var(--hot);line-height:1.5}

.gwr__acts{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px}
.gwr__edit>summary{list-style:none;cursor:pointer}
.gwr__edit>summary::-webkit-details-marker{display:none}

/* The add and edit forms. Stacked, because the rail is narrow and two inputs
   side by side at 290px are two inputs nobody can read. */
.gwr__form{display:flex;flex-direction:column;align-items:flex-start;gap:6px;
  width:100%;padding-top:6px}
.gwr__form--add{padding-top:2px}
.gwr__f{width:100%;padding:6px 9px;border-radius:var(--chip-radius);
  border:.5px solid var(--line2);background:var(--panel);color:var(--ink);
  font-family:var(--body);font-size:var(--fs-sm)}
.gwr__f::placeholder{color:var(--slate)}
.gwr__f:focus-visible{outline:2px solid var(--ink);outline-offset:1px}

/* Contacts this Client already has. Never invented — an empty offer list is the
   honest answer for a Client with no usable contact email. */
.gws__offers{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 10px;
  padding-top:2px}
.gws__offert{font-size:var(--fs-2xs);letter-spacing:.16em;text-transform:uppercase;
  color:var(--slate)}
.gws__offer{display:inline}

.gws__ok{color:var(--ok)}

/* --- the delivery panel (Task 012C) --------------------------------------- */

/* Moved out of the settings rail and given the main column, because it is a
   workflow rather than a setting: recipients, the email, the terms, the PIN,
   then preview and send — read top to bottom in the order the work happens.
   One form drives all of it; the fields attach to it by `form=` so the layout
   can interleave the template pickers, which cannot be nested inside it. */
/* Its own page since Task 012C. It sat on the Gallery workspace briefly and
   took most of it, pushing a shoot's photographs into a strip along the top —
   the workspace exists for the images, and composing a delivery is a different
   job done once at the end. */
.gwdp{display:block}
.gwd{margin:clamp(16px,2vw,26px) 0 0;padding:clamp(20px,2.2vw,30px);
  /* Left-aligned and bounded rather than full-bleed: a send workflow reads as a
     column of decisions, and one stretched across a 27in display stops being
     one. `max-width` only ever narrows, so every viewport below it — including
     every phone — is unaffected. */
  max-width:960px;
  background:var(--panel);border:.5px solid var(--line);border-radius:var(--row-radius);
  display:flex;flex-direction:column;gap:clamp(16px,1.8vw,22px)}
.gwd__hd{display:flex;align-items:baseline;justify-content:space-between;gap:14px;
  padding-bottom:14px;border-bottom:.5px solid var(--line)}
.gwd__hd h2{margin:0;font-family:var(--body);font-size:var(--fs-md);font-weight:500;
  letter-spacing:.2em;text-transform:uppercase;color:var(--ink)}
.gwd__state{font-size:var(--fs-sm);letter-spacing:.16em;text-transform:uppercase;
  color:var(--slate2);white-space:nowrap}
.gwd__state.is-done{color:var(--ok)}
.gwd__form{display:contents}

/* The sections fill the panel, which is what actually carries the width. They
   used to carry a 640px cap of their own, so widening the panel would have added
   whitespace on the right rather than room for the fields. The measure now lives
   in one place. */
.gwd__sec{display:flex;flex-direction:column;gap:9px}
.gwd__lbl{font-size:var(--fs-2xs);letter-spacing:.2em;text-transform:uppercase;
  color:var(--slate)}
.gwd__none{margin:0;font-size:var(--fs-sm);color:var(--slate2);line-height:1.55}
.gwd__hint{margin:0;font-size:var(--fs-xs);color:var(--slate2);line-height:1.55}

/* Prefilled from a Client default, said out loud — an operator should not have
   to wonder where wording they did not type came from. */
.gwd__from{margin:0;font-size:var(--fs-xs);color:var(--slate2);line-height:1.55;
  padding-left:9px;border-left:2px solid var(--line2)}

/* The Client's own contacts, as a checkbox list. The primary starts ticked. */
.gwd__pick{display:flex;flex-direction:column;align-items:flex-start;gap:7px}
.gwd__opt{display:flex;align-items:baseline;gap:9px;cursor:pointer;
  font-size:var(--fs-sm);color:var(--ink);flex-wrap:wrap}
.gwd__opt input{accent-color:var(--ink)}
.gwd__optn{color:var(--ink)}
.gwd__opte{color:var(--slate2);font-size:var(--fs-xs)}
.gwd__optp{font-size:var(--fs-2xs);letter-spacing:.16em;text-transform:uppercase;
  color:var(--slate)}
.gwd__more>summary,.gwd__manage>summary{list-style:none;cursor:pointer}
.gwd__more>summary::-webkit-details-marker,
.gwd__manage>summary::-webkit-details-marker{display:none}

.gwd__tpl{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.gwd__sel{flex:1;min-width:180px;max-width:320px;padding:7px 9px;border-radius:var(--chip-radius);
  border:.5px solid var(--line2);background:var(--panel2);color:var(--ink);
  font-family:var(--body);font-size:var(--fs-sm)}
.gwd__area{width:100%;padding:10px 12px;border-radius:var(--chip-radius);
  border:.5px solid var(--line2);background:var(--panel2);color:var(--ink);
  font-family:var(--body);font-size:var(--fs-base);line-height:1.55;resize:vertical}
.gwd__area:focus-visible,.gwd__sel:focus-visible{outline:2px solid var(--ink);outline-offset:1px}

.gwd__manage{padding-top:2px}
.gwd__row{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding:7px 0;border-top:.5px solid var(--line)}
.gwd__row.is-off{opacity:.55}
.gwd__rown{display:flex;flex-direction:column;gap:2px;min-width:0}
.gwd__rown>span:first-child{font-size:var(--fs-sm);color:var(--ink)}
.gwd__rowx{font-size:var(--fs-xs);color:var(--slate2);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}

/* Recipient shortcuts — Task 012D. The whole client network, and each saved
   contact group, as one press each. A chip rather than a primary button: these
   ADD people to the list, and the only button on this page that sends anything
   is the one at the bottom. */
.gwd__grps{display:flex;flex-direction:column;align-items:flex-start;gap:7px;
  padding-top:4px}
/* The shortcut forms are flex ITEMS of a column section, so without this they
   stretch to its full width and the chip inside them is dragged to the right —
   `.gws__inline` justifies to the end, which is correct for a row of actions
   beside a recipient and wrong for a button that starts a line. */
.gwd__sec>.gws__inline,.gwd__grps>.gws__inline{align-self:flex-start;justify-content:flex-start}
.gwd__add{align-self:flex-start;display:inline-flex;align-items:baseline;gap:8px;
  padding:6px 12px;border-radius:var(--pill-radius);border:.5px solid var(--line2);
  background:var(--panel2);color:var(--ink);cursor:pointer;
  font-family:var(--body);font-size:var(--fs-sm);text-align:left}
.gwd__add:hover:not(:disabled){border-color:var(--ink)}
.gwd__add:focus-visible{outline:2px solid var(--ink);outline-offset:1px}
/* Exhausted, or a group with nobody eligible left in it. It stays visible and
   inert rather than disappearing — a button that vanished the moment it was
   used would leave an operator wondering whether they pressed it. */
.gwd__add:disabled{opacity:.45;cursor:default}
.gwd__addn{font-size:var(--fs-2xs);letter-spacing:.12em;color:var(--slate)}
/* `+ Add recipient` is an anchor rather than a submit — it opens the picker, it
   does not write anything — and wears the same chip so the three ways to fill
   the list read as three of one kind. */
a.gwd__add{text-decoration:none}
a.gwd__add:hover{border-color:var(--ink)}

/* The picker itself: a search, a short result list, and a way to type somebody
   ImageBooth does not hold. */
.gwd__picker{display:flex;flex-direction:column;align-items:flex-start;gap:12px;
  padding:12px;background:var(--panel2);border-radius:var(--row-radius);width:100%}
.gwd__psearch{display:flex;align-items:stretch;gap:8px;width:100%}
.gwd__psearch .fld{flex:1 1 auto}
.gwd__psearch .btn{flex-shrink:0}
.gwd__optr{font-size:var(--fs-xs);color:var(--slate)}

/* The preloaded primary contact: the person and the Add on one line, with the
   action at the far end where Edit sits on a contact row. */
.gwd__primary{display:flex;align-items:baseline;gap:14px;width:100%}
.gwd__primaryadd{margin-left:auto;flex-shrink:0}

.gwd__go{display:flex;align-items:center;gap:14px;flex-wrap:wrap;
  padding-top:6px;border-top:.5px solid var(--line)}

/* ---------------------------------------------------------------------------
   COMPACT CONTACT ROWS — Task 012E.

   A contact is three facts — name, role, email — and one control. The row used
   to carry four controls, two of them destructive, which on an eighteen-contact
   client is a wall of buttons an operator has to read past to find a phone
   number. Everything but Edit now lives inside the row you open.
   --------------------------------------------------------------------------- */
.cro__hd{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  padding:16px 0 8px}
.cro__count{font-size:var(--fs-xs);color:var(--slate2);font-variant-numeric:tabular-nums}
/* Task 012F. The way into the individual contacts, and the way back out. One
   line, quiet, and the only thing standing between an operator and thirty rows
   of address book. */
/* The disclosure row. It carries its own count — "View 30 other contacts" —
   because that number is what an operator is deciding on, and a bare "View
   contacts" made the smallest thing on screen the only pressable one. Shaped
   like a group row so the two read as the same kind of control. */
.crodisc{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding:11px 0;border-bottom:.5px solid var(--line);
  font-size:var(--fs-base);color:var(--slate2);transition:color 140ms var(--ease)}
.crodisc:hover{color:var(--ink)}
.crodisc:focus-visible{outline:2px solid var(--accent);outline-offset:2px;color:var(--ink)}
.crodisc__c{color:var(--slate);flex-shrink:0}
.cro__none{margin:0;padding:9px 0;font-size:var(--fs-sm);color:var(--slate2);line-height:1.55}
.cro{display:flex;align-items:center;justify-content:space-between;gap:14px;
  padding:9px 0;border-bottom:.5px solid var(--line)}
.cro.is-open{border-bottom:0}
.cro.is-archived{opacity:.6}
.cro__id{display:flex;flex-direction:column;gap:1px;min-width:0}
.cro__n{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;
  font-size:var(--fs-base);color:var(--ink)}
.cro__r{font-size:var(--fs-xs);color:var(--slate2)}
.cro__e{font-size:var(--fs-sm);color:var(--slate2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cro__tag{font-size:var(--fs-2xs);letter-spacing:.12em;text-transform:uppercase;
  color:var(--slate);border:.5px solid var(--line2);border-radius:var(--pill-radius);padding:2px 7px}
/* The exception marker — a contact who cannot receive galleries. Warned rather
   than neutral, because it is the one row state that changes what a delivery
   does. */
.cro__tag--off{color:var(--warn);border-color:var(--warn)}

/* Restrained until the row is hovered or the link is focused — the same
   treatment the Files row action has carried since Task 004.5. The action is
   always reachable by keyboard; it is simply not shouting from eighteen rows
   at once. */
.cro__act{flex-shrink:0;font-family:var(--body);font-size:var(--fs-sm);
  color:var(--slate);transition:color 140ms var(--ease)}
.cro:hover .cro__act{color:var(--ink)}
.cro__act:focus-visible{outline:2px solid var(--accent);outline-offset:2px;color:var(--ink)}

/* The opened row. Fields first, then the state changes, then Archive — which
   is the only place Archive appears on this tab. */
.croe{padding:4px 0 16px;border-bottom:.5px solid var(--line);
  display:flex;flex-direction:column;gap:12px}
.croe__f{display:flex;flex-direction:column;gap:10px;padding:12px;
  background:var(--panel2);border-radius:var(--row-radius)}
.croe__acts{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.croe__more{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.croe__note{font-size:var(--fs-xs);color:var(--slate2);line-height:1.5;max-width:44ch}
/* Pushed to the far end, away from the actions somebody actually came for. */
.croe__arch{margin-left:auto}

/* A folded section: archived contacts, the add form, delivery defaults. One
   line at rest, opens on click, no scripting. */
.cfold{margin:14px 0}
.cfold>summary{list-style:none;cursor:pointer}
.cfold>summary::-webkit-details-marker{display:none}
.cfold__s{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding:9px 0;border-bottom:.5px solid var(--line);
  font-size:var(--fs-sm);color:var(--slate2);transition:color 140ms var(--ease)}
.cfold__s:hover{color:var(--ink)}
.cfold>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.cfold__c{font-size:var(--fs-xs);color:var(--slate);font-variant-numeric:tabular-nums}

/* ---------------------------------------------------------------------------
   THE RATE CARD SELECTOR — Task 012E.

   The resting state is the answer, not the library. Change opens a search;
   the list that follows is bounded and says what it is not showing.
   --------------------------------------------------------------------------- */
.rcs{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;
  padding:14px 0;border-bottom:.5px solid var(--line)}
.rcs__k{font-size:var(--fs-2xs);letter-spacing:.12em;text-transform:uppercase;
  color:var(--slate2);width:100%}
.rcs__v{font-size:var(--fs-lg-plus);color:var(--ink)}
.rcs__s{font-size:var(--fs-xs);color:var(--slate2)}
.rcs__go{margin-left:auto;flex-shrink:0}
.rcs__search{display:flex;align-items:stretch;gap:8px;margin:14px 0}
.rcs__search .fld{flex:1 1 auto}
.rcs__search .btn{flex-shrink:0}
/* One control rather than a column of radios: it names the current answer and
   opens the alternatives on demand, which is what a select is for and what a
   thirty-card library needs. */
.rcs__pick{display:flex;flex-direction:column;gap:5px;width:100%;max-width:420px}
.rcs__sel{padding:9px 11px;border-radius:var(--chip-radius);border:.5px solid var(--line2);
  background:var(--panel2);color:var(--ink);
  font-family:var(--body);font-size:var(--fs-base)}
.rcs__sel:focus-visible{outline:2px solid var(--ink);outline-offset:1px}

/* Contact groups, on the Client drawer's Contacts tab — Task 012D. Compact and
   under the contacts they reference: this is a grouping of the list above, not
   a second contact store. */
.ccg{display:flex;flex-direction:column;align-items:flex-start;gap:9px;
  margin:14px 0;padding:14px;background:var(--panel2);border-radius:var(--row-radius);width:100%}
.ccg__t{font-size:var(--fs-2xs);letter-spacing:.2em;text-transform:uppercase;color:var(--slate)}
.ccg__n{margin:0;font-size:var(--fs-xs);color:var(--slate2);line-height:1.5}
/* Collapsed at rest — Task 012E. One line per group: name, how many contacts,
   a disclosure arrow. Four groups on a Belle Property client are four lines
   rather than four open checkbox lists of eighteen people each. */
.ccg__g{width:100%;border-top:.5px solid var(--line)}
.ccg__g.is-off{opacity:.55}
.ccg__g>summary{list-style:none;cursor:pointer}
.ccg__g>summary::-webkit-details-marker{display:none}
.ccg__g>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.ccg__s{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding:10px 0;transition:color 140ms var(--ease)}
.ccg__s::after{content:"›";color:var(--slate);flex-shrink:0}
/* `.ccg__s` IS the summary, not a child of it — a descendant selector here
   matches nothing and the caret never flips. */
.ccg__g[open]>summary.ccg__s::after{content:"⌄"}
.ccg__nm{font-size:var(--fs-base);color:var(--ink)}
.ccg__c{margin-left:auto;font-size:var(--fs-xs);color:var(--slate2)}
.ccg__acts{display:flex;flex-direction:column;align-items:flex-start;gap:10px;
  padding:0 0 12px}
.ccg__arch{margin-top:2px}

/* Creating a group is an action at rest and a form when asked for. The input
   used to sit open under the list on every visit. */
.ccg__new{width:100%;border-top:.5px solid var(--line)}
.ccg__new>summary{list-style:none;cursor:pointer}
.ccg__new>summary::-webkit-details-marker{display:none}
.ccg__new>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.ccg__news{display:block;padding:11px 0;font-size:var(--fs-sm);color:var(--slate2);
  transition:color 140ms var(--ease)}
.ccg__news:hover{color:var(--ink)}
.ccg__new[open]>summary .ccg__news,.ccg__new[open]>summary.ccg__news{color:var(--ink)}
.ccg__new .inline-form{padding:0 0 12px}
.ccg__f{padding:6px 9px;border-radius:var(--chip-radius);border:.5px solid var(--line2);
  background:var(--panel);color:var(--ink);font-family:var(--body);font-size:var(--fs-sm);
  min-width:180px}
.ccg__f:focus-visible{outline:2px solid var(--ink);outline-offset:1px}
.ccg__mem{display:flex;flex-direction:column;align-items:flex-start;gap:6px;padding-top:7px}
.ccg__opt{display:flex;align-items:baseline;gap:8px;cursor:pointer;flex-wrap:wrap;
  font-size:var(--fs-sm);color:var(--ink)}
.ccg__opt input{accent-color:var(--ink)}
.ccg__optn{color:var(--ink)}
.ccg__opte{font-size:var(--fs-xs);color:var(--slate2)}
/* In the group, but not somebody a gallery would be sent to today. Named rather
   than silently dropped, so the counts above make sense. */
.ccg__optw{font-size:var(--fs-2xs);letter-spacing:.14em;text-transform:uppercase;color:var(--slate)}

/* Client delivery defaults, on the Client drawer's Contacts tab. */
.cdd{display:flex;flex-direction:column;align-items:flex-start;gap:9px;
  margin:14px 0;padding:14px;background:var(--panel2);border-radius:var(--row-radius)}
.cdd__t{font-size:var(--fs-2xs);letter-spacing:.2em;text-transform:uppercase;color:var(--slate)}
.cdd__f{display:flex;flex-direction:column;gap:5px;width:100%;max-width:360px}
.cdd__l{font-size:var(--fs-xs);color:var(--slate2)}
.cdd__s{padding:7px 9px;border-radius:var(--chip-radius);border:.5px solid var(--line2);
  background:var(--panel);color:var(--ink);font-family:var(--body);font-size:var(--fs-sm)}
.cdd__s:focus-visible{outline:2px solid var(--ink);outline-offset:1px}
.cdd__n{margin:0;font-size:var(--fs-xs);color:var(--slate2);line-height:1.5}

/* The delivery form and the email wording, Task 012B. Stacked at rail width;
   the PIN option is a checkbox and a field that only appear when the gallery
   actually has a PIN to include. */
.gws__deliverstate{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  font-size:var(--fs-md);color:var(--ink)}
.gws__deliver{display:flex;flex-direction:column;align-items:flex-start;gap:7px;width:100%}
.gws__pinopt{display:flex;align-items:flex-start;gap:8px;cursor:pointer;
  font-size:var(--fs-sm);color:var(--ink);line-height:1.4;text-align:left}
.gws__pinopt input{flex:none;margin-top:2px;accent-color:var(--ink)}
.gws__hint{margin:0;font-size:var(--fs-xs);color:var(--slate2);line-height:1.5;text-align:left}
.gws__flabel{font-size:var(--fs-2xs);letter-spacing:.16em;text-transform:uppercase;
  color:var(--slate)}
.gws__area{resize:vertical;min-height:56px;line-height:1.5;font-family:var(--body)}

/* Why this deployment cannot send, stated on the way in rather than discovered
   at send time. It names a configuration variable and never its value. */
.gws__warn{margin:2px 0 0;font-size:var(--fs-xs);color:var(--warn);
  line-height:1.5}

/* The Download PIN row, Task 011C, now one line.
   ---------------------------------------------------------------------------
   It was deliberately two: a state word with its Remove, and the field on its
   own line beneath, because "a text input squeezed onto the same baseline as
   Set reads as a search box, and this is a credential."

   That reasoning held while the rail also printed the gallery address and a
   state word. With both of those gone the row has the width to be one line,
   and the field no longer reads as a search box for two reasons that did not
   apply before: it is `type="password"`, so what goes in is dots, and it is
   short and letter-spaced like the four digits it takes. `wrap` is the safety
   net — at the rail's narrow end Remove drops to a second line rather than
   crushing the field. */
.gws__pin{display:flex;align-items:center;justify-content:flex-end;
  gap:10px;flex-wrap:wrap;width:100%}
.gws__pinset{display:flex;align-items:center;gap:8px;justify-content:flex-end;
  flex:1;min-width:0}
.gws__pinfield{flex:1;min-width:0;max-width:112px;padding:6px 9px;
  border-radius:var(--chip-radius);border:.5px solid var(--line2);
  background:var(--panel2);color:var(--ink);
  font-family:var(--body);font-size:var(--fs-sm);letter-spacing:.22em;
  font-variant-numeric:tabular-nums;text-align:left}
.gws__pinfield::placeholder{letter-spacing:.02em;color:var(--slate)}
.gws__pinfield:focus-visible{outline:2px solid var(--ink);outline-offset:1px}

/* ---------------------------------------------------------------------------
   CLIENT SELECTION — Task 013, admin side.

   The client's choice, on the operator's grid. Deliberately a BADGE and not the
   admin's own checkbox: an operator has to tell "the client selected this" from
   "I have ticked this for a bulk action" at a glance, and both appear on the
   same tile.
   --------------------------------------------------------------------------- */
.galgrid__badge--client{background:var(--accent);color:var(--accent-ink);border-color:transparent}

.gwsel{display:flex;align-items:baseline;justify-content:space-between;gap:14px;
  flex-wrap:wrap;flex-shrink:0;margin-bottom:10px;padding:10px 14px;border-radius:var(--row-radius);
  background:var(--panel2);border:.5px solid var(--line2)}
.gwsel__t{font-size:var(--fs-sm);color:var(--slate2)}
.gwsel__t b{color:var(--ink);font-weight:500}
.gwsel__warn{margin-left:8px;color:var(--warn);font-size:var(--fs-xs)}
.gwsel__acts{display:flex;align-items:center;gap:14px;flex-wrap:wrap}

/* ===========================================================================
   Task 012E — operational typography.

   Everything above this line is the admin. Everything below it is the client
   gallery, which is a marketing surface and keeps Cormorant and the Termina
   wordmark; nothing in this block reaches it.

   THE RULE: if something is operational, it is General Sans.

   That was applied by repointing every `font-family:var(--sans)` in the admin
   to `var(--body)` at its own declaration, which is why there is no long family
   list here. Termina survives in exactly four rules — `.sb__brand .wm`,
   `.solo__brand .wm`, `.mgram` and `.cghero__mark` — and every one of them is a
   Studio Hill MARK rather than interface text. A monogram is a badge, not a
   name; the wordmark is the logo. A guard test pins that count, so a fifth use
   has to be argued for rather than acquired.

   What is left here is case and tracking on the surfaces the shared action-pill
   rule does not own. Navigation and page furniture were deliberately excluded
   from that rule in Task 008B, on the argument that navigation is not an
   action. True, and beside the point: WIDE UPPERCASE is the thing being
   removed, and a tab shouting at .22em is the clearest instance of it on the
   screen. Sentence case, ordinary tracking.
   ------------------------------------------------------------------------- */
.tab,.dtab,.primary,.subacts button,.linklike,.pconfirm,.ni,
/* The action pills are repeated from their shared rule near the top of this
   sheet. That rule sits before `.rowact` and `.st` declare their own
   `text-transform`, so at equal specificity the later declaration was winning
   and VIEW GALLERY stayed in caps while EDIT next to it did not. Restated here,
   after everything, so there is one last word rather than a race. */
.btn,.gbtn,.glink,.chip,.crewpill,.rowact,.hd__btn,.wfs,
/* `.gbtn.pri` is two classes and outranks the single-class list above it, so
   the primary submit — Save contact, Add contact — kept its caps while the
   Cancel beside it lost them. Restated at matching specificity. */
.gbtn.pri,
/* The rest of the buttons in the sheet: secondary actions, the segmented
   controls, pagination and the popover's pair. Every one of them is something
   an operator presses, which is the whole test. */
.btn-sec,.tm__seg button,.seg button,.tfoot .pg button,.tfoot .pg a,.pop .ghost,.pop .go{
  text-transform:none;letter-spacing:.01em}

/* KEPT IN CAPS, deliberately: the small badges. `.locked`, `.tagr`, `.ptag`,
   `.cro__tag` and the role chip under the avatar are eyebrows worn as pills —
   PRIMARY, GALLERY, OWNER — and at 8px sentence case makes them read as body
   text that has been shrunk rather than as a marker. They are labels, not
   actions, and nobody presses them. */

/* The sidebar. JOB REGISTER at .22em was wide enough to wrap onto two lines in
   a 220px rail — which is the tell that this was display typography doing a
   navigation job. The SECTION labels above it (OPERATIONS, PRODUCTION,
   BUSINESS) keep their caps: those are eyebrows grouping the list, and they are
   the thing the case is actually useful for. */
.ni{font-size:var(--fs-md-plus)}

/* The nav and tab labels lose the tracking with the case; without this they
   would be sentence case at display spacing, which reads as neither. */
.tab{font-size:var(--fs-sm-plus)}

/* THE PRIMARY BUTTON IS UNTRACKED, which is `.dash__cta` exactly.
   ---------------------------------------------------------------------------
   The rule above sets .01em across the whole action family; the Dashboard's
   call to action sets none. At 13.5px that is a fifth of a pixel per character
   — invisible on its own, and not the same value. Stated here rather than with
   the other primary rules because the family rule is later in the sheet and
   was quietly winning. */
.hd__btn{letter-spacing:normal}
.dtab{font-size:var(--fs-sm)}

/* Micro-labels KEEP their uppercase — a 10px eyebrow over a field is doing
   structural work and sentence case at that size reads as body copy that has
   been shrunk. What they lose is the display tracking: .2–.24em is a magazine
   measure, and at label size it costs an operator scanning a form real speed.
   One value everywhere, so a label is a label wherever it appears. */
/* `.kpi__l` LEFT THIS LIST. It is the one member with a direct counterpart on
   the Dashboard — `.stat__l`, the letterspaced caps under a stat numeral — and
   that counterpart is set at .18em. Everything still named here has no twin on
   the reference screen: a table header, a field label, a sidebar section. They
   keep the .12em this rule argued for. */
.eyebrow,.sec,.ni,.k,.lbl,.sub-hd,.rail .k,.dr__sec>.k,
.inline-edit__k,.gwd__lbl,.cdd__t,.ccg__t,th{
  letter-spacing:.12em}

/* ===========================================================================
   The client gallery's tiles — the part the Portal's stock library also draws.

   The delivered gallery at /g/ has its own sheet, `imagebooth-gallery.css`
   (design 15A/15B), linked only by the public layout. What remains here is the
   tile vocabulary the stock library reuses on the Portal — the justified rows
   the script measures, the frame, the pick and the download chip — and the
   wordmark rule. Nothing below is about the gallery page's composition.
   =========================================================================== */
.cghero__mark{font-family:var(--sans);font-weight:900;font-size:var(--fs-md);
  letter-spacing:.04em;color:var(--on-photo)}

/* --- justified rows ------------------------------------------------------- */

/* The script solves each row's height so the widths sum exactly to the page,
   then writes a width and a height onto every figure. Until it runs — and
   forever, with scripting off — this is a plain flex wrap at a common height:
   the same photographs in the same order, justified approximately. */
.cgrows{display:flex;flex-wrap:wrap;gap:clamp(4px,.6vw,9px);align-items:flex-start}
.cgfig{position:relative;margin:0;min-width:0;overflow:hidden;
  background:var(--panel2);border-radius:3px;height:clamp(180px,26vw,320px)}
.cgfig img{width:100%;height:100%;object-fit:cover;display:block}
.cgrows.is-justified .cgfig{height:auto}
.cgrows.is-justified .cgfig img{height:100%}
.cgrows:not(.is-justified) .cgfig{width:auto}
.cgrows:not(.is-justified) .cgfig img{width:auto;height:100%}
.cgfig__fr{display:block;width:100%;height:100%;position:relative}
.cgfig__fr--plain{aspect-ratio:4/3;background:var(--panel2)}
.cg__ph{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.cg__ph svg{width:20px;height:20px;stroke:var(--slate);fill:none}
.cgfig__fr::after{content:'';position:absolute;inset:0;pointer-events:none;
  background:var(--photo-hover-veil);opacity:0;transition:opacity 160ms var(--ease)}
.cgfig:hover .cgfig__fr::after,.cgfig:focus-within .cgfig__fr::after{opacity:1}

/* --- download ------------------------------------------------------------- */
.cgfig__cap{position:absolute;right:8px;bottom:8px;opacity:0;
  transition:opacity 160ms var(--ease)}
.cgfig:hover .cgfig__cap,.cgfig:focus-within .cgfig__cap{opacity:1}
@media (hover:none){
  .cgfig__cap{opacity:1}
}
.cgfig__dl{display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;border-radius:50%;
  background:var(--photo-chip);color:var(--photo-chip-ink);
  box-shadow:0 2px 10px var(--photo-chip-shadow);
  transition:background 140ms var(--ease)}
.cgfig__dl:hover{background:var(--photo-chip-hover)}
.cgfig__dl svg{width:15px;height:15px;stroke:currentColor;fill:none}

/* ---------------------------------------------------------------------------
   CLIENT SELECTION — Task 013, the tile's own control.
   Restrained on purpose. A selection here may be finals, retouch requests,
   approval or a shortlist, so the control is a tick rather than a heart — a
   heart says only one of those things.
   --------------------------------------------------------------------------- */
.cgpick{position:absolute;left:14px;top:14px;z-index:3;
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  opacity:0;transition:opacity 160ms var(--ease)}
.cgfig:hover .cgpick,.cgpick:focus-within,.cgfig.is-picked .cgpick{opacity:1}
.cgpick input{position:absolute;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
.cgpick__box{display:flex;align-items:center;justify-content:center;
  width:26px;height:26px;border-radius:var(--chip-radius);
  background:var(--photo-pick);border:1px solid var(--photo-pick-line);
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
.cgpick__box svg{width:13px;height:13px;stroke:transparent;fill:none;stroke-width:2.4}
.cgpick input:checked + .cgpick__box{background:var(--accent);border-color:transparent}
.cgpick input:checked + .cgpick__box svg{stroke:var(--accent-ink)}
.cgpick input:focus-visible + .cgpick__box{outline:2px solid var(--accent);outline-offset:2px}
/* A chosen frame reads as chosen from across the grid, without a border that
   would crop into the photograph. */
.cgfig.is-picked{outline:2px solid var(--accent);outline-offset:-2px}
/* The viewer's own select control — the same decision, made where the client is
   actually looking at the photograph large enough to judge it. */
.lb__pick{display:inline-flex;align-items:center;gap:8px;margin-left:auto;
  padding:7px 13px;border-radius:var(--pill-radius);
  background:var(--photo-pick-soft);border:1px solid var(--photo-pick-softline);
  color:var(--photo-pick-ink);font-family:var(--body);font-size:var(--fs-sm);cursor:pointer}
.lb__pick svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2.4}
.lb__pick.is-on{background:var(--accent);border-color:transparent;color:var(--accent-ink)}
.lb__pick:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ---------------------------------------------------------------------------
   JOB REGISTER — LIST / GRID

   Two drawings of one result set. The switcher changes a template choice and
   nothing else: `view` never reaches `JobRegisterConditions`, so List and Grid
   are structurally incapable of showing different Jobs, and neither of them
   pages, sorts or filters differently.

   No new colour, no new type family, no page-local token. Everything below is
   the admin's existing surfaces, edges and status pills at a different size.
   --------------------------------------------------------------------------- */

/* --- the switcher --------------------------------------------------------- */

/* One control with two states, so it is drawn as one pill with a divider
   rather than as two loose buttons. It sits at the end of the filter row,
   beside Sort, because it belongs to the same "how am I looking at this"
   family as the sorts do. */
.vsw{display:inline-flex;align-items:center;border:.5px solid var(--line);
  border-radius:var(--pill-radius);overflow:hidden;flex-shrink:0}
.vsw__b{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;
  font-family:var(--body);font-size:var(--fs-2xs);color:var(--slate2);
  background:transparent;text-decoration:none;
  transition:color 140ms var(--ease),background 140ms var(--ease)}
.vsw__b+.vsw__b{border-left:.5px solid var(--line)}
.vsw__b svg{width:12px;height:12px}
.vsw__b:hover{color:var(--ink);background:var(--panel-hi)}
/* The selected side reads as pressed: a filled surface and full-strength ink,
   the same language the Retouch tabs use for the tab you are on. */
.vsw__b.on{color:var(--ink);background:var(--panel-hi)}
.vsw__b:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;color:var(--ink)}

/* --- the thumbnail, both sizes -------------------------------------------- */

/* `.jthumb` already existed as a 34px placeholder square, and `.jthumb.mgram`
   borrows it for client monograms — so everything here is additive and scoped
   to the two new variants. Nothing above this line changed. */
.jthumb--list{width:34px;height:34px}
.jthumb--grid{width:100%;height:auto;border-radius:var(--row-radius);
  /* A consistent container, and the ONLY thing that is consistent: the image
     inside keeps its own proportions and is covered into this box rather than
     being squashed to fit it. A portrait interior and a landscape exterior
     both come out as the same rectangle on the grid without either being
     distorted. */
  aspect-ratio:4/3}
.jthumb--grid svg{width:22px;height:22px}

/* The image fills its container and is cropped, never stretched. `display:block`
   removes the inline-baseline gap that would otherwise show a hairline of the
   placeholder ground under every photograph. */
.jthumb.is-image{overflow:hidden;background:var(--panel2)}
.jthumb.is-image img{width:100%;height:100%;object-fit:cover;display:block}

/* --- the grid ------------------------------------------------------------- */

/* Column count follows the width rather than a breakpoint list: `auto-fill`
   with a 232px floor gives two columns on a narrow laptop pane and six or more
   on a wide desktop, with no media query to keep in step. */
.jgrid{display:grid;gap:20px;padding:20px;
  grid-template-columns:repeat(auto-fill,minmax(232px,1fr))}

.jcard{display:flex;flex-direction:column;min-width:0;
  border-radius:var(--card-radius);border:.5px solid var(--edge-row);
  background:var(--surface-row);box-shadow:var(--elev-row);overflow:hidden;
  transition:border-color 140ms var(--ease),box-shadow 140ms var(--ease),
    transform 140ms var(--ease)}
.jcard:hover{border-color:var(--ring-hover);box-shadow:var(--elev-card);
  transform:translateY(-2px)}
@media (prefers-reduced-motion:reduce){
  .jcard{transition:border-color 140ms var(--ease)}
  .jcard:hover{transform:none}
}

/* The whole card above the actions is one anchor to the Job drawer. Padding
   lives here rather than on `.jcard`, so the photograph reaches the card's own
   edges and the text is inset from them. */
.jcard__open{display:flex;flex-direction:column;gap:0;text-decoration:none;
  color:inherit;min-width:0;padding:10px 10px 0}
.jcard__open:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}

.jcard__body{display:flex;flex-direction:column;gap:5px;padding:11px 4px 12px;
  min-width:0}
/* Cormorant, as the Register's project titles already are — this is the same
   editorial voice at card size, not a new one. Two lines then ellipsis, so a
   long project name cannot make one card taller than its neighbours. */
.jcard__t{font-family:var(--serif);font-size:var(--fs-lg);line-height:1.25;
  color:var(--ink);
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden}
.jcard__cl{font-size:var(--fs-xs);color:var(--slate2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.jcard__meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;
  font-size:var(--fs-2xs);color:var(--slate);text-transform:uppercase}
.jcard__dot{color:var(--line2)}
.jcard__st{display:flex;align-items:center;justify-content:space-between;
  gap:10px;flex-wrap:wrap;margin-top:3px}
.jcard__gal{font-size:var(--fs-2xs);color:var(--slate);
  text-transform:uppercase}
.jcard__gal.ok{color:var(--ok)}

/* The two explicit actions, outside the card's anchor so neither is nested in
   the other. A quiet divider keeps them from reading as part of the metadata. */
.jcard__acts{display:flex;align-items:center;gap:6px;flex-wrap:wrap;
  padding:9px 14px;border-top:.5px solid var(--line);margin-top:auto}
.jcard__acts .rowact{margin-left:0}

/* The grid's own footer sits on the card surface, like the table's does. */
.jgrid+.tfoot{border-top:.5px solid var(--line)}

@media (max-width:720px){
  .jgrid{gap:14px;padding:14px;grid-template-columns:repeat(auto-fill,minmax(160px,1fr))}
  .jcard__t{font-size:var(--fs-md-plus)}
}

/* ---------------------------------------------------------------------------
   DASHBOARD — Task 014

   Hierarchy first. Today and Needs Attention share the top band at full size;
   everything below them is a summary in a smaller voice. Nothing here is a
   chart, and every number is a count of things somebody can act on.

   No new colour and no new type family: the status dots are `--info`, `--warn`,
   `--ok` and `--hot`, which is the same four-tone vocabulary the stage pills
   have always used, and the headings are the same Cormorant the Job Register's
   is. There is no page-local `:root` anywhere in this block.
   --------------------------------------------------------------------------- */

/* THE PAGE'S OWN RHYTHM.
   ---------------------------------------------------------------------------
   This was `gap:0`, with every section carrying its own `margin-top` instead.
   That was wrong twice: it threw away `.ws`'s existing 16px rhythm, and it left
   the header with NO gap at all — the New Job button's bottom edge landed
   exactly on the Invoice-ready card's top edge, measured at zero clearance.
   One gap, slightly more generous than the default, and the sections below
   carry no margins of their own. */
.dash{gap:20px}

/* --- needs attention ------------------------------------------------------
   The component was rewritten for the approved design and now lives with the
   rest of that composition at the end of this file. The rules that stood here
   drew a dot-and-two-lines row; the approved row is a Job line, a caps status
   line and a muted sub-line, with no dot. */

/* A footer link, pinned to the base of its panel — the pattern the reference
   uses on every column. `margin-top:auto` is what makes the two top-band
   panels end on the same line whatever is in them. */

.minilist{display:flex;flex-direction:column}
.minilist li{display:flex;align-items:center;justify-content:space-between;
  gap:12px;border-top:.5px solid var(--line);padding:9px 2px;min-width:0}
.minilist li>a:first-child{display:flex;flex-direction:column;gap:2px;
  min-width:0;text-decoration:none;color:inherit;flex:1}
.minilist li>a:first-child:hover .minilist__t{color:var(--ink)}
.minilist li>a:first-child:focus-visible{outline:2px solid var(--accent);
  outline-offset:-2px}
.minilist__t{font-size:var(--dash-fs-title);color:var(--slate2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  transition:color 140ms var(--ease)}
.minilist__m{font-size:var(--dash-fs-meta);color:var(--slate)}
/* "To confirm" is not a number and must not read like one. */
.minilist .rowact{margin-left:0;flex-shrink:0}
/* ---------------------------------------------------------------------------
   DASHBOARD — density, hierarchy and the header modules

   The refinement pass. Three faults were being fixed: operational text at
   9.5px, panels holding a fixed height whether or not they had anything in
   them, and every card the same size as every other. The type scale lives in
   the token sheet as `--dash-fs-*`; this block is spacing and proportion.
   --------------------------------------------------------------------------- */

/* --- header --------------------------------------------------------------- */

/* --- top band: content-driven ---------------------------------------------- */

/* `start`, not `stretch`. A panel with two shoots in it should be as tall as
   two shoots — the earlier stretch made the shorter card hold a column of
   nothing to keep its neighbour company. */
.minilist li{padding:8px 2px}

/* --- the row is the target, so the row responds ---------------------------- */

/* Attention and minilist rows were changing one line of text on hover while the
   whole row was clickable. The surface now answers, on the same radius the
   other rows use, and the text still lifts to full ink on top of it. */
@media (hover:hover) and (pointer:fine){
}

/* --- overdue reads as overdue ---------------------------------------------- */

/* Every severity was a coloured dot of identical weight, so the most urgent
   state in the admin looked exactly like the least. A halo, not a pulse: this
   is a screen somebody keeps open, and a throbbing dot on it would be a
   gimmick that has to be looked away from. */

/* --- entrance -------------------------------------------------------------- */

/* ONE-TIME, ON LOAD, AND SHORT.
   -----------------------------------------------------------------------
   The Dashboard is opened a handful of times a day, not a hundred — this is
   not a keyboard-initiated action, and it is the screen whose whole job is to
   feel alive when somebody arrives at it. Five blocks, 40ms apart, 240ms each:
   everything has settled 400ms in, which is below the point where a page reads
   as slow to appear.

   A rise of 6px and nothing else. No scale, because a whole page scaling in is
   a marketing animation, and no stagger inside the blocks, because nested
   stagger is where this stops being a dashboard and starts being a demo.

   `both` fill with a visible end state means an engine that refuses to run the
   animation shows the finished page rather than an empty one. */
@keyframes dash-rise{
  from{opacity:0;transform:translateY(6px)}
  to{opacity:1;transform:translateY(0)}
}

/* TWO ROWS, ONE BEAT APART. It used to be four staggered blocks — KPI row,
   band, module grid, footer — which at 40ms each was 160ms of the page
   assembling itself in front of the reader. Two rows is one short beat. */
.dash>.dash__stats,
.dash>.dash__mid,
.dash>.dash__grid{animation:dash-rise 240ms var(--ease) both}
.dash>.dash__mid{animation-delay:60ms}
.dash>.dash__grid{animation-delay:120ms}

/* Reduced motion means fewer and gentler, not none: the fade stays because it
   aids comprehension, and every movement goes. */
@media (prefers-reduced-motion:reduce){
  .dash>.dash__stats,
  .dash>.dash__mid,
  .dash>.dash__grid{animation:none}
}

/* ---------------------------------------------------------------------------
   DASHBOARD — ONE SCREEN

   What you see on load IS the Dashboard. At the desktop breakpoint the page
   does not scroll: every module is on screen at once and Production is the
   bottom row.

   The mechanism is flow, not position. `.dash` is a full-height flex column
   that hides its own overflow; the top band takes the slack; Production is
   pushed down by `margin-top:auto`. Nothing is `position:fixed`, so the strip
   cannot cover content — on a short window it simply sits after the content
   like any other row, and the page scrolls as it always did.

   Typography is NOT reduced to achieve this. The `--dash-fs-*` scale is
   untouched; the fit comes from capped lists and tighter rhythm.
   --------------------------------------------------------------------------- */

@media (min-width:1281px) and (min-height:900px){
  /* The workspace hands its height to the page and tightens its rhythm.

     `overflow:auto`, NOT `hidden`. The goal is that it never needs to scroll —
     achieved by capping the lists so the content fits — and `hidden` would
     turn any miscalculation into silently amputated content. A scrollbar that
     never appears costs nothing; a clipped Production strip that nobody can
     reach is a bug. Fit by design; scroll as the safety net; never clip. */
  /* 10px below the footer strip, not the workspace's usual 26. Measured: with 26
     the modules all fitted and the PADDING was what overflowed — a scrollbar
     that existed to reveal empty space. */
  .ws.dash{overflow-y:auto;gap:12px;padding-bottom:10px}

  /* The last 34 pixels, measured.
     -----------------------------------------------------------------------
     At 1600×1000 the composed page came to 1034 against 1000 — a 3% miss, and
     a miss is a scrollbar. These four are density, not typography: the type
     scale is untouched and every one of them is scoped to this breakpoint, so
     a smaller window gets the roomier rhythm back.

     Listed rather than folded into the rules above so the next person can see
     exactly what was spent, and put it back first if a module is ever added. */
  /* NOTHING STRETCHES TO FILL THE VIEWPORT.
     -----------------------------------------------------------------------
     Panels are content-sized and the slack collects at the BOTTOM of the page
     as breathing room. That is a deliberate reversal of the earlier rule,
     where a footer strip with `margin-top:auto` pushed the spare height into
     one gap and every panel above it stretched to meet it — which on a quiet
     morning produced a 400-pixel panel holding one line of empty state.

     Empty space at the foot of a composed page is fine. A tall empty box is
     not, and filling space simply because it exists is what made this page
     feel like a form rather than a surface. */
  .dash>.dash__stats,
  .dash>.dash__mid,
  .dash>.dash__grid{flex:0 0 auto}
}

/* ---------------------------------------------------------------------------
   DASHBOARD — SECTION HEADINGS AND THE ACTION MODULES
   --------------------------------------------------------------------------- */

/* Headings up ~10% and given real weight. The body scale is untouched: this is
   about the hierarchy BETWEEN a section's name and its contents, which at 400
   weight and the same size as a Job title was doing very little work. */
/* SECTION HEADINGS: General Sans Medium, not Cormorant.
   ---------------------------------------------------------------------------
   The brief asks for these to be bolder. CORMORANT HAS NO BOLD — the project
   ships Light (300) and Regular (400) and nothing else, and asking for 600
   would make the browser synthesise a fake bold by smearing the Regular. A
   `design_fidelity` guard catches exactly that, and it caught this.

   So the weight comes from the face that actually has one: General Sans Medium,
   which is a supplied file, and which is also what every OTHER panel heading in
   the admin already uses. Cormorant stays where it belongs and where the brief
   asks it to stay — the greeting, at display size, as the page's one editorial
   moment. A section label is not that.

   Size is up ~11% (17 → 19px) and the tracking is pulled in slightly, which is
   what a heading at this weight wants. */

/* Three action states rather than four lifecycle counts, so they sit in a row
   and each gets more width than a 2×2 grid gave it. */
/* Two counts side by side, for a module whose third number is context rather
   than an action — see My Tasks, where "this week" is an aside instead. */
/* A zero in an action module is context, not an alarm — it recedes rather than
   sitting at full strength beside a number that needs doing. */

/* ===========================================================================
   TASK 015 — THE MORNING BRIEF AND MY TASKS
   ===========================================================================
   Every colour here is a token. The two modules land inside the existing
   five-across lower row without changing it, and the one-screen contract is
   preserved by the caps in the template rather than by shrinking type. */

/* One brief line: a tone dot, a title, a quiet detail. Same rhythm as `.attn`
   and `.feed`, because a brief line is the same kind of object as an attention
   row — a fact about the day with somewhere to go. */
.brieflist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
.brieflist__i{display:flex;align-items:flex-start;gap:8px}
.brieflist__dot{width:6px;height:6px;border-radius:var(--pill-radius);flex-shrink:0;
  margin-top:6px;background:var(--slate)}
.brieflist__dot.hot{background:var(--hot)}
.brieflist__dot.warn{background:var(--warn)}
.brieflist__dot.info{background:var(--info)}
.brieflist__body{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
.brieflist__t{font-size:var(--dash-fs-title);color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
a.brieflist__t{text-decoration:none}
a.brieflist__t:hover{color:var(--accent)}
a.brieflist__t:focus-visible{outline:2px solid var(--accent);outline-offset:2px;
  border-radius:2px}
.brieflist__d{font-size:var(--dash-fs-meta);color:var(--slate);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* §5 — WHICH SYSTEM OWNS THE LINE.
   A shoot is Studio Hill's record; a meeting is Google's. The left rule is the
   quietest mark that reads as a category at a glance, and it differs by kind so
   the two are never confused in a mixed list. */
.brieflist--full .brieflist__i{padding:8px 0 8px 10px;
  border-left:2px solid var(--line);border-bottom:.5px solid var(--line);
  align-items:center}
.brieflist--full .brieflist__i:last-child{border-bottom:0}
.brieflist--full .brieflist__i--shoot{border-left-color:var(--accent)}
.brieflist--full .brieflist__i--conflict{border-left-color:var(--hot)}
.brieflist--full .brieflist__i--task{border-left-color:var(--warn)}
.brieflist--full .brieflist__t{white-space:normal}

/* The kind, said in words on the full page where there is room for it. */
.brieflist__k{font-family:var(--mono);font-size:var(--dash-fs-label);
  letter-spacing:.06em;text-transform:uppercase;color:var(--slate);
  flex-shrink:0;padding-left:10px}

/* The conflict banner on the Dashboard card. One line, loud enough to read
   before anything else in the module and no louder. */

/* §5 — CALENDAR INSIDE TODAY.
   Deliberately unlike a shoot: no image, no stage pill, no client line, and a
   lighter voice. It shares the panel because a day contains both, and it is
   drawn differently because Studio Hill owns one and not the other. */

/* A conflict row in Needs Attention. Same component as every other row there;
   only the reason differs. */

/* --- The full brief page ------------------------------------------------- */
.briefpage{display:grid;grid-template-columns:minmax(0,1fr) 260px;gap:22px;
  align-items:start}
.briefpage__main{display:flex;flex-direction:column;gap:18px;min-width:0}
.briefpage__side{border-left:.5px solid var(--line);padding-left:18px}
.briefsec{border:.5px solid var(--line);border-radius:var(--row-radius);padding:14px 16px;
  background:var(--panel)}
.briefpage__side .minilist li>a:first-child{padding:7px 0}
.briefpage__side .minilist li>a[aria-current="page"] .minilist__t{color:var(--accent)}

@media (max-width:980px){
  .briefpage{grid-template-columns:minmax(0,1fr)}
  .briefpage__side{border-left:0;padding-left:0;
    border-top:.5px solid var(--line);padding-top:16px}
}

/* ===========================================================================
   DASHBOARD — THE APPROVED STUDIO HILL COMPOSITION
   ===========================================================================
   Every measurement here is stated by the signed-off design. Colours come from
   the `--dash-*` tokens in `studiohill-tokens.css`, which resolve for explicit
   light, explicit dark and system-follows-the-OS exactly as every other token
   in the application does — there is no page-local `:root` in this file.

   The composition is LOCAL to `.dash`. No other screen inherits the glass
   treatment: shared primitives are shared through the token file, page
   composition stays here. */

/* --- The page ground ----------------------------------------------------- */

/* NO BACKGROUND HERE ANY MORE. The Dashboard used to paint the ground itself,
   which is exactly what made it the odd one out — see the note on `.app`. It
   now sits on the shared ground like every other screen, in the same box, so
   "matches the Dashboard" is true by construction rather than by two values
   being kept in step by hand. */
.ws.dash{position:relative;color:var(--dash-ink);
  transition:color 400ms ease}

/* Three slow-drifting blurred fields. Decorative, behind everything, and
   pinned to the workspace rather than the viewport so they scroll with it. */
.dash__bloom{position:absolute;inset:0;overflow:hidden;pointer-events:none;
  z-index:-1}
.dash__blob{position:absolute;border-radius:50%;display:block}
.dash__blob--1{top:-160px;left:10%;width:700px;height:520px;
  background:radial-gradient(circle,var(--dash-blob-1),transparent 65%);
  filter:blur(60px);animation:dash-drift-a 18s ease-in-out infinite}
.dash__blob--2{top:20%;right:-140px;width:640px;height:640px;
  background:radial-gradient(circle,var(--dash-blob-2),transparent 65%);
  filter:blur(70px);animation:dash-drift-b 22s ease-in-out infinite}
.dash__blob--3{bottom:-200px;left:35%;width:620px;height:620px;
  background:radial-gradient(circle,var(--dash-blob-3),transparent 65%);
  filter:blur(70px);animation:dash-drift-a 26s ease-in-out infinite}

@keyframes dash-drift-a{
  0%,100%{transform:translate(0,0) scale(1)}
  50%{transform:translate(90px,-60px) scale(1.18)}
}
@keyframes dash-drift-b{
  0%,100%{transform:translate(0,0) scale(1)}
  50%{transform:translate(-80px,50px) scale(1.1)}
}

/* --- Header -------------------------------------------------------------- */

.dash__hdr{display:flex;align-items:center;justify-content:space-between;
  gap:20px;flex-shrink:0}
/* The greeting's column may shrink; the controls beside it may not. */
.dash__id{min-width:0}
/* `nowrap` is the design's, and it is why this also has to clip.
   ---------------------------------------------------------------------------
   The heading is one line by decision. A display name is operator-entered and
   `Greeting` caps a first name at 24 characters, so the longest thing this can
   be asked to draw is a 24-character name inside the longest phrase — which
   measures 1225px against a 996px header and pushes New Job off the screen.
   Clipping keeps the controls where they are; the alternative is a header that
   silently breaks for one person. Nothing changes for a real name: at "Jenni"
   the greeting is 460px with 550px to spare. */
.dash__greet{margin:0;font-family:var(--serif);font-weight:400;
  font-size:var(--dash-fs-display);letter-spacing:-.01em;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;min-width:0;
  color:var(--dash-ink);
  /* 1.25, AND THE FLOOR IS THE FONT'S OWN METRICS.
     `overflow:hidden` above clips BOTH axes — it is there for the horizontal
     ellipsis, and the vertical clipping was an unwanted side effect nobody had
     a reason to notice until a name with a descender arrived. Measured at
     46px: Cormorant reports 43px of ascent and 13px of descent, so the glyph
     box is 56px and a 1.1 line box is 50.6px. The italic J in "Josh" and
     "Jenni" lost 2px off the bottom, and the cap 2px off the top.
     56/46 is 1.218, so 1.22 is the smallest value that can contain any glyph
     in the face; 1.25 takes it with a little room rather than exactly none.
     It costs 7px of header height, which is the price of the descender. */
  line-height:1.25}
/* THE ONE ITALIC IN THE APPLICATION. No italic Cormorant file was supplied
   with the design, so this word is a synthesised oblique — deliberate, bounded
   to the greeting, and asserted as bounded by `design_fidelity`. */
/* The one synthesised italic in the product, shared by the Dashboard's
   greeting and the client portal's. Extended by selector rather than
   redeclared, so there is still exactly one italic in this file. */
.dash__greet em,.pth__greet .pth__name{font-style:italic}

.dash__hdr-right{display:flex;align-items:center;gap:14px;flex-shrink:0}

.dash__meta{margin:6px 0 0;font-size:var(--dash-fs-line);color:var(--dash-muted);flex-shrink:0}
.dash__clock{font-variant-numeric:tabular-nums}
.dash__sep{margin:0 7px;opacity:.5}

/* The weather pill. Glass, like every other surface on this page. */
.wx{display:flex;align-items:center;gap:16px;padding:0 18px;
  min-height:var(--dash-ctl-h);white-space:nowrap;
  border-radius:var(--dash-r-card);background:var(--dash-glass);
  backdrop-filter:blur(var(--dash-blur)) saturate(1.4);
  -webkit-backdrop-filter:blur(var(--dash-blur)) saturate(1.4);
  border:1px solid var(--dash-glass-border);font-size:var(--dash-fs-meta);
  transition:background 400ms ease,border-color 400ms ease}
.wx__now{display:flex;align-items:center;gap:8px;font-weight:600}
.wx__sun{width:11px;height:11px;border-radius:50%;background:var(--dash-sun);
  box-shadow:var(--dash-sun-glow);flex-shrink:0;transition:background 400ms ease}
.wx__times{color:var(--dash-muted)}
.wx__chip{padding:4px 10px;border-radius:var(--pill-radius);background:var(--dash-chip);
  border:1px solid var(--dash-chip-border);font-weight:600;
  color:var(--dash-chip-ink);white-space:nowrap;
  transition:background 400ms ease,color 400ms ease}

/* SEARCH, IN THE DASHBOARD'S OWN MATERIAL.
   ---------------------------------------------------------------------------
   The markup is `.hd__search`, unchanged, so the behaviour and the accessible
   name are the Job Register's. Only the surface differs: on this page a panel
   is glass over the ground, and an opaque cream field beside the weather pill
   would be the one solid object in a header made of glass.

   Height is matched to `.dash__cta` beside it. MEASURED, not assumed: at 12px
   of padding the field came out 42 against the button's 44, which is exactly
   the kind of two-pixel step that reads as sloppy without being nameable. 13px
   lands both at 44. */
.dash__search{background:var(--dash-glass);
  backdrop-filter:blur(var(--dash-blur)) saturate(1.4);
  -webkit-backdrop-filter:blur(var(--dash-blur)) saturate(1.4);
  border:1px solid var(--dash-glass-border);border-radius:var(--dash-r-card);
  padding:0 18px;min-height:var(--dash-ctl-h);flex:0 1 250px;min-width:170px;
  transition:background 400ms ease,border-color 400ms ease}
.dash__search svg{stroke:var(--dash-muted)}
.dash__search input{color:var(--dash-ink);font-size:var(--dash-fs-body)}
.dash__search input::placeholder{color:var(--dash-faint)}
.dash__search:focus-within{border-color:var(--dash-hi-border)}

/* The one primary button on the page. */
.dash__cta{font-family:var(--body);font-size:var(--dash-fs-nav);font-weight:700;
  display:inline-flex;align-items:center;justify-content:center;
  padding:0 22px;min-height:var(--dash-ctl-h);border-radius:var(--dash-r-card);
  border:1px solid var(--dash-btn-border);background:var(--dash-btn);
  color:var(--dash-btn-ink);box-shadow:var(--dash-btn-shadow);
  text-decoration:none;white-space:nowrap;
  transition:transform 160ms var(--dash-ease),box-shadow 160ms var(--dash-ease),
    background 400ms ease,color 400ms ease}
.dash__cta:hover{transform:translateY(-2px)}
.dash__cta:active{transform:scale(.97)}
.dash__cta:focus-visible{outline:2px solid var(--dash-info);outline-offset:3px}

/* --- The stat row -------------------------------------------------------- */

.dash__stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  gap:var(--dash-gap);margin-top:30px;flex-shrink:0}
.stat{padding:var(--dash-pad-stat);border-radius:var(--dash-r-card);
  background:var(--dash-glass);backdrop-filter:blur(var(--dash-blur));
  -webkit-backdrop-filter:blur(var(--dash-blur));
  border:1px solid var(--dash-glass-border);box-shadow:var(--dash-glass-shadow);
  text-decoration:none;color:inherit;display:block;
  transition:transform 200ms var(--dash-ease),box-shadow 200ms var(--dash-ease),
    background 400ms ease,border-color 400ms ease}
.stat:hover{transform:translateY(-5px);box-shadow:var(--dash-glass-shadow-hi)}
.stat:active{transform:translateY(-2px) scale(.99)}
.stat:focus-visible{outline:2px solid var(--dash-info);outline-offset:3px}
/* A LIT CARD IS A CARD WITH SOMETHING IN IT. The border and glow are applied
   only when the count is non-zero — nothing to do should not glow. */
.stat--hi{border-color:var(--dash-hi-border);
  box-shadow:var(--dash-hi-glow),var(--dash-glass-shadow)}
.stat--hi:hover{box-shadow:var(--dash-hi-glow),var(--dash-glass-shadow-hi)}
.stat--warn{border-color:var(--dash-warn-border);
  box-shadow:var(--dash-warn-glow),var(--dash-glass-shadow)}
.stat--warn:hover{box-shadow:var(--dash-warn-glow),var(--dash-glass-shadow-hi)}
.stat__n{display:block;font-family:var(--serif);font-size:var(--dash-fs-stat);
  line-height:1;color:var(--dash-ink);font-weight:400}
.stat__n.is-hi{color:var(--dash-num);text-shadow:var(--dash-num-glow)}
.stat__n.is-warn{color:var(--dash-warn-num);text-shadow:var(--dash-warn-num-glow)}
.stat__n.is-zero{color:var(--dash-num-dim)}
.stat__l{display:block;margin-top:10px;font-size:var(--dash-fs-label);
  letter-spacing:.18em;text-transform:uppercase;color:var(--dash-muted)}

/* --- Cards --------------------------------------------------------------- */

.dash__card{padding:var(--dash-pad-card);border-radius:var(--dash-r-card);
  background:var(--dash-glass);backdrop-filter:blur(var(--dash-blur));
  -webkit-backdrop-filter:blur(var(--dash-blur));
  border:1px solid var(--dash-glass-border);box-shadow:var(--dash-glass-shadow);
  display:flex;flex-direction:column;min-width:0;
  transition:background 400ms ease,border-color 400ms ease}

.dash__hd{display:flex;align-items:center;justify-content:space-between;gap:12px}
.dash__t{margin:0;font-family:var(--serif);font-size:var(--dash-fs-heading);
  font-weight:500;letter-spacing:0;line-height:1.2;color:var(--dash-ink)}
.dash__hdmeta{font-size:var(--dash-fs-meta);color:var(--dash-muted);
  font-weight:500;white-space:nowrap}
/* A GROUP HEADING ON A LIST PAGE, and its count.
   ---------------------------------------------------------------------------
   My Tasks and the full Morning Brief both group their rows under a heading —
   "Overdue", "Due today" — and both were the last two selectors still being
   served by the superseded Task 014 block: a Cormorant 400 heading and a
   hot-bordered count pill, sitting inside pages that had otherwise moved to
   the approved language. Restated here, so those two pages read as the same
   product as the card they came from.

   The heading is `.dash__t` at the same size — a group inside a page is not
   smaller than a card's title, it is the same rank in a different container.
   The count is the approved `.dash__badge` treatment without its urgency: a
   group of four tasks is a fact, not an alarm. */
.dash__t--sm{font-size:var(--dash-fs-heading)}
.dash__count{min-width:22px;height:22px;border-radius:var(--pill-radius);
  background:var(--dash-item);border:1px solid var(--dash-glass-border);
  color:var(--dash-muted);font-size:var(--dash-fs-meta);font-weight:600;
  display:inline-flex;align-items:center;justify-content:center;padding:0 6px}
.dash__badge{min-width:22px;height:22px;border-radius:var(--pill-radius);
  background:var(--dash-badge);border:1px solid var(--dash-badge-border);
  color:var(--dash-badge-ink);font-size:var(--dash-fs-meta);font-weight:700;
  display:inline-flex;align-items:center;justify-content:center;padding:0 6px;
  transition:background 400ms ease}

.dash__micro{font-size:var(--dash-fs-label);letter-spacing:.18em;
  text-transform:uppercase;color:var(--dash-faint)}
.dash__none{font-size:var(--dash-fs-body);color:var(--dash-muted);
  line-height:1.5;padding:10px 0 2px}
.dash__clear{display:flex;align-items:center;gap:8px;
  font-size:var(--dash-fs-body);color:var(--dash-muted);padding:12px 0 4px}
.dash__clear svg{width:14px;height:14px;flex-shrink:0;stroke:var(--dash-info)}
.dash__aside{font-size:var(--dash-fs-small);color:var(--dash-faint);
  padding-top:10px;margin-top:10px;border-top:1px solid var(--dash-item-border)}
.dash__link{font-size:var(--dash-fs-meta);font-weight:600;color:var(--dash-info);
  text-decoration:none}
.dash__link:hover{opacity:.8}
.dash__link--foot{margin-top:auto;padding-top:14px}
.dash__empty{padding:26px 8px;text-align:center;color:var(--dash-muted)}
.dash__empty svg{width:22px;height:22px;stroke:var(--dash-faint);margin-bottom:10px}
.dash__empty .t{font-size:var(--dash-fs-title);color:var(--dash-ink)}
.dash__empty .s{font-size:var(--dash-fs-small);margin-top:5px;line-height:1.5}

/* --- Today + Upcoming ---------------------------------------------------- */

/* THE MIDDLE ROW — Task 018.
   ---------------------------------------------------------------------------
   Today at about 65%, the studio's latest posts at about 35%. Today used to
   run the full width, which left a light day stretched across the whole page;
   the Instagram module is what balances it.

   `minmax(0,…)` on both columns for the reason the lower grid uses it: without
   it a long shoot title makes the first column refuse to shrink and blows the
   row out. When there is nothing to draw the row is one column and Today is
   exactly the card it was — the module never leaves a gap. */
.dash__mid{display:grid;grid-template-columns:minmax(0,62fr) minmax(0,38fr);
  gap:var(--dash-gap);margin-top:var(--dash-gap);align-items:stretch;flex-shrink:0}
.dash__mid--solo{grid-template-columns:minmax(0,1fr)}

/* THE INSTAGRAM CARD CARRIES LESS SIDE PADDING than a card of prose does:
   every millimetre off the sides is a millimetre of photograph, and its
   content is a band of images rather than text that wants a margin. The
   VERTICAL padding is Today's exactly, so the two cards start and finish on
   the same lines.

   It lives here rather than beside the rest of the Instagram rules because
   `.dash__card` is declared further down this sheet, and a single-class
   override written earlier would lose to it on source order — which is
   precisely what happened the first time. */
.dash__social{padding:24px 14px}

.dash__today{display:grid;
  grid-template-columns:2fr 1fr;gap:26px;flex-shrink:0}
.dash__todaymain{display:flex;flex-direction:column;min-width:0}
.dash__upnext{border-left:1px solid var(--dash-glass-border);padding-left:26px;
  display:flex;flex-direction:column;gap:10px;
  transition:border-color 400ms ease}

/* Two shoots per row, which is what makes eight fit without a scroll. */
.shoots{display:grid;grid-template-columns:1fr 1fr;gap:8px 14px;margin-top:14px}
.shot{display:flex;align-items:center;gap:12px;padding:9px 12px;
  border-radius:var(--dash-r-row);background:var(--dash-item);
  border:1px solid var(--dash-item-border);text-decoration:none;color:inherit;
  min-width:0;
  transition:transform 160ms var(--dash-ease),border-color 160ms ease,
    background 400ms ease}
a.shot:hover{transform:translateX(3px);border-color:var(--dash-hi-border)}
a.shot:active{transform:translateX(3px) scale(.99)}
a.shot:focus-visible{outline:2px solid var(--dash-info);outline-offset:2px}
.shot__time{font-size:var(--dash-fs-time);font-weight:600;color:var(--dash-info);
  width:52px;flex-shrink:0;font-variant-numeric:tabular-nums}
.shot--warn .shot__time{color:var(--dash-status-warn)}
.shot__body{display:flex;flex-direction:column;gap:1px;min-width:0}
.shot__t{font-size:var(--dash-fs-body);font-weight:600;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;color:var(--dash-ink)}
.shot__m{font-size:var(--dash-fs-small);color:var(--dash-muted);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.shot--warn .shot__m{color:var(--dash-status-warn)}
/* A CALENDAR COMMITMENT IS NOT A SHOOT. Same row geometry so the day reads
   chronologically, but a dot instead of a time column and a muted title — the
   studio owns one of these and not the other. */
.shot--cal .shot__t{font-weight:500;color:var(--dash-muted)}
.shot__dot{width:52px;flex-shrink:0;display:flex;align-items:center}
.shot__dot::before{content:"";width:7px;height:7px;border-radius:50%;
  background:var(--dash-dot);box-shadow:var(--dash-dot-glow);margin-left:19px}

.upnext{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;
  list-style:none}
.upnext a{display:flex;flex-direction:column;gap:1px;text-decoration:none;
  color:inherit}
.upnext a:hover .upnext__t{color:var(--dash-info)}
.upnext__d{font-size:var(--dash-fs-label);letter-spacing:.1em;
  text-transform:uppercase;color:var(--dash-faint)}
.upnext__t{font-size:var(--dash-fs-body);font-weight:600;color:var(--dash-ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.upnext__c{font-size:var(--dash-fs-small);color:var(--dash-muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* --- The lower grid ------------------------------------------------------ */

/* 6:3:3 — the Morning Brief at half the row, the two lists sharing the rest.
   ---------------------------------------------------------------------------
   It was 5:4:3 with the brief in the narrow third column, which was the right
   proportion for the panel it used to be: four assembled lines. It is now the
   only panel on this screen carrying PROSE, and a paragraph in a 265px column
   is four words a line. Half the row gives the narrative a readable measure.

   My Tasks and Needs Attention are lists, they are each one click from their
   own full screen, and a list does not need the width a paragraph does — so
   they are what gives the width up.

   The `minmax(0,…)` is required, not stylistic: without it a long task title
   refuses to shrink and pushes the whole grid past the viewport. */
.dash__grid{display:grid;
  grid-template-columns:minmax(0,6fr) minmax(0,3fr) minmax(0,3fr);
  gap:var(--dash-gap);margin-top:var(--dash-gap);align-items:stretch}

/* My tasks */
.dash__marker{display:flex;align-items:center;gap:8px;margin-top:14px;
  font-size:var(--dash-fs-micro);letter-spacing:.16em;text-transform:uppercase;
  font-weight:700;color:var(--dash-badge-ink)}
.dash__markdot{width:7px;height:7px;border-radius:50%;
  background:var(--dash-badge-border);box-shadow:0 0 8px var(--dash-badge-border)}
.dash__markn{font-size:var(--dash-fs-meta);color:var(--dash-faint);
  font-weight:600;letter-spacing:0}
.tsks{display:flex;flex-direction:column;gap:8px;margin:10px 0 0;padding:0;
  list-style:none}
.tsk{display:flex;align-items:center;gap:11px;padding:10px 12px;
  border-radius:var(--dash-r-row);background:var(--dash-item);
  border:1px solid var(--dash-item-border);min-width:0;
  transition:border-color 160ms ease,background 400ms ease}
.tsk:hover{border-color:var(--dash-hi-border)}

/* --- a card wears its list's colour ---------------------------------------
   The rail tints each list's dot; the cards now carry the same tone on their
   outline, so a Retouching card and the Retouching row read as one thing
   without the eye having to go back to the rail and check.

   THE TONE IS SET BY A CLASS, NOT AN INLINE STYLE. `style-src 'self'` has no
   `unsafe-inline`, so a `style="--tsk-tone: …"` attribute would be dropped by
   the Content-Security-Policy and the cards would silently stay grey. Five
   classes, the same arrangement `.trail__dot--N` already uses — and the tone
   numbers come from one map in the template, so the dot and the card cannot
   disagree.

   SUBTLE AT REST, FULL ON HOVER. Six saturated outlines at rest would turn a
   board into a paint chart and drown the one signal that matters, which is
   which card is late. `color-mix` derives the resting tint from the same
   token rather than introducing five more; the plain colour is declared first
   so a browser without `color-mix` keeps a working, if louder, border.
   ------------------------------------------------------------------------- */
.tsk--t1{--tsk-tone:var(--hot)}
.tsk--t2{--tsk-tone:var(--warn)}
.tsk--t3{--tsk-tone:var(--info)}
.tsk--t4{--tsk-tone:var(--ok)}
.tsk--t5{--tsk-tone:var(--crew-violet)}

.tsk[class*="tsk--t"]{border-color:var(--tsk-tone)}
@supports (border-color:color-mix(in srgb,red 50%,transparent)){
  .tsk[class*="tsk--t"]{
    border-color:color-mix(in srgb,var(--tsk-tone) 34%,transparent)}
}
.tsk[class*="tsk--t"]:hover{border-color:var(--tsk-tone)}

/* A completed card stops advertising its list. It is done: the colour was a
   routing signal and there is nothing left to route. */
.tsk--done[class*="tsk--t"],
.tsk--done[class*="tsk--t"]:hover{border-color:var(--dash-item-border)}
.tsk__box{width:16px;height:16px;border-radius:50%;
  border:1.5px solid var(--dash-faint);flex-shrink:0}
.tsk__t{flex:1;min-width:0;font-size:var(--dash-fs-title);font-weight:500;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--dash-ink)}
.tsk__late{flex-shrink:0;font-size:var(--dash-fs-label);font-weight:600;
  padding:3px 10px;border-radius:var(--dash-r-badge);background:var(--dash-badge);
  border:1px solid var(--dash-badge-border);color:var(--dash-badge-ink);
  white-space:nowrap;transition:background 400ms ease}
.tsk__m{flex-shrink:0;font-size:var(--dash-fs-small);color:var(--dash-muted)}
.tsk__who{flex-shrink:0;width:24px;height:24px;border-radius:50%;
  background:var(--dash-item);border:1px solid var(--dash-glass-border);
  display:inline-flex;align-items:center;justify-content:center;
  font-size:var(--dash-fs-avatar);font-weight:700;color:var(--dash-muted);text-transform:uppercase}

/* Needs attention */
.attn{display:flex;flex-direction:column;margin-top:12px}
/* THE ONE PIXEL-LEVEL CHANGE TO THE LOCKED COMPOSITION, and it is a
   reconciliation rather than a redesign: this row was the only 14px corner on
   the page. Its two neighbours in the same lower grid — a shoot row and a task
   row — are both `--dash-r-row`, and an attention row is the same kind of
   object as either. 14 to 12 on a hover surface is imperceptible; three rows
   in one grid disagreeing about their corner is not. */
.attn__i{display:block;padding:13px 12px;border-radius:var(--dash-r-row);
  text-decoration:none;color:inherit;transition:background 150ms ease}
.attn__i:hover{background:var(--dash-item-hover)}
.attn__i:focus-visible{outline:2px solid var(--dash-info);outline-offset:-2px}
.attn__top{display:flex;justify-content:space-between;gap:12px;
  font-size:var(--dash-fs-title);font-weight:600;color:var(--dash-ink)}
.attn__job{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.attn__when{font-weight:400;color:var(--dash-faint);font-size:var(--dash-fs-meta);
  flex-shrink:0}
.attn__why{display:block;margin-top:4px;font-size:var(--dash-fs-label);
  letter-spacing:.1em;text-transform:uppercase;font-weight:700;
  color:var(--dash-info)}
.attn__why.info{color:var(--dash-info)}
.attn__why.warn,.attn__why.hot{color:var(--dash-status-warn)}
.attn__why.neu,.attn__why.ok{color:var(--dash-muted)}
.attn__sub{display:block;margin-top:3px;font-size:var(--dash-fs-meta);
  color:var(--dash-muted);overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}

/* Morning brief */
.brf{display:flex;flex-direction:column;gap:11px;margin:14px 0 0;padding:0;
  list-style:none;font-size:var(--dash-fs-title)}
.brf__i{display:flex;align-items:flex-start;gap:9px;min-width:0}
.brf__dot{width:7px;height:7px;border-radius:50%;background:var(--dash-dot);
  box-shadow:var(--dash-dot-glow);flex-shrink:0;margin-top:6px;
  animation:dash-pulse 3s ease-in-out infinite}
.brf__i:nth-child(2) .brf__dot{animation-delay:.4s}
.brf__i:nth-child(3) .brf__dot{animation-delay:.8s}
.brf__i:nth-child(4) .brf__dot{animation-delay:1.2s}
.brf__t{min-width:0;color:var(--dash-ink);text-decoration:none;line-height:1.4}
a.brf__t:hover{color:var(--dash-info)}
a.brf__t:focus-visible{outline:2px solid var(--dash-info);outline-offset:2px;
  border-radius:2px}

/* --- the written brief ----------------------------------------------------
   Studio Hill's 6am briefing writes prose. It leads the full brief page and
   contributes one line to the Dashboard card.

   IT IS SET FOR READING, NOT FOR SCANNING. Everything else in this product is
   tuned to be read at a glance from a step back — short lines, tight leading,
   tabular numerals. A paragraph is the opposite job: it wants a measure, a
   looser line height and a serif that does not fight the Cormorant already on
   the page. This is the only prose surface in the admin, and it is the only
   place these values appear.
   ------------------------------------------------------------------------- */

/* The card's one line. Clamped at two so a long opening sentence cannot push
   the task rows off the bottom of a locked card. */
.brf__lead{margin:14px 0 0;font-size:var(--dash-fs-title);line-height:1.5;
  color:var(--dash-ink);display:-webkit-box;-webkit-box-orient:vertical;
  -webkit-line-clamp:2;overflow:hidden}
.brf__lead+.brf{margin-top:12px;padding-top:12px;
  border-top:.5px solid var(--edge-row)}

/* The full prose, on /admin/brief. */
.brf__pw{font-variant-numeric:tabular-nums}
.brf__pbody{margin-top:10px;max-width:68ch}
.brf__pp{margin:0 0 11px;font-size:var(--fs-base);line-height:1.65;
  color:var(--linen)}
.brf__pbody>:last-child{margin-bottom:0}
.brf__ph{margin:16px 0 7px;font-family:var(--body);font-size:var(--fs-sm);
  letter-spacing:.12em;text-transform:uppercase;color:var(--slate);
  font-weight:600}
.brf__pbody>.brf__ph:first-child{margin-top:0}
.brf__pl{margin:0 0 11px;padding:0 0 0 17px;list-style:disc}
.brf__pl li{font-size:var(--fs-base);line-height:1.6;color:var(--linen);
  margin-bottom:4px}
.brf__pl li::marker{color:var(--slate)}
/* `<strong>` is the ONLY tag `NarrativeMarkup` will emit from the source text.
   Toned to the brighter ink rather than given a heavier weight, because a
   paragraph with bold runs in it should stay a paragraph. */
.brf__pbody strong{font-weight:600;color:var(--ink)}

@keyframes dash-pulse{
  0%,100%{opacity:.85}
  50%{opacity:.35}
}

/* --- My Tasks page ------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   NO NEW VISUAL LANGUAGE. Every surface on this screen is a `.dash__card`,
   every row is a `.tsk`, every heading is `.dash__t`. What is below is layout
   and the two controls the Dashboard's read-only card never needed: a tick
   that submits, and a move button.
   --------------------------------------------------------------------------- */

.tasksec{margin-top:22px}
.tasklist{display:flex;flex-direction:column;gap:8px;margin:12px 0 0;padding:0;
  list-style:none}

/* The board. Three equal columns that stack on a narrow workspace, with
   `minmax(0,…)` for the same reason the Dashboard's lower grid has it: a long
   task title must truncate rather than push the grid out. */
/* Clear sits between the column's name and its count, so it reads as part of
   the heading rather than as a fourth thing in the row. `margin-left:auto`
   pushes it right; the count keeps its own place after it. */
.tcol__clear{margin-left:auto;display:flex}

.tboard{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:var(--dash-gap);align-items:start;min-height:0}
.tcol{min-width:0}
.tcol .tsks{margin-top:12px}

/* THE TICK IS A BUTTON NOW.
   The Dashboard drew `.tsk__box` as a span because nothing could be written.
   It is a submit control on both screens today, so it needs a pointer, a
   pressed state and a visible focus ring — and the form around it must not
   introduce a line box of its own. */
.tsk__tick{display:flex;align-items:center;flex-shrink:0}
button.tsk__box{cursor:pointer;padding:0;background:none;
  transition:border-color 140ms var(--ease),background 140ms var(--ease)}
button.tsk__box:hover{border-color:var(--accent);background:var(--accent-dim)}
button.tsk__box:active{transform:scale(.92)}
button.tsk__box:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* A completed task's circle is filled, and its title is struck through. Both,
   because colour alone is not a state. */
button.tsk__box.is-done{background:var(--accent);border-color:var(--accent)}
.tsk--done .tsk__t{text-decoration:line-through;color:var(--slate)}

/* THE METADATA GROUP. Transparent in the list view — `display:contents` means
   the children lay out on the row as if the wrapper were absent — and a second
   line on the board, where a 360px column cannot carry a title and four pieces
   of metadata on one line without the title losing. */
.tsk__meta{display:contents}
.tcol .tsk{flex-wrap:wrap;row-gap:5px}
.tcol .tsk__t{flex:1 1 auto}
.tcol .tsk__meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  flex:1 1 100%;padding-left:27px}
.tcol .tsk__who{margin-left:auto}

/* The Job reference on a task row. A link, in the operational accent, at the
   same size as the other metadata beside it. */
.tsk__job{flex-shrink:0;font-size:var(--dash-fs-small);color:var(--accent);
  font-weight:600;text-decoration:none}
.tsk__job:hover{text-decoration:underline}

/* --- the lists rail ------------------------------------------------------- */
/* Lists are how the studio organises tasks, so they are a permanent surface
   rather than a dropdown inside a form. The rail is a sibling of the board,
   not part of it: the board scrolls its columns, the rail does not move. */
.tlayout{display:grid;grid-template-columns:190px minmax(0,1fr);
  gap:var(--dash-gap);align-items:start;min-height:0}
.tmain{min-width:0}
.trail{display:flex;flex-direction:column;gap:20px;position:sticky;top:0}
.trail__grp{display:flex;flex-direction:column;gap:1px}
.trail__k{font-family:var(--body);font-size:var(--fs-2xs);letter-spacing:.12em;
  text-transform:uppercase;color:var(--slate);padding:0 10px 8px}
.trail__i{display:flex;align-items:center;gap:9px;padding:7px 10px;
  border-radius:var(--chip-radius);font-size:var(--fs-md-plus);color:var(--slate2);
  text-decoration:none;transition:background 140ms var(--ease),color 140ms var(--ease)}
.trail__i:hover{background:var(--surface-row-hover);color:var(--ink)}
.trail__i.on{background:var(--accent-dim);color:var(--ink);font-weight:600}
.trail__i:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
/* The count sits hard right and never competes with the name for width. */
.trail__n{margin-left:auto;font-size:var(--fs-sm);color:var(--slate);
  font-variant-numeric:tabular-nums;flex-shrink:0}
.trail__i.on .trail__n{color:var(--ink)}

/* A dot per list. The tones are the crew palette's, reused rather than a
   second set of colours: a list is a category, and the palette already exists
   to tell categories apart at a glance. */
.trail__dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;
  background:var(--slate)}
.trail__dot--all{background:var(--accent)}
.trail__dot--1{background:var(--hot)}
.trail__dot--2{background:var(--warn)}
.trail__dot--3{background:var(--info)}
.trail__dot--4{background:var(--ok)}
.trail__dot--5{background:var(--crew-violet)}
.trail__av{width:20px;height:20px;border-radius:50%;flex-shrink:0;
  display:inline-flex;align-items:center;justify-content:center;
  background:var(--panel2);border:1px solid var(--line);
  font-size:var(--fs-micro);font-weight:700;color:var(--slate2);
  text-transform:uppercase}

/* --- renaming a list ------------------------------------------------------ */
/* The group heading carries its own toggle. It sits on the baseline of the
   "LISTS" caption rather than becoming a button, because renaming is not an
   operation on the same footing as filtering — it is the small print of the
   rail, and it is styled as such until it is wanted. */
.trail__k{display:flex;align-items:baseline;gap:8px}
.trail__k-a{margin-left:auto;letter-spacing:.06em;color:var(--slate2);
  text-decoration:none;transition:color 140ms var(--ease)}
.trail__k-a:hover{color:var(--accent)}
.trail__k-a:focus-visible{outline:2px solid var(--accent);outline-offset:2px;
  border-radius:2px}

/* A rename row occupies the same box as the row it replaces, to the pixel: the
   filter row measures 31px tall, and 4px of padding either side of a 21px tick
   plus the 1px border this one carries comes to the same 31. The mode changes
   what the rail does, not where anything on it sits. */
.trail__ren{display:flex;align-items:center;gap:9px;padding:4px 8px;
  border-radius:var(--chip-radius);background:var(--surface-row);
  border:1px solid var(--line)}
.trail__ren:focus-within{border-color:var(--accent)}
.trail__name{flex:1 1 auto;min-width:0;background:none;border:0;outline:0;
  padding:0;font-family:var(--body);font-size:var(--fs-md-plus);
  color:var(--ink)}
/* The tick is the same affordance as a card's, at the size the rail can spare.
   It stays visible rather than appearing on hover: in a mode built for typing,
   a control that has to be found by mousing is a control that gets missed. */
.trail__save{flex-shrink:0;width:21px;height:21px;padding:0;border:0;
  border-radius:var(--chip-radius);background:none;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;
  color:var(--slate2);transition:background 140ms var(--ease),color 140ms var(--ease)}
.trail__save svg{width:13px;height:13px;stroke:currentColor}
.trail__save:hover{background:var(--accent-dim);color:var(--accent)}
.trail__save:focus-visible{outline:2px solid var(--accent);outline-offset:-1px}

/* Delete sits beside the tick, at the same size, and only turns warm on
   hover — a row of red crosses down the rail would read as five problems
   rather than five lists. */
.trail__kill{flex-shrink:0;width:21px;height:21px;padding:0;border:0;
  border-radius:var(--chip-radius);background:none;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;
  color:var(--slate);transition:background 140ms var(--ease),color 140ms var(--ease)}
.trail__kill svg{width:12px;height:12px;stroke:currentColor}
.trail__kill:hover{background:var(--hot-dim);color:var(--hot)}
.trail__kill:focus-visible{outline:2px solid var(--hot);outline-offset:-1px}

/* The add row is a rename row that has nothing to rename: same box, same dot,
   same field, so it does not read as a different kind of thing. */
.trail__add{display:flex;align-items:center;gap:9px;padding:4px 8px;
  border-radius:var(--chip-radius);background:var(--surface-row);
  border:1px dashed var(--line2);margin-top:5px}
.trail__add:focus-within{border-style:solid;border-color:var(--accent)}
.trail__add .trail__name::placeholder{color:var(--slate)}

/* --- the week's progress -------------------------------------------------- */
.tweek{padding:14px;border-radius:var(--card-radius);
  background:var(--surface-card);border:1px solid var(--edge-card);
  -webkit-backdrop-filter:blur(var(--dash-blur));
  backdrop-filter:blur(var(--dash-blur))}
.tweek .trail__k{padding:0 0 8px}
.tweek__none{font-size:var(--fs-sm-plus);color:var(--slate);line-height:1.5}
.tweek__line{font-size:var(--fs-md-plus);color:var(--ink)}
.tweek__figure{display:flex;align-items:baseline;gap:8px;margin-top:6px}
/* Cormorant, at the KPI numeral's size and weight. The tracker is a number
   worth reading from a step back, and this is the face this product uses when
   a number is the point. */
.tweek__pc{font-family:var(--serif);font-size:var(--dash-fs-figure);font-weight:400;
  line-height:1;color:var(--accent)}
.tweek__togo{font-family:var(--mono);font-size:var(--fs-sm);color:var(--slate)}
.tweek__bar{margin-top:10px;height:6px;border-radius:var(--pill-radius);
  background:var(--panel2);overflow:hidden}
.tweek__fill{display:block;height:100%;border-radius:var(--pill-radius);
  background:var(--accent);transition:width 400ms var(--ease)}

/* THE BAR'S WIDTH, AS CLASSES, 0 TO 100.
   ---------------------------------------------------------------------------
   The Content-Security-Policy is `style-src 'self'` with no `unsafe-inline`, so
   a percentage cannot be written as a style attribute — it would be dropped and
   the bar would render empty. Every step is generated rather than rounded to
   fives, so the bar and the number beside it always agree: a tracker reading
   "83%" above a bar drawn at 85 is worse than no bar.

   This is the same mechanism `.s-t-N` and `.s-h-N` use for the Schedule, for
   the same reason. */
.s-w-0{width:0%}
.s-w-1{width:1%}
.s-w-2{width:2%}
.s-w-3{width:3%}
.s-w-4{width:4%}
.s-w-5{width:5%}
.s-w-6{width:6%}
.s-w-7{width:7%}
.s-w-8{width:8%}
.s-w-9{width:9%}
.s-w-10{width:10%}
.s-w-11{width:11%}
.s-w-12{width:12%}
.s-w-13{width:13%}
.s-w-14{width:14%}
.s-w-15{width:15%}
.s-w-16{width:16%}
.s-w-17{width:17%}
.s-w-18{width:18%}
.s-w-19{width:19%}
.s-w-20{width:20%}
.s-w-21{width:21%}
.s-w-22{width:22%}
.s-w-23{width:23%}
.s-w-24{width:24%}
.s-w-25{width:25%}
.s-w-26{width:26%}
.s-w-27{width:27%}
.s-w-28{width:28%}
.s-w-29{width:29%}
.s-w-30{width:30%}
.s-w-31{width:31%}
.s-w-32{width:32%}
.s-w-33{width:33%}
.s-w-34{width:34%}
.s-w-35{width:35%}
.s-w-36{width:36%}
.s-w-37{width:37%}
.s-w-38{width:38%}
.s-w-39{width:39%}
.s-w-40{width:40%}
.s-w-41{width:41%}
.s-w-42{width:42%}
.s-w-43{width:43%}
.s-w-44{width:44%}
.s-w-45{width:45%}
.s-w-46{width:46%}
.s-w-47{width:47%}
.s-w-48{width:48%}
.s-w-49{width:49%}
.s-w-50{width:50%}
.s-w-51{width:51%}
.s-w-52{width:52%}
.s-w-53{width:53%}
.s-w-54{width:54%}
.s-w-55{width:55%}
.s-w-56{width:56%}
.s-w-57{width:57%}
.s-w-58{width:58%}
.s-w-59{width:59%}
.s-w-60{width:60%}
.s-w-61{width:61%}
.s-w-62{width:62%}
.s-w-63{width:63%}
.s-w-64{width:64%}
.s-w-65{width:65%}
.s-w-66{width:66%}
.s-w-67{width:67%}
.s-w-68{width:68%}
.s-w-69{width:69%}
.s-w-70{width:70%}
.s-w-71{width:71%}
.s-w-72{width:72%}
.s-w-73{width:73%}
.s-w-74{width:74%}
.s-w-75{width:75%}
.s-w-76{width:76%}
.s-w-77{width:77%}
.s-w-78{width:78%}
.s-w-79{width:79%}
.s-w-80{width:80%}
.s-w-81{width:81%}
.s-w-82{width:82%}
.s-w-83{width:83%}
.s-w-84{width:84%}
.s-w-85{width:85%}
.s-w-86{width:86%}
.s-w-87{width:87%}
.s-w-88{width:88%}
.s-w-89{width:89%}
.s-w-90{width:90%}
.s-w-91{width:91%}
.s-w-92{width:92%}
.s-w-93{width:93%}
.s-w-94{width:94%}
.s-w-95{width:95%}
.s-w-96{width:96%}
.s-w-97{width:97%}
.s-w-98{width:98%}
.s-w-99{width:99%}
.s-w-100{width:100%}

/* --- the card menu -------------------------------------------------------- */
/* A `<details>`, like every other menu in this application: it opens, closes
   and is keyboard-navigable with no JavaScript. Drag-and-drop is the
   enhancement; this is the path that always works. */
.tsk__menu{position:relative;display:inline-flex;flex-shrink:0;margin-left:2px}
.tsk__menu>summary{list-style:none;cursor:pointer}
.tsk__menu>summary::-webkit-details-marker{display:none}
.tsk__more{display:inline-flex;align-items:center;justify-content:center;
  width:24px;height:24px;border-radius:var(--chip-radius);color:var(--slate);
  transition:background 140ms var(--ease),color 140ms var(--ease)}
.tsk__more:hover{background:var(--surface-row-hover);color:var(--ink)}
.tsk__more svg{width:14px;height:14px}
.tsk__menu>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.tsk__menuitems{position:absolute;right:0;top:calc(100% + 6px);z-index:30;
  display:flex;flex-direction:column;min-width:200px;padding:5px;
  background:var(--panel);border:1px solid var(--line2);
  border-radius:var(--row-radius);box-shadow:var(--elev-card)}
.tsk__menuitems form{display:contents}
.tsk__menuitem{display:flex;align-items:center;gap:9px;padding:8px 10px;width:100%;
  border-radius:var(--chip-radius);text-align:left;white-space:nowrap;
  font-family:var(--body);font-size:var(--fs-sm-plus);color:var(--ink);
  text-decoration:none;transition:background 140ms var(--ease)}
.tsk__menuitem svg{width:13px;height:13px;stroke:currentColor;fill:none;flex-shrink:0}
.tsk__menuitem:hover{background:var(--surface-row-hover)}
.tsk__menuitem:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.tsk__menuitem--warn{color:var(--hot)}
.tsk__menuitem--warn:hover{background:var(--hot-dim)}

/* --- dragging ------------------------------------------------------------- */
/* The card is the handle, so the whole thing takes the grab cursor. Only on a
   real pointer: on a touch screen there is no drag and the menu is the way. */
@media (hover:hover) and (pointer:fine){
  .tcol [data-task-card]{cursor:grab}
  .tcol [data-task-card]:active{cursor:grabbing}
}
.tcol [data-task-card].is-dragging{opacity:.45}
/* The column being dropped into. Never the one the card came from — a card
   dropped where it started is not a move, and the JS does not light it. */
.tcol__drop.is-drop{outline:2px dashed var(--ring-hover);outline-offset:4px;
  border-radius:var(--row-radius)}
.tcol__drop{min-height:44px;list-style:none}
.tcol__empty{font-size:var(--fs-sm-plus);color:var(--slate);padding:10px 2px}

/* The create / edit form. One form for both, so the field list lives once. */
/* --- Settings: Team & Access ----------------------------------------------
   Built from the pieces the Dashboard established: `.dash__card` for the
   panel, `.fld` for the controls, `.filt` for the row actions. Nothing here
   introduces a colour, a radius or a shadow.
   ------------------------------------------------------------------------- */
.team{display:flex;flex-direction:column;gap:8px;margin:14px 0 0;padding:0;
  list-style:none}
.team__i{display:grid;
  grid-template-columns:auto minmax(0,2fr) minmax(0,1.2fr) auto minmax(0,1fr) auto;
  align-items:center;gap:14px;padding:12px 14px;
  border-radius:var(--row-radius);background:var(--surface-row);
  border:1px solid var(--edge-row);transition:background 140ms var(--ease)}
.team__i:hover{background:var(--surface-row-hover)}
/* A deactivated row stays legible and stops competing. It is history, not a
   warning: the person is meant to still be findable. */
.team__i--off{opacity:.62}
.team__av{width:30px;height:30px;border-radius:50%;flex-shrink:0;
  display:inline-flex;align-items:center;justify-content:center;
  background:var(--panel2);border:1px solid var(--line);
  font-size:var(--fs-sm);font-weight:700;color:var(--slate2);
  text-transform:uppercase}
.team__id{display:flex;flex-direction:column;gap:2px;min-width:0}
.team__n{display:flex;align-items:center;gap:7px;font-size:var(--fs-lg);
  color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.team__you{font-size:var(--fs-micro);letter-spacing:.14em;text-transform:uppercase;
  color:var(--accent);border:1px solid var(--accent-dim);
  border-radius:var(--pill-radius);padding:1px 7px;flex-shrink:0}
.team__e{font-size:var(--fs-sm-plus);color:var(--slate);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.team__role{display:flex;flex-direction:column;gap:2px;min-width:0;
  font-size:var(--fs-md-plus);color:var(--linen)}
.team__cap{font-size:var(--fs-micro);letter-spacing:.1em;text-transform:uppercase;
  color:var(--slate)}
/* The status is a `.st` — the Register's own vocabulary — so "Active" reads
   here exactly as a stage does there. No rule is needed for it. */
.team__i>.st{white-space:nowrap}
.team__when{font-size:var(--fs-sm);color:var(--slate);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.team__acts{display:flex;align-items:center;gap:6px;flex-shrink:0}
.team__form{display:contents}
.team__foot{margin:14px 0 0;font-size:var(--fs-sm-plus);color:var(--slate);
  line-height:1.5}

.teamform{margin-top:var(--dash-gap)}
.teamform__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:12px;margin-top:14px}
.teamform__acts{display:flex;justify-content:flex-end;margin-top:16px}
.teamform__note{margin:12px 0 0;font-size:var(--fs-sm-plus);color:var(--slate);
  line-height:1.55;max-width:64ch}
.teamform__check{display:flex;align-items:center;gap:9px;
  font-size:var(--fs-lg);color:var(--ink)}
.fld--check{justify-content:center}

@media (max-width:1250px){
  /* The row loses its two least urgent columns first: when somebody last
     signed in, and their capability caption. Name, email, role, status and
     the actions are what the screen is for. */
  .team__i{grid-template-columns:auto minmax(0,2fr) minmax(0,1fr) auto auto}
  .team__when{display:none}
}
@media (max-width:900px){
  .team__i{grid-template-columns:auto minmax(0,1fr);gap:10px 14px}
  .team__role,.team__i>.st,.team__acts{grid-column:2}
  .teamform__grid{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:640px){
  .teamform__grid{grid-template-columns:minmax(0,1fr)}
}

.taskform{margin-top:var(--dash-gap)}
.taskform__grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  gap:12px;margin-top:14px}
.taskform__grid .fld--wide{grid-column:1 / -1}
.taskform__acts{display:flex;justify-content:flex-end;margin-top:16px}
.taskform__archive{margin-top:12px;padding-top:12px;
  border-top:1px solid var(--dash-item-border)}
.lbl__opt{color:var(--slate);font-weight:400}

/* THE RAIL LIES DOWN BEFORE THE BOARD GIVES UP A COLUMN.
   ---------------------------------------------------------------------------
   Measured at 1280: the workspace is 1060, the rail takes 190 and a gap, and
   the three columns are left with 262px each — enough that a task title
   truncates on every card. Three columns is the whole point of the board, so
   the rail is what moves: below 1400 it becomes a horizontal strip of the same
   links above the board, which costs one row of height and returns 208px of
   width to the columns.

   The week tracker goes with it and sits at the end of the strip, because a
   progress bar is the one thing on the rail that is not a filter. */
@media (max-width:1400px){
  .tlayout{grid-template-columns:minmax(0,1fr)}
  .trail{position:static;flex-direction:row;flex-wrap:wrap;align-items:center;
    gap:8px 18px}
  .trail__grp{flex-direction:row;flex-wrap:wrap;align-items:center;gap:4px}
  .trail__k{padding:0 4px 0 0}
  .trail__i{padding:6px 11px;border-radius:var(--pill-radius);
    border:1px solid var(--line);background:var(--glass)}
  .trail__n{margin-left:4px}
  /* Rename rows keep their pill shape and a FIXED width. Sharing the strip's
     width between them gave each one about 90px, narrower than the names
     being typed into them; letting them grow to fill it made whichever list
     wrapped onto its own line twice the width of the four above it. 220px
     holds the longest of the studio's names with room to type past it. */
  .trail__ren,.trail__add{border-radius:var(--pill-radius);flex:0 1 220px}
  .trail__add{margin-top:0}
  .trail__k-a{margin-left:0}
  /* The tracker becomes one line: the figure beside the sentence rather than
     under it, and the bar spans what is left. */
  .tweek{display:flex;align-items:center;gap:14px;flex:1 1 260px;
    padding:10px 14px}
  .tweek .trail__k{padding:0}
  .tweek__figure{margin-top:0}
  .tweek__pc{font-size:var(--dash-fs-heading)}
  .tweek__bar{margin-top:0;flex:1 1 80px;min-width:60px}
}

@media (max-width:1150px){
  .tboard{grid-template-columns:minmax(0,1fr)}
  .taskform__grid{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:720px){
  .taskform__grid{grid-template-columns:minmax(0,1fr)}
}

/* --- Motion, and its absence --------------------------------------------- */

@media (prefers-reduced-motion:reduce){
  /* Fewer and gentler, not none: the theme crossfade stays because it aids
     comprehension; every ambient movement goes. */
  .dash__blob{animation:none}
  .brf__dot{animation:none}
  .stat:hover,.dash__cta:hover,a.shot:hover{transform:none}
}

/* --- Responsive ---------------------------------------------------------- */

/* THE HEADER GIVES THINGS UP IN ORDER, and the search is not first.
   Sun times go, then the search narrows, then the weather pill goes entirely —
   the greeting and the two controls are what the header is for. */
@media (max-width:1500px){
  .dash__greet{font-size:var(--dash-fs-display-sm)}
}
/* THE HEADER GIVES THINGS UP IN A MEASURED ORDER.
   ---------------------------------------------------------------------------
   Every threshold below is the width at which the row actually stops fitting,
   read out of the browser rather than picked. At full content the row needs
   1397px of header, and the header is the viewport less the 220px sidebar and
   56px of workspace padding — so sunrise/sunset only fit from 1673 up, and the
   breakpoint is 1700.

   These numbers moved once already, and the reason is worth keeping: the pill
   used to absorb a squeeze by WRAPPING to a second line, which is why it stood
   56px tall beside two 44px controls and why the same 20px corner read as
   three different shapes. `white-space:nowrap` fixed the shape and turned the
   vertical overflow into a horizontal one — the pill now demands its width
   instead of quietly growing, so the widths had to be honest.

   Order of sacrifice: the search narrows first (`flex:0 1 250px`), then
   sunrise/sunset, then the condition, and last the pill itself. A place, a
   temperature and the golden hour are what a photographer opens this for. */
@media (max-width:1700px){
  .wx__times{display:none}
}
@media (max-width:1500px){
  .wx__cond{display:none}
}
/* 1300, not 1250. At 1260 the row came out three pixels over its header — the
   kind of miss that shows up as a scrollbar and nothing else. */
@media (max-width:1300px){
  .wx{display:none}
}
/* THE 6:3:3 GRID HOLDS DOWN TO 1150.
   Measured in the browser at a 1280 viewport: the three panels land at
   484 / 242 / 242, with no horizontal overflow. The brief keeps a readable
   measure — its lead line clamps at two lines there — and the two lists still
   fit a title and its meta without wrapping.

   BELOW THAT THE BRIEF SPANS AND THE LISTS PAIR UP. It is the FIRST child now
   rather than the last, so the rule follows it — a `:last-child` here would
   have spanned Needs Attention and left the paragraph in a half-width column,
   which is the opposite of what the widths are for. */
@media (max-width:1150px){
  .dash__grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
  .dash__grid>*:first-child{grid-column:1 / -1}
}
/* THE FOUR-CARD SUMMARY ROW COLLAPSES AT ONE WIDTH, NOT TWO.
   ---------------------------------------------------------------------------
   The Job Register's KPIs dropped to 2x2 below 1320 while the Dashboard's stat
   row held four across down to 1024 — so on an ordinary 1280 laptop the same
   four-card row was drawn two different ways one navigation click apart. Both
   now break at 1024. Measured at 1280: four KPI cards land at 240px each, which
   carries a numeral and its label without wrapping. */
@media (max-width:1024px){
  .kpis{grid-template-columns:repeat(2,1fr)}
}

@media (max-width:1024px){
  .dash__hdr{flex-wrap:wrap}
  .dash__greet{white-space:normal}
  .dash__stats{grid-template-columns:repeat(2,minmax(0,1fr))}
  .dash__mid{grid-template-columns:minmax(0,1fr)}
  .dash__today{grid-template-columns:minmax(0,1fr)}
  .dash__upnext{border-left:0;border-top:1px solid var(--dash-glass-border);
    padding-left:0;padding-top:18px}
  .shoots{grid-template-columns:minmax(0,1fr)}
  .dash__grid{grid-template-columns:minmax(0,1fr)}
  .dash__grid>*:first-child{grid-column:auto}
}

/* ===========================================================================
   THE CLIENT PORTAL — the same product, with fewer modules
   ---------------------------------------------------------------------------
   NOT A SECOND DESIGN SYSTEM. The shell is `.app` and `.main`; the rail is
   `.sb` and every class inside it; the ground is `.dash__bloom`; the surfaces
   are `.dash__card`; the rows borrow `.shot`'s geometry; the statuses are
   `.st`; the actions are `.btn`. All of it already existed.

   What is below is (1) the phone behaviour the admin never needed, because the
   admin is a desktop tool, and (2) the composition of screens that do not exist
   on the admin side. No colour, no radius, no easing and no type size is
   introduced — every value is a token that was already in the sheet.
   =========================================================================== */

/* --- The shell ----------------------------------------------------------- */
/* `.app` is `height:100vh; overflow:hidden`, which is right for an operator who
   lives in one screen and wrong for a client reading a list on a phone. The
   portal scrolls the document instead. */
/* THE RAIL IS THE VIEWPORT'S HEIGHT AND THE PAGE SCROLLS INSIDE `.main`.
   ---------------------------------------------------------------------------
   This was `height:auto` with the whole document scrolling, which on a long
   page — Book a Shoot is the worst of them — carried the rail's own column
   down with the content until Theme, the contact and Sign out were below the
   fold with no way back to them but scrolling up.
   
   It is structural, not positional: `.app`'s grid row is already
   `minmax(0,1fr)`, `.sb` is already `flex-direction:column`, and `.sb nav` is
   already `flex:1; min-height:0; overflow-y:auto`. All that was missing was
   capping the shell at the viewport so those three facts could do their work.
   
   `100dvh` with a `100vh` fallback: on a phone `vh` is the LARGEST viewport,
   so the browser chrome eats the bottom of the rail — which is exactly where
   Sign out lives. */
/* THE CLIENT WORKSPACE MEASURE, declared once.
   ---------------------------------------------------------------------------
   The top rail and the content column have to agree about where the right edge
   is, or the one control in the rail floats free of everything under it. They
   used to hold the numbers separately: the workspace capped at 1240 and the
   rail did not, so above ~1280px the button drifted right — 74px of it at
   1542. Two values for one grid is the drift a token exists to prevent. */
.app--pt{height:100vh;height:100dvh;overflow:hidden;
  --pt-measure:1240px;
  --pt-gutter:clamp(18px,2.4vw,40px)}
.app--pt .main{height:100%;overflow-y:auto;overflow-x:hidden}
.pt__ws{height:auto;overflow:visible;max-width:var(--pt-measure);
  padding:clamp(26px,3vw,40px) var(--pt-gutter) 72px;gap:0}
/* The footer never gives up its height to a long navigation. */
.sb__theme,.sb__user,.sb__out{flex-shrink:0}

.pt__menu{display:none}
.pt__scrim{display:none}
/* The Admin phone band, like the two above it: drawn only under 860px. */
.adtop{display:none}

/* --- Home ---------------------------------------------------------------- */
/* THE GREETING IS THE DASHBOARD'S: Cormorant at display size, the name in the
   one synthesised italic this product has. Two lines rather than the admin's
   single elided one — a client reads it once a month and it is the most
   personal thing on the page, where the Dashboard shares its row with a clock
   and the weather. */
/* --- THE WORKSPACE TOP RAIL ----------------------------------------------- */

/* A slim band across the WORKSPACE, starting where the navigation rail ends —
   so the two read as two surfaces rather than one bar spanning the window.

   It exists to frame. Without it the greeting began in open space and the page
   had no top edge, which is what made everything below feel like it was
   floating rather than sitting in something.

   Not a navbar: no links, no search, no menus. Whose ImageBooth this is, the
   one client action, and a hairline. */
/* STICKY, because `.main` is what scrolls in the client shell: a rail that
   scrolled away would frame the top of the document rather than the top of the
   workspace, which is the opposite of what it is for.

   The product's own glass — a translucent ground over the blurred bloom, the
   same treatment the phone's menu button uses — rather than a flat slab, so
   the page still shows through it faintly and it reads as a surface rather
   than as a bar bolted on. */
/* The SURFACE spans the whole workspace — the band and its hairline are what
   frame the page, and a rail that stopped at the measure would be a card. Only
   the CONTENTS take the measure, in `.pttop__in`. */
.pttop{position:sticky;top:0;z-index:3;flex-shrink:0;
  display:flex;height:68px;padding:0;
  background:color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter:blur(var(--dash-blur));
  -webkit-backdrop-filter:blur(var(--dash-blur));
  border-bottom:1px solid var(--dash-item-border)}
/* The same box as `.pt__ws`: same maximum, same gutters, left-aligned the same
   way — so the CTA's right edge and the hero image's right edge are the one
   line, at every width. */
.pttop__in{display:flex;align-items:center;gap:16px;
  width:100%;max-width:var(--pt-measure);height:100%;
  padding:0 var(--pt-gutter)}
.pttop__c{font-size:var(--fs-xs);letter-spacing:.24em;text-transform:uppercase;
  color:var(--dash-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Greyscale is a decision, not a style: it means a client's brand colours
   cannot enter ImageBooth's palette however strong the artwork is. Beside their
   name rather than over the photograph, which is the one thing on this page
   that should not be competed with. */
.pttop__mark{height:22px;width:auto;max-width:150px;object-fit:contain;
  opacity:.3;filter:grayscale(1);flex-shrink:0}
.pttop__cta{margin-left:auto;flex-shrink:0}

/* --- THE DASHBOARD HERO --------------------------------------------------- */

/* Two columns on one grid: the editorial column and the photograph, both
   inside the workspace measure. Nothing bleeds — the hero used to reach the
   window edge, which is what made the picture the subject of the screen
   instead of a feature on it.

   It closes with a hairline under everything, counters included, so the whole
   composition reads as one block rather than as parts with rules between
   them. */
.pthero{position:relative;margin:0 0 clamp(28px,3.4vw,44px);
  padding:0 0 clamp(22px,2.6vw,32px);
  border-bottom:1px solid var(--dash-item-border)}
/* TOP-ALIGNED, not centred. The photograph rises to the panel's own top edge
   instead of floating in the middle of the row — which is what made it read as
   placed ON the hero rather than as part of it. */
.pthero__in{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,0.86fr);
  align-items:start;gap:clamp(24px,4vw,64px)}
/* With no photograph the editorial column takes the width and the measure
   keeps it readable — no empty second column holding the page open. */
.pthero:not(.pthero--photo) .pthero__in{grid-template-columns:minmax(0,1fr)}
/* Above the photograph in the stacking order, so the plate can bleed left
   underneath the greeting rather than pushing it aside. */
.pthero__lead{position:relative;z-index:1;max-width:46ch;min-width:0}

/* The date as an editorial device, with the conditions beside it. */
.pthero__meta{display:flex;align-items:center;flex-wrap:wrap;gap:14px 30px;
  margin:26px 0 0}
.pthero__date{display:flex;align-items:center;gap:12px}
/* THE DATE NUMERAL takes the accent. It and the three counters are the only
   numbers on this page that are ANCHORS — things the eye lands on first — and
   giving those one shared colour is what makes the hierarchy read without
   turning the page into a KPI board. Everything measured rather than counted
   (the temperature, the conditions, the place) stays neutral, because colouring
   those would say they are the same kind of fact, and they are not. */
.pthero__day{font-family:var(--serif);font-size:var(--dash-fs-display-sm);
  line-height:.9;color:var(--accent);font-variant-numeric:tabular-nums}
.pthero__when{display:flex;flex-direction:column;gap:2px}
.pthero__dow{font-size:var(--fs-xs);letter-spacing:.2em;text-transform:uppercase;
  color:var(--dash-muted)}
.pthero__mon{font-size:var(--dash-fs-small);color:var(--dash-faint)}
/* Compact by design: presentation information beside a date, not a widget. */
.pthero__wx{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;
  font-size:var(--dash-fs-small);color:var(--dash-muted)}
.pthero__t{font-family:var(--serif);font-size:var(--dash-fs-title);
  color:var(--dash-ink)}
.pthero__sep{color:var(--dash-faint)}

/* THE IMAGE OF THE WEEK. A landscape plate, contained and quiet.
   ---------------------------------------------------------------------------
   The mask is what makes this editorial rather than a picture in a box: all
   four edges dissolve into the page ground, so the photograph arrives and
   leaves instead of being cropped by a rectangle. With the opacity below it,
   the image reads as atmosphere behind the greeting's weight — present, not
   competing. */
/* It reaches ABOVE and LEFT of its own grid cell.
   ---------------------------------------------------------------------------
   Up by exactly the workspace's top padding, so the picture meets the top
   rail's hairline and the hero panel has no gap above it.

   Left by a clamp, into the greeting's column. The faded end of the mask is
   what travels there, so nothing legible is covered — and it is the overlap
   that makes the photograph feel integrated with the panel rather than parked
   in the right-hand half of a two-column grid. `.pthero__lead` carries the
   z-index that keeps the type on top. */
.pthero__plate{display:flex;flex-direction:column;gap:14px;
  min-width:0;text-decoration:none;
  margin-top:calc(-1 * clamp(26px,3vw,40px));
  margin-left:calc(-1 * clamp(48px,9vw,168px))}
.pthero__art{display:block;position:relative;aspect-ratio:2/1;
  overflow:hidden;min-width:0}
/* DIRECTIONAL, LEFT TO RIGHT — not a vignette.
   ---------------------------------------------------------------------------
   This was a radial mask dissolving all four edges, and the result read as a
   photograph floating in the middle of nowhere. The composition it belongs to
   does something else entirely: an editorial surface on the left, a controlled
   transition, and a photograph ANCHORED to the right. A vignette has no
   direction, so it cannot express that — it softens the very edge the picture
   is supposed to be held by.

   So the ramp runs one way. The left dissolves into the page ground under the
   greeting's column; the middle carries the transition; the right two-fifths
   arrive at full strength and keep their edge, which is what makes the image
   feel placed rather than floated.

                    0 – 4%    gone, the ground alone
                    4 – 26%   heavily faded
                   26 – 46%   the transition
                   46 – 62%   resolving
                   62 – 100%  intact, and the right edge stays an edge

   Vertically it does nothing at all: the top and bottom are deliberately left
   as clean edges, because fading them is what turned this into a vignette in
   the first place. The caption sits BELOW the plate on the page ground, so
   there is nothing for a bottom fade to integrate with.

   Opacity is now .92 rather than .55. The mask does the fading, and dimming
   the whole element on top of it was fighting the same job twice — it is what
   kept the right-hand side looking washed when it was meant to be the strong
   part of the picture. */
.pthero__img{width:100%;height:100%;object-fit:cover;display:block;
  opacity:.92;transition:opacity 240ms var(--ease);
  -webkit-mask-image:linear-gradient(to right,
    transparent 4%, var(--mask-soft) 26%,
    var(--mask-strong) 46%, var(--mask-solid) 62%);
  mask-image:linear-gradient(to right,
    transparent 4%, var(--mask-soft) 26%,
    var(--mask-strong) 46%, var(--mask-solid) 62%)}
.pthero__plate:hover .pthero__img{opacity:1}

/* The caption is a PLATE CAPTION, ranged right to the image's own edge.
   ---------------------------------------------------------------------------
   The plate's right edge is the photograph's right edge — the art fills it —
   so ranging right here puts the caption on the same vertical line the image
   is anchored to, and on the same line as the Book a shoot control two rows
   above it. Left-ranged it hung off the faded end of the picture, which is the
   end that is dissolving into the page.

   Finer than it was, on both lines. A plate caption is not body copy: it is
   read once, beside the thing it names, and at editorial scale it should sit
   under the photograph rather than compete with it.

   ONE of the two goes below the 10px content floor and it is the one allowed
   to. `IMAGE OF THE WEEK` is all-caps and letterspaced, which is the exemption
   the floor rule already carries — caps have no descenders to lose and the
   tracking does the legibility work. The place line is sentence case and a
   real word a client reads, so it stays ABOVE the floor at 10.5px. Smaller,
   but not smaller than a person can read. */
.pthero__cap{display:flex;flex-direction:column;align-items:flex-end;gap:4px;
  min-width:0;text-align:right}
.pthero__capk{font-size:var(--fs-micro);letter-spacing:.3em;text-transform:uppercase;
  color:var(--dash-faint);transition:color 140ms var(--ease)}
.pthero__capv{font-size:var(--fs-xs);letter-spacing:.02em;color:var(--dash-muted);
  transition:color 140ms var(--ease)}
.pthero__plate:hover .pthero__capk,.pthero__plate:hover .pthero__capv{color:var(--dash-ink)}
.pthero__plate:focus-visible{outline:2px solid var(--dash-info);outline-offset:4px}

/* The counters in their own horizontal zone, above the hero's closing line
   rather than with a rule running through them.

   Scoped to `.pthero` deliberately: `.ptstat` carries its own rule top and
   bottom and is declared LATER in this sheet, so a single-class override loses
   on source order and the counters keep the two lines they are supposed to
   have shed. Two classes wins it without moving either block. */
.pthero .pthero__stat{margin:clamp(14px,2vw,26px) 0 0;padding:0;
  border-top:0;border-bottom:0}

/* --- The Awaiting Action queue -------------------------------------------- */

/* One list, several domains. It is the Register's own row language — the same
   hairline rows, the same hover — because these are the same operator's work,
   not a second dashboard. */
.actq{display:flex;flex-direction:column}
.actq__i{display:flex;align-items:center;gap:18px;
  padding:14px 18px;border-bottom:1px solid var(--line2);
  text-decoration:none;transition:background 140ms var(--ease)}
.actq__i:last-child{border-bottom:0}
.actq__i:hover{background:var(--panel2)}
.actq__i:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
/* The reason, set as a fixed-width label so the eye can group down the column
   without the list needing headings. */
.actq__k{flex:0 0 138px;font-size:var(--fs-xs);letter-spacing:.16em;
  text-transform:uppercase;color:var(--slate2)}
.actq__b{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto}
.actq__t{font-size:var(--fs-base);color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.actq__m{font-size:var(--fs-sm);color:var(--slate2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* What to do about it, at the end of the row where an action belongs. */
.actq__n{flex:0 0 auto;font-size:var(--fs-sm);color:var(--slate2);
  text-align:right;white-space:nowrap}

@media (max-width:820px){
  .actq__i{flex-wrap:wrap;gap:6px 14px;padding:13px 14px}
  .actq__k{flex:0 0 100%}
  .actq__n{flex:0 0 100%;text-align:left}
}

/* --- The client drawer's Dashboard tab ------------------------------------ */

/* Three settings, each in its own block with a heading, a sentence of context
   and its controls. Ordinary forms — the whole panel works with scripting off,
   like every other write in this drawer. */
.dsec{padding:18px 0;border-bottom:1px solid var(--dash-item-border)}
/* The client's aerial pin, shown on a checkerboard so its transparency reads. */
.dpin{display:inline-block;margin:0 0 12px;padding:12px;border-radius:8px;border:1px solid var(--dash-item-border);background:repeating-conic-gradient(rgba(0,0,0,.07) 0 25%,transparent 0 50%) 0 0/16px 16px}
.dpin__img{display:block;max-width:160px;max-height:96px;width:auto;height:auto}
.dsec:last-child{border-bottom:0}
.dsec__h{display:flex;align-items:baseline;justify-content:space-between;
  gap:14px;margin:0 0 6px}
.dsec__s{font-size:var(--fs-xs);letter-spacing:.16em;text-transform:uppercase;
  color:var(--dash-faint)}
.dform{display:flex;flex-direction:column;gap:12px;margin:14px 0 0}
.dform__go{display:flex;align-items:center;gap:14px;flex-wrap:wrap}

/* --- FROM STUDIO HILL ----------------------------------------------------- */

/* One current note, set as an editorial aside rather than a panel.
   ---------------------------------------------------------------------------
   It was a filled, bordered, rounded card — which is the shape a product uses
   for a NOTIFICATION: something addressed at you, with an action implied and
   usually a dismiss in the corner. This is none of those. It is a line from
   the photographer, and it has no controls at all because there is nothing in
   the model to press.

   So the box goes and a rule takes its place. A single hairline down the left
   is the oldest way of marking a passage as somebody's voice, it needs no
   surface of its own, and it cannot be mistaken for something to click. The
   inset comes down with it — a card's padding is furniture the rule does not
   need.

   Named `ptmsg`, not `ptnote`: `.ptnote` is already the project detail's prose
   paragraph, and a second meaning for one class restyles the first. */
.ptmsg{position:relative;align-self:stretch;margin:0 0 clamp(24px,3vw,34px);
  padding:2px 0 2px clamp(16px,1.8vw,22px);
  border-left:2px solid var(--dash-item-border)}
.ptmsg__k{display:block;margin:0 0 8px;
  font-size:var(--fs-xs);letter-spacing:.26em;text-transform:uppercase;
  color:var(--dash-faint)}
.ptmsg__t{font-family:var(--serif);font-size:var(--dash-fs-title);font-weight:400;
  color:var(--dash-ink);margin:0 0 6px}
.ptmsg__b p{margin:0 0 7px;max-width:68ch;
  font-size:var(--fs-base);line-height:1.62;color:var(--dash-muted)}
.ptmsg__b p:last-child{margin-bottom:0}

/* --- The main-content footer ---------------------------------------------- */

/* A hairline and one line of type, so a short page ends rather than stopping.
   Separate from the rail's pinned footer and saying something different: the
   rail says who is signed in, this says whose ImageBooth this is. */
.ptfoot{display:flex;flex-wrap:wrap;align-items:center;gap:8px;
  margin:clamp(40px,6vw,72px) 0 0;padding:18px 0 4px;
  border-top:1px solid var(--dash-item-border);
  font-size:var(--fs-xs);letter-spacing:.18em;text-transform:uppercase;
  color:var(--dash-faint)}
.ptfoot__c{color:var(--dash-muted)}
.ptfoot__d{color:var(--dash-faint)}

.pth{display:flex;align-items:flex-end;justify-content:space-between;gap:28px;
  flex-wrap:wrap;margin:0 0 30px}
.pth__greet{white-space:normal;overflow:visible;text-overflow:clip;
  line-height:1.1;margin:0}
.pth__greet .pth__name{display:block}
.pth__sub{margin:12px 0 0;font-size:var(--fs-base);color:var(--dash-muted);max-width:44ch}
/* Proportionate, not a full-width slab. It sits on the greeting's baseline. */
.pth__cta{flex-shrink:0;margin-bottom:6px}

/* THE ACTIVITY STRIP. An editorial rule with three figures on it — not three
   cards, because a client's home is calm and a card implies something to press.
   Cormorant for the numeral and the product's micro-caps for the label, both
   already in the sheet. */
.ptstat{display:flex;flex-wrap:wrap;gap:8px 44px;margin:0 0 30px;padding:16px 0;
  border-top:1px solid var(--dash-item-border);
  border-bottom:1px solid var(--dash-item-border)}
.ptstat__i{display:flex;align-items:baseline;gap:10px;min-width:0}
/* The three counts take the SAME accent as the date numeral — one colour for
   every numeric anchor on the page, never one per statistic. Their labels stay
   neutral: the number is the anchor, the label is what it means. */
.ptstat__v{font-family:var(--serif);font-size:var(--dash-fs-heading);
  color:var(--accent);font-variant-numeric:tabular-nums;line-height:1}
.ptstat__l{font-size:var(--fs-xs);letter-spacing:.16em;text-transform:uppercase;
  color:var(--dash-faint)}

.ptsec{margin:0 0 26px}
.ptsec__hd{display:flex;align-items:baseline;justify-content:space-between;
  gap:16px;margin:0 0 12px}
.ptsec__more{font-family:var(--body);font-size:var(--fs-md);color:var(--dash-muted);
  text-decoration:none;transition:color 140ms var(--ease)}
.ptsec__more:hover{color:var(--dash-ink)}

/* --- The project row ----------------------------------------------------- */
/* `.shot`'s material, geometry and interaction, widened for a client's row. */
.ptlist{display:flex;flex-direction:column;gap:8px}
/* A plain entry in a client's own list — the changes they have asked for and
   what Studio Hill said. Not `.ptrow`: that is a link to somewhere, and these
   go nowhere. */
.ptlist__i{display:flex;flex-direction:column;gap:6px;padding:13px 16px;
  border-radius:var(--dash-r-row);background:var(--dash-item);
  border:1px solid var(--dash-item-border);min-width:0}
.ptlist__m{font-size:var(--fs-md);color:var(--dash-faint)}
.ptrow{display:flex;align-items:center;gap:16px;padding:13px 16px;
  border-radius:var(--dash-r-row);background:var(--dash-item);
  border:1px solid var(--dash-item-border);text-decoration:none;color:inherit;
  min-width:0;
  transition:transform 160ms var(--dash-ease),border-color 160ms ease,
    background 400ms ease}
a.ptrow:hover{transform:translateX(3px);border-color:var(--dash-hi-border);
  background:var(--dash-item-hover)}
a.ptrow:active{transform:translateX(3px) scale(.99)}
a.ptrow:focus-visible{outline:2px solid var(--dash-info);outline-offset:2px}
/* A fixed box, so a portrait and a landscape make the same row height and the
   list scans as a column. */
.ptrow__img{width:76px;height:54px;flex-shrink:0;border-radius:var(--chip-radius);
  overflow:hidden;background:var(--panel2)}
.ptrow__img img{width:100%;height:100%;object-fit:cover;display:block}
.ptrow__main{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1 1 auto}
.ptrow__title{font-size:var(--dash-fs-title);font-weight:600;color:var(--dash-ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ptrow__meta{font-size:var(--dash-fs-small);color:var(--dash-muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ptrow__sep{margin:0 7px;opacity:.5}
.ptrow__st{flex-shrink:0}

/* --- Jobs ---------------------------------------------------------------- */
.ptfind{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:0 0 18px}
/* The Register's own search material. */
.ptfind__box{display:flex;align-items:center;gap:9px;flex:1 1 240px;min-width:0;
  background:var(--surface-card);border:1px solid var(--dash-item-border);
  border-radius:var(--dash-r-row);padding:10px 14px;
  transition:border-color 140ms var(--ease)}
.ptfind__box:focus-within{border-color:var(--dash-hi-border)}
.ptfind__box svg{width:13px;height:13px;color:var(--dash-faint);flex-shrink:0}
.ptfind__in{flex:1 1 auto;min-width:0;background:transparent;border:0;
  color:var(--ink);font-family:var(--body);font-size:var(--fs-base)}
.ptfind__in::placeholder{color:var(--dash-faint)}
.ptfind__in:focus{outline:none}

.ptfind__tabs{display:flex;gap:6px;flex-wrap:wrap}
.ptfind__tab{position:relative;display:inline-flex}
.ptfind__tab input{position:absolute;inset:0;opacity:0;width:100%;height:100%;
  margin:0;cursor:pointer}
.ptfind__tab span{display:inline-block;padding:9px 15px;border-radius:var(--dash-r-row);
  border:1px solid var(--dash-item-border);background:var(--dash-item);
  font-family:var(--body);font-size:var(--fs-md);font-weight:500;
  color:var(--dash-muted);
  transition:color 140ms var(--ease),background 140ms var(--ease),
    border-color 140ms var(--ease),transform 120ms var(--ease)}
.ptfind__tab:hover span{color:var(--dash-ink)}
.ptfind__tab input:active+span{transform:scale(.97)}
.ptfind__tab input:focus-visible+span{outline:2px solid var(--dash-info);outline-offset:2px}
.ptfind__tab input:checked+span{color:var(--dash-ink);font-weight:600;
  border-color:var(--dash-hi-border);background:var(--dash-item-hover)}

/* --- The project grid ---------------------------------------------------- */
/* A LIBRARY, NOT A TABLE. The photograph leads and the words follow it, which
   is the opposite emphasis from the admin Register — an operator scans rows for
   a status, a client looks for the shoot they remember by how it looked.
   Three across where there is room, two on a tablet, one on a phone. */
/* A LIBRARY, AND LIBRARIES ARE DENSE. Three across read as a dashboard of
   three big cards; a client with forty shoots wants to see them.
   
   `auto-fill` with a 190px floor rather than counted breakpoints: the column
   count then follows the space that actually exists, which is the only thing
   that is true at every width. At 1920px the content column gives six across,
   at 1440px five, at a laptop four, and it degrades through three and two to
   one on a phone without a single viewport number being written down. */
.ptgrid{display:grid;gap:14px;
  grid-template-columns:repeat(auto-fill,minmax(190px,1fr))}
/* THE STOCK LIBRARY — Task 017.
   ---------------------------------------------------------------------------
   Collections as tiles. The Projects card, three across rather than five,
   because a collection is a place — a suburb, a streetscape — and a place
   wants a wider photograph than a shoot does. It closes to two at a laptop
   and one on a phone through the same minmax the Projects grid uses, so no
   viewport number is written down. */
.ptgrid--stock{grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));
  gap:18px;list-style:none;margin:0;padding:0}
.ptstock__lead{margin:-6px 0 22px;font-size:var(--dash-fs-body);color:var(--dash-muted)}
.ptstock__card{height:100%}
.ptstock__card .ptcard__b{padding:13px 14px 14px}
.ptstock__art{aspect-ratio:3/2}
/* On a phone the tiles stack one to a column, so each hero is the full
   width and a 3:2 plate is 222px tall — six collections is a long scroll.
   Shorter here, and only here: 16:10 keeps the plate photographic (208px at
   375) while a client moves through suburbs with less thumb. Tablet and
   desktop keep 3:2. */
@media (max-width:600px){
  .ptstock__art{aspect-ratio:16/10}
}
.ptstock__d{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;
  overflow:hidden;white-space:normal}

/* One collection, open. The gallery's rows and tiles, inside the portal. */
.stk__hd{display:flex;flex-direction:column;gap:12px;margin-bottom:22px}
.stk__title{margin:0}
.stk__desc{margin:-4px 0 0;max-width:64ch;font-size:var(--dash-fs-body);color:var(--dash-muted)}
.stk__all{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.stk__allk{font-size:var(--dash-fs-small);letter-spacing:.14em;text-transform:uppercase;
  color:var(--dash-faint);margin-right:4px}
.stk__rows{margin:0 0 84px}
.stk__rows .cgfig{border-radius:var(--dash-r-row)}
.stk__rows .cgfig.is-picked{outline:2px solid var(--accent);outline-offset:2px}

/* The download bar: quiet, sticky to the foot of the workspace so it is
   reachable from anywhere in a long collection. It draws attention only
   once something is ticked. */
.stk__bar{position:sticky;bottom:18px;z-index:2;margin:0 0 8px;
  border-radius:var(--dash-r-row);border:1px solid var(--dash-item-border);
  background:color-mix(in srgb, var(--bg) 88%, transparent);backdrop-filter:blur(var(--dash-blur));
  transition:border-color 160ms var(--ease)}
.stk__bar.is-live{border-color:var(--accent)}
.stk__bar__in{display:flex;align-items:center;justify-content:space-between;gap:14px;
  padding:10px 12px 10px 16px;flex-wrap:wrap}
.stk__state{font-size:var(--dash-fs-body);color:var(--dash-muted)}
.stk__state b{color:var(--dash-ink);font-variant-numeric:tabular-nums}
.stk__acts{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.stk__acts .btn[disabled]{opacity:.45;cursor:default}

@media (max-width:720px){
  .stk__rows{margin-bottom:110px}
  .stk__bar__in{padding:10px 12px}
}

.ptcard{display:flex;flex-direction:column;min-width:0;text-decoration:none;
  color:inherit;border-radius:var(--dash-r-row);border:1px solid var(--dash-item-border);
  background:var(--dash-item);overflow:hidden;
  transition:transform 160ms var(--dash-ease),border-color 160ms ease,
    background 400ms ease}
a.ptcard:hover{transform:translateY(-2px);border-color:var(--dash-hi-border);
  background:var(--dash-item-hover)}
a.ptcard:active{transform:translateY(-2px) scale(.995)}
a.ptcard:focus-visible{outline:2px solid var(--dash-info);outline-offset:2px}
/* The image slot is `.jthumb--grid`, unchanged: a 4:3 box with the photograph
   covered into it, and the product's existing restrained mark when there is
   nothing to draw. */
.ptcard .jthumb--grid{border-radius:0;border-bottom:1px solid var(--dash-item-border)}
.ptcard__b{display:flex;flex-direction:column;gap:3px;padding:11px 12px 12px;min-width:0}
.ptcard__t{font-size:var(--dash-fs-body);font-weight:600;color:var(--dash-ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ptcard__a{font-size:var(--dash-fs-small);color:var(--dash-muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ptcard__f{display:flex;align-items:center;justify-content:space-between;gap:8px;
  flex-wrap:wrap;margin-top:6px}
.ptcard__m{font-size:var(--dash-fs-small);color:var(--dash-muted);min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The view toggle. It sits under the search row, at the right, because it
   changes how the same projects are drawn rather than which ones — and it is
   LINKS, not radios: the pair used to live inside the search form, where
   choosing List only ticked a control and waited for a button marked Search.

   Styled as the filter tabs are, so the two read as the same family of control
   even though only one of them submits anything. */
.ptfind__view{display:flex;justify-content:flex-end;gap:6px;margin:-6px 0 16px}
.ptview{display:inline-block;padding:7px 14px;border-radius:var(--dash-r-row);
  border:1px solid transparent;background:transparent;
  font-family:var(--body);font-size:var(--dash-fs-small);font-weight:500;
  color:var(--dash-muted);text-decoration:none;
  transition:color 140ms var(--ease),background 140ms var(--ease),
    border-color 140ms var(--ease),transform 120ms var(--ease)}
.ptview:hover{color:var(--dash-ink);background:var(--dash-item)}
.ptview:active{transform:scale(.97)}
.ptview:focus-visible{outline:2px solid var(--dash-info);outline-offset:2px}
.ptview.is-on{color:var(--dash-ink);font-weight:600;
  border-color:var(--dash-hi-border);background:var(--dash-item-hover)}

/* --- Detail, prose, empty ------------------------------------------------ */
.ptempty{margin:0;padding:30px 2px;color:var(--dash-muted);font-size:var(--fs-base);
  line-height:1.65;max-width:52ch}
.ptnext,.ptnote{color:var(--dash-muted);font-size:var(--fs-base);line-height:1.65;
  margin:14px 0 0;max-width:56ch}
.ptback{margin:0 0 16px}
.ptdetail{margin:20px 0 0;display:flex;flex-direction:column;gap:0}
.ptdetail div{display:flex;gap:20px;flex-wrap:wrap;padding:13px 0;
  border-bottom:1px solid var(--dash-item-border)}
.ptdetail div:first-child{border-top:1px solid var(--dash-item-border)}
.ptdetail dt{min-width:150px;color:var(--dash-muted);font-size:var(--fs-md);margin:0}
.ptdetail dd{margin:0;font-size:var(--dash-fs-title);color:var(--dash-ink)}
.ptdetail__status{margin:12px 0 0}
.ptout{margin:30px 0 0}

/* --- Book a shoot -------------------------------------------------------- */
/* The request header: a high-contrast band on the rail's own dark ground, so
   the page opens with the product's voice rather than with a form. */
/* `--band` and `--band-ink` are the existing pair for a near-black surface and
   the cream that sits on it, already used by the four dark bands elsewhere in
   the product. `--band-line` is their hairline. Nothing new is introduced. */
.ptband{border-radius:var(--dash-r-card);background:var(--band);
  padding:30px 32px;margin:0 0 26px;color:var(--band-ink)}
.ptband h1{font-family:var(--serif);font-weight:400;
  font-size:var(--dash-fs-display-sm);line-height:1.1;margin:0;color:var(--band-ink)}
.ptband p{margin:12px 0 0;font-size:var(--fs-base);line-height:1.6;
  color:var(--band-ink);opacity:.72;max-width:52ch}
.ptband__note{display:inline-block;margin:18px 0 0;padding:7px 13px;
  border-radius:var(--pill-radius);border:1px solid var(--band-line);
  font-size:var(--fs-md);color:var(--band-ink);opacity:.86}

/* Two columns on desktop: the form, and the summary beside it. */
.ptbook{display:grid;grid-template-columns:minmax(0,1fr) 316px;gap:34px;
  align-items:start}
.ptform{display:flex;flex-direction:column;gap:26px;min-width:0}
.ptfs{border:0;padding:0;margin:0;display:flex;flex-direction:column;gap:12px}
/* SECTION HEADINGS, in the booking portal's own rhythm: Cormorant at heading
   size with a full stop and a hairline beneath, so "Photography." reads as a
   chapter rather than as a form label. Same face and same rule the admin's
   card headings use. */
.ptfs__l{font-family:var(--serif);font-weight:400;font-size:var(--dash-fs-heading);
  letter-spacing:0;text-transform:none;color:var(--dash-ink);padding:0 0 10px;
  margin:0 0 4px;width:100%;border-bottom:1px solid var(--dash-item-border);
  display:block}
/* The full stop belongs to the word, so it must not be a flex item that
   `space-between` can push to the far end of the rule. */
.ptfs__l::after{content:"."}
.ptfs__l .ptfs__opt{float:right;font-family:var(--body);
  font-size:var(--fs-xs);letter-spacing:.16em;text-transform:uppercase;
  color:var(--dash-faint)}
.ptfs__opt,.ptfld__opt{letter-spacing:.01em;text-transform:none;
  color:var(--dash-faint);font-size:var(--fs-md)}

/* PACKAGE CARDS, two across. Not a price list. */
.ptpk{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
/* THE PACKAGE CARD, in the booking portal's rhythm: the name and what it
   includes CENTRED, the price and the add control on a baseline beneath. That
   centring is the thing that makes a grid of these read as a menu of services
   rather than as a price list, and it is why the cards are the composition the
   external portal has always used. */
.ptpk__c{position:relative;display:flex;flex-direction:column;gap:6px;
  text-align:center;min-height:118px;
  padding:20px 22px 16px;border-radius:var(--dash-r-row);
  border:1px solid var(--dash-item-border);background:var(--dash-item);
  cursor:pointer;min-width:0;
  transition:border-color 140ms var(--ease),background 140ms var(--ease),
    transform 120ms var(--ease)}
.ptpk__c:hover{border-color:var(--dash-hi-border);background:var(--dash-item-hover)}
.ptpk__c:active{transform:scale(.995)}
.ptpk__c input{position:absolute;inset:0;opacity:0;width:100%;height:100%;
  margin:0;cursor:pointer}
/* SELECTED INVERTS THE CARD, as the reference does — the strongest possible
   state, and unmistakable across a grid. `--band` and `--band-ink` are the
   existing near-black/cream pair. */
/* THE ACCENT RING IS WHAT MAKES THIS WORK IN DARK. `--band` is near-black in
   both themes, which inverts dramatically against a light card and barely at
   all against a dark one — so the fill carries the state in light and the ring
   carries it in dark. One rule, no theme branch, unmistakable in both. */
.ptpk__c:has(input:checked){border-color:var(--accent);background:var(--band);
  box-shadow:inset 0 0 0 1px var(--accent)}
.ptpk__c:has(input:checked) .ptpk__n,
.ptpk__c:has(input:checked) .ptpk__p{color:var(--band-ink)}
.ptpk__c:has(input:checked) .ptpk__d{color:var(--band-ink);opacity:.72}
.ptpk__c:has(input:checked) .ptpk__p--tba{opacity:.72}
.ptpk__c input:focus-visible~.ptpk__n{outline:2px solid var(--dash-info);outline-offset:3px}
.ptpk__n{font-size:var(--dash-fs-title);font-weight:600;color:var(--dash-ink)}
.ptpk__d{font-size:var(--dash-fs-small);color:var(--dash-muted);line-height:1.55;
  flex:1 1 auto}
.ptpk__f{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  margin-top:auto;text-align:left}
.ptpk__p{font-size:var(--dash-fs-body);font-weight:600;color:var(--dash-ink);
  font-variant-numeric:tabular-nums}
.ptpk__p--tba{font-weight:500;color:var(--dash-muted)}
/* ADD / SELECTED, in the chip vocabulary. */
.ptpk__a{font-size:var(--fs-xs);letter-spacing:.14em;text-transform:uppercase;
  color:var(--dash-faint)}
.ptpk__c:has(input:checked) .ptpk__a{color:var(--band-ink)}
.ptpk__c:has(input:checked) .ptpk__a::after{content:"Selected"}
.ptpk__c:not(:has(input:checked)) .ptpk__a::after{content:"Add"}

/* ADD-ONS as compact chips. */
.ptchips{display:flex;flex-wrap:wrap;gap:8px}
.ptchip{position:relative;display:inline-flex;align-items:center;gap:9px;
  padding:9px 15px;border-radius:var(--pill-radius);
  border:1px solid var(--dash-item-border);background:var(--dash-item);
  cursor:pointer;
  transition:border-color 140ms var(--ease),background 140ms var(--ease),
    transform 120ms var(--ease)}
.ptchip:hover{border-color:var(--dash-hi-border)}
.ptchip:active{transform:scale(.97)}
.ptchip input{position:absolute;inset:0;opacity:0;width:100%;height:100%;
  margin:0;cursor:pointer}
.ptchip:has(input:checked){border-color:var(--accent);background:var(--band);
  box-shadow:inset 0 0 0 1px var(--accent)}
.ptchip:has(input:checked) .ptchip__n{color:var(--band-ink)}
.ptchip:has(input:checked) .ptchip__p{color:var(--band-ink);opacity:.72}
.ptchip__n{font-size:var(--fs-base);color:var(--dash-ink)}
.ptchip:has(input:checked) .ptchip__n{font-weight:600}
.ptchip__p{font-size:var(--dash-fs-small);color:var(--dash-muted);
  font-variant-numeric:tabular-nums}

.ptfld{display:flex;flex-direction:column;gap:7px}
.ptfld>span{font-size:var(--fs-md);color:var(--dash-muted)}
.ptfld input,.ptfld textarea,.ptfld select{padding:11px 14px;
  border:1px solid var(--dash-item-border);border-radius:var(--dash-r-row);
  background:var(--surface-card);color:var(--ink);font-family:var(--body);
  font-size:var(--fs-base);transition:border-color 140ms var(--ease)}
.ptfld input:focus,.ptfld textarea:focus,.ptfld select:focus{outline:none;
  border-color:var(--dash-hi-border)}
.ptfld__row{display:flex;gap:12px;flex-wrap:wrap}
.ptfld__row .ptfld{flex:1 1 150px}

/* --- The request summary ------------------------------------------------- */
/* Sticky beside the form on desktop; inline above the button on a phone. */
.ptsum{position:sticky;top:34px;border-radius:var(--dash-r-card);
  background:var(--dash-glass);backdrop-filter:blur(var(--dash-blur));
  -webkit-backdrop-filter:blur(var(--dash-blur));
  border:1px solid var(--dash-glass-border);box-shadow:var(--dash-glass-shadow);
  padding:22px 22px 20px;display:flex;flex-direction:column;gap:14px}
.ptsum__h{font-family:var(--body);font-size:var(--fs-xs);letter-spacing:.16em;
  text-transform:uppercase;color:var(--dash-faint);margin:0}
.ptsum__l{display:flex;flex-direction:column;gap:9px;margin:0;padding:0;list-style:none}
.ptsum__i{display:flex;justify-content:space-between;gap:12px;
  font-size:var(--dash-fs-small)}
.ptsum__i span:first-child{color:var(--dash-ink);min-width:0;
  overflow:hidden;text-overflow:ellipsis}
.ptsum__i span:last-child{color:var(--dash-muted);flex-shrink:0;
  font-variant-numeric:tabular-nums}
.ptsum__none{font-size:var(--dash-fs-small);color:var(--dash-faint);margin:0}
.ptsum__d{display:flex;flex-direction:column;gap:6px;padding-top:12px;
  border-top:1px solid var(--dash-item-border)}
.ptsum__row{display:flex;justify-content:space-between;gap:12px;
  font-size:var(--dash-fs-small);color:var(--dash-muted)}
.ptsum__row b{font-weight:500;color:var(--dash-ink);text-align:right;min-width:0}
.ptsum__t{display:flex;justify-content:space-between;gap:12px;padding-top:12px;
  border-top:1px solid var(--dash-item-border);align-items:baseline}
.ptsum__tl{font-size:var(--fs-md);color:var(--dash-muted)}
/* `em` for the semantics, not for a slant — this typeface ships no
   italic face, so the emphasis is carried by tracking and case. */
.ptsum__ex{font-style:normal;letter-spacing:.14em;text-transform:uppercase;
  font-size:var(--fs-xs);color:var(--dash-faint);margin-left:5px}
.ptsum__tv{font-family:var(--serif);font-size:var(--dash-fs-heading);
  color:var(--dash-ink);font-variant-numeric:tabular-nums}
.ptsum__fine{margin:0;font-size:var(--fs-xs);color:var(--dash-faint);line-height:1.5}
.ptsum__go{width:100%;justify-content:center}

.ptok,.pterr{padding:18px 20px;border-radius:var(--dash-r-row);
  background:var(--dash-item);margin:0 0 22px}
.ptok{border:1px solid var(--ok)}
.pterr{border:1px solid var(--hot)}
.ptok strong{font-family:var(--serif);font-weight:500;
  font-size:var(--dash-fs-heading);color:var(--dash-ink);display:block}
.ptok p{margin:8px 0 0;color:var(--dash-muted);font-size:var(--fs-base)}
.pterr p{margin:0;font-size:var(--fs-base);color:var(--dash-ink)}

/* --- Sign in ------------------------------------------------------------- */
.ptauth__h{font-family:var(--serif);font-weight:400;font-size:var(--dash-fs-heading);
  color:var(--ink);margin:0 0 12px}
.ptauth__p{color:var(--dash-muted);font-size:var(--fs-base);line-height:1.65;margin:0 0 20px}
.ptauth__err{color:var(--hot);font-size:var(--fs-base);line-height:1.6;margin:0 0 20px}
.ptauth__form{display:flex;flex-direction:column;gap:12px}
.ptauth__lbl{font-size:var(--fs-md);color:var(--dash-muted)}
.ptauth__in{padding:12px 14px;border:1px solid var(--dash-item-border);
  border-radius:var(--dash-r-row);background:var(--surface-card);color:var(--ink);
  font-family:var(--body);font-size:var(--fs-base);
  transition:border-color 140ms var(--ease)}
.ptauth__in:focus{outline:none;border-color:var(--dash-hi-border)}

/* --- Tablet and phone ---------------------------------------------------- */
@media (max-width:1080px){
  .ptbook{grid-template-columns:minmax(0,1fr)}
  /* The summary stops being a column and becomes a block above the send
     button, which is where a thumb expects it. */
  .ptsum{position:static;order:2}
  .ptpk{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
}

@media (max-width:860px){
  /* THE RAIL SLIDES AWAY. It is still the same rail — same markup, same
     component — it simply stops taking a column.

     ALL THREE AUDIENCES, not just the two portals. The Admin shell used to
     keep a 180px rail at every width, which on a 390px phone left `main`
     210px to draw 514px of content — and `overflow:hidden` turned that into
     amputation rather than a scrollbar. It is the same rail and the same
     mechanism; only the selector needed widening. */
  .app,
  .app--pt{grid-template-columns:minmax(0,1fr);background-size:100% 100%;
    background-position:0 0}
  /* Off-canvas: the rail leaves the grid entirely, so it takes its height from
     the viewport rather than from a column that no longer exists. */
  .pt__rail .sb{height:100dvh}
  .pt__rail{position:fixed;inset:0 auto 0 0;width:var(--sidebar-w);z-index:40;
    transform:translateX(-100%);transition:transform 260ms var(--ease-drawer);
    display:flex}
  .pt__rail .sb{height:100%;width:100%}
  .app.is-open .pt__rail{transform:translateX(0)}
  /* ONLY WHILE THE RAIL IS OPEN. A class rule beats the `[hidden]` UA rule, so
     `display:block` on its own left the scrim dimming the whole page on every
     phone visit — it has to be scoped to the open state, not to the width. */
  .app.is-open .pt__scrim{display:block;position:fixed;inset:0;z-index:39;
    background:rgba(7,7,6,.55)}
  /* AND SO IS THE DRAWER, as a FIXED sheet rather than an absolute one.
     `position:absolute;right:0` resolves against `.main`, and `.main` is the
     box the Job Register's table overflows — so at 360px the drawer came out
     the right width and 14px too far to the right, hanging off the screen.
     Taking it out of that box entirely is both the simpler rule and the one
     that cannot be moved by whatever a page puts inside `main`.

     Below the rail and its scrim, which are 40 and 39: if both are somehow
     open the navigation wins, because that is the way out. */
  .drawer{position:fixed;inset:0;z-index:38;width:100%;max-width:100%;
    border-left:0;box-shadow:none}

  /* THE MODAL IS THE SCREEN at this width. `width:1180px` capped to
     `calc(100% - 48px)` is a phone-sized dialog wearing a desktop dialog's
     inset and radius; full-bleed is the honest shape for it here. Geometry
     lives in this sheet because the skin is guarded against setting it. */
  .modal{top:0;bottom:0;left:0;right:0;transform:none;width:100%;max-width:100%}
  /* In the top rail now, not floating over the page: a control at the left of
     a band is where one is looked for, and it no longer sits on top of
     whatever happens to have scrolled under it. */
  .pt__menu{display:inline-flex;flex-direction:column;justify-content:center;
    gap:4px;flex-shrink:0;width:38px;height:38px;
    padding:0 9px;border-radius:var(--chip-radius);
    border:1px solid var(--dash-item-border);background:var(--surface-card);
    backdrop-filter:blur(var(--dash-blur));
    -webkit-backdrop-filter:blur(var(--dash-blur));
    transition:transform 120ms var(--ease)}
  .pt__menu:active{transform:scale(.94)}
  .pt__menu span{display:block;height:1.5px;background:var(--ink);border-radius:2px}
  .app--pt{--pt-gutter:16px}
  .pt__ws{padding:24px 16px 56px}
  .pth{gap:18px}
  .pth__cta{margin-bottom:0}

  /* THE TOP RAIL ON A PHONE. The menu button, the client's name, and the
     action — the mark is the first thing to go, because a second faint layer
     at this width is noise rather than identity. */
  .pttop{height:60px}
  .pttop__in{gap:10px;padding:0 16px}
  .pttop__mark{display:none}
  /* Tighter tracking, not smaller type: `--fs-micro` is below the content
     floor, and a client's own name is content.
     It yields the room rather than the button does — a name can ellipse, and a
     CTA that shrinks to fit is a CTA nobody can press. */
  .pttop__c{letter-spacing:.14em;min-width:0;flex-shrink:1}

  /* THE ACTION, ABOUT A FIFTH SMALLER.
     Utility, not the point of the rail. The reduction is taken from the
     horizontal padding and the type — which is where the visual weight
     actually is — and NOT from the height, which stays at 40px so it is still
     comfortably tappable. Shrinking a control below the thumb is a worse
     outcome than a button that looks slightly heavy. */
  .pttop__cta{min-height:40px;height:40px;padding:0 15px;
    font-size:var(--fs-sm);letter-spacing:.02em}

  /* THE GREETING, a step down at phone widths.
     Not for its own sake: it buys the date, the conditions and the top of the
     photograph a place in the first viewport, which is what a client opening
     this on a phone is actually looking for. The Cormorant hierarchy and the
     italic name are untouched — this is a size, not a restyle. */
  .app--pt .dash__greet{font-size:calc(var(--dash-fs-display-sm) * .92)}

  /* THE HERO ON A PHONE. One column: greeting, then the photograph beneath it,
     then its caption, then the counters. The plate keeps its mask — it is what
     stops the picture reading as a pasted-in rectangle at any width. */
  .pthero{margin:0 0 24px;padding:0 0 20px}
  .pthero__in{grid-template-columns:minmax(0,1fr);gap:22px}
  /* One column here, so there is no cell to reach out of and nothing to the
     left to bleed into. */
  .pthero__plate{margin-top:0;margin-left:0}
  .pthero__meta{gap:12px 22px;margin-top:20px}
  .pthero__stat{margin-top:22px}
  .ptmsg{padding:2px 0 2px 14px;margin-bottom:22px}
  .ptfoot{margin-top:38px;letter-spacing:.14em}
  /* Two up then one, rather than three squeezed across a phone. */
  .ptstat{gap:14px 28px;margin-bottom:24px}
  .ptrow{gap:12px;padding:12px 13px;flex-wrap:wrap}
  .ptrow__img{width:58px;height:42px}
  .ptrow__st{margin-left:auto}
  .ptdetail div{flex-direction:column;gap:4px}
  .ptdetail dt{min-width:0}
  /* One column on a phone. The floor above would otherwise still fit two at
     375px, and two 170px cards is a contact sheet rather than a library. */
  .ptgrid{grid-template-columns:minmax(0,1fr);gap:12px}
  .ptfind__tabs{width:100%}
  /* The toggle keeps its own row on a phone but stops hugging the right edge:
     with the filters full-width above it, a right-aligned pair of links reads
     as detached from them. */
  .ptfind__view{justify-content:flex-start;margin:-4px 0 14px}
  .ptfind__tab{flex:1 1 0}
  .ptfind__tab span{width:100%;text-align:center}
  .ptband{padding:24px 22px}
}

/* --- The admin booking queue --------------------------------------------- */
.brq{display:flex;flex-direction:column;gap:12px}
.brq__c{border-radius:var(--dash-r-card);background:var(--dash-glass);
  border:1px solid var(--dash-glass-border);box-shadow:var(--dash-glass-shadow);
  padding:20px 22px}
.brq__hd{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
.brq__who{display:flex;flex-direction:column;gap:3px;min-width:0}
.brq__client{font-size:var(--fs-xs);letter-spacing:.16em;text-transform:uppercase;
  color:var(--dash-faint)}
.brq__title{font-family:var(--serif);font-size:var(--dash-fs-heading);
  font-weight:500;color:var(--dash-ink)}
.brq__addr{font-size:var(--dash-fs-small);color:var(--dash-muted)}
.brq__when{display:flex;flex-direction:column;gap:2px;text-align:right}
.brq__date{font-size:var(--dash-fs-title);font-weight:600;color:var(--dash-ink)}
.brq__alt,.brq__time{font-size:var(--dash-fs-small);color:var(--dash-muted)}
.brq__items{margin:14px 0 0;padding:0 0 0 18px;color:var(--dash-muted);
  font-size:var(--dash-fs-small)}
.brq__ref,.brq__notes{margin:10px 0 0;font-size:var(--dash-fs-small);
  color:var(--dash-muted);line-height:1.6}
.brq__go{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;
  margin:16px 0 0;padding:16px 0 0;border-top:1px solid var(--dash-item-border)}
.brq__f{display:flex;flex-direction:column;gap:5px}
.brq__f>span{font-size:var(--fs-md);color:var(--dash-muted)}
.brq__f input,.brq__f select{padding:9px 12px;border:1px solid var(--dash-item-border);
  border-radius:var(--chip-radius);background:var(--surface-card);color:var(--ink);
  font-family:var(--body);font-size:var(--fs-base)}
.brq__f--wide{flex:1 1 220px}
.brq__opt{color:var(--dash-faint)}
.brq__more{margin:14px 0 0}

/* --- What changed, and what it used to say -------------------------------- */
/* Two places use this: a request a client edited while it waited, and a change
   they have asked for on a booking that is already confirmed. Both answer the
   same question — "this is different from what I last read" — so they are drawn
   the same way rather than invented twice.

   The old value is struck through and quiet; the new one is the weight. An
   operator scanning the queue should read the CURRENT address without effort
   and find the previous one only when they go looking for it. */
.brq__edit{margin:14px 0 0;font-size:var(--dash-fs-small);color:var(--dash-muted)}
.brq__edit strong{color:var(--dash-ink);font-weight:600}
.brq__diff{list-style:none;margin:8px 0 0;padding:0;display:flex;
  flex-direction:column;gap:6px}
.brq__diff li{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;
  font-size:var(--dash-fs-small);line-height:1.5;min-width:0}
/* Fixed but shrinkable: the labels line up into a column on a wide card and
   give way rather than pushing the values off a phone. */
.brq__diffl{flex:0 1 128px;color:var(--dash-faint)}
.brq__diff s{color:var(--dash-faint);text-decoration-thickness:1px;
  overflow-wrap:anywhere;min-width:0}
.brq__diff b{color:var(--dash-ink);font-weight:600;overflow-wrap:anywhere;
  min-width:0}

/* A second queue under the first. Spaced away from the section above it so the
   two never read as one list with a stray heading in the middle. */
.hd--sub{margin-top:38px}

/* --- The whole chain, oldest last ---------------------------------------- */
/* Drawn on two screens from one partial: folded into a queue card, and open on
   the Job edit form. Each entry is a header line and a diff, and the diff reuses
   `.brq__diff` above rather than restating it — the two must look identical or
   the same record would read differently depending on where it was opened.

   `list-style:none` on an `<ol>` on purpose: the order is the meaning and a
   screen reader still announces it as a list, but a column of numbers beside
   timestamps that are already in order is noise. */
.bkhist{list-style:none;margin:10px 0 0;padding:0;display:flex;
  flex-direction:column;gap:2px}
.bkhist__i{padding:12px 14px;border-radius:var(--dash-r-row);
  background:var(--dash-item);border:1px solid var(--dash-item-border);min-width:0}
.bkhist__hd{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;margin:0;
  font-size:var(--dash-fs-small);color:var(--dash-muted)}
.bkhist__hd strong{color:var(--dash-ink);font-weight:600}
.bkhist__when{color:var(--dash-faint)}
/* The figure. Same geometry as a diff row so it lines up under one, and the
   only place in this component that carries money — it is rendered only when
   the reader holds `JobPricingView`. */
.bkhist__money{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;
  margin:8px 0 0;padding-top:8px;border-top:1px solid var(--dash-item-border);
  font-size:var(--dash-fs-small)}
.bkhist__money s{color:var(--dash-faint)}
.bkhist__money b{color:var(--dash-ink);font-weight:600}

/* --- Studio Hill Help: what the gated skin cannot hold ---------------------- */
/* The drawer's motion, and the page lock while it is open. Here rather than in
   imagebooth-skin.css because every selector there is gated to the skinned
   shell, and neither a keyframe's `from`/`to` nor the root element can be. Both
   are inert
   anywhere the drawer is not: nothing else names these keyframes, and only the
   drawer's script sets the class. */
@keyframes phelp-in{from{transform:translateX(24px);opacity:0}to{transform:none;opacity:1}}
@keyframes phelp-up{from{transform:translateY(24px);opacity:0}to{transform:none;opacity:1}}
/* The page behind stays where it was while Help is open. The dialog already
   makes it inert; this stops a touch scroll moving it underneath the sheet. */
html.phelp-open{overflow:hidden}
