/* DDM-BUILD-MARKER: toolbar-wrap+collapse-pin+deadgroup-collapse 2026-08-20b */
/* The deliverable dashboard at 390px (2026-08-19) — the phone stylesheet.
   Implements the standing rules of section 8 of the Content Calendar spec, the
   ones that hold on EVERY dashboard and every card, so no card has to restate
   them: no horizontal page scroll, tables stack, 44px targets, a visible label
   above every input, a reduced rich-text toolbar, exactly three thumbnail
   sizes, and a real empty or loading state instead of a bare spinner.

   ── LOAD ORDER MATTERS ─────────────────────────────────────────────────────
   This file must be linked AFTER deliverable-dashboard.css and after the card
   stylesheets (dd-focus-points-card.css, cc-schedule.css, cc-connect-wizard.css).
   Almost everything here is a cross-cutting override of a rule those files
   already set at the same specificity, so a link tag placed above them makes
   half of this file silently inert. It is deliberately the last stylesheet in
   the dashboard block, and that is why it carries very few !important markers:
   later-wins is doing the work instead.

   ── WHY 760px AND NOT 390px ────────────────────────────────────────────────
   390px is the width this team VERIFIES at, not the width the rules start at.
   deliverable-dashboard.css:524 already stacks the posts table below 760px,
   dd-focus-points-card.css:217 stacks the focus point rows at the same edge and
   cc-schedule.css:337 does the same for its line items. A second breakpoint
   would give a 500px screen a stacked posts table with desktop tap targets and
   a desktop toolbar, which is the worst of both. So the whole phone band is one
   breakpoint and it is the one already in the build: max-width 760px.

   ── WHAT THIS FILE DOES NOT DO ─────────────────────────────────────────────
   It does not restyle the bottom sheet. dd-sheet.js injects its own stylesheet
   on first open, on purpose, so a page that never opens a sheet does not carry
   its styles. Re-declaring .dd-sheet-* here would fork that.
   It does not restyle a card's interior where that card's own stylesheet
   already has a 760px block. It supplies the shared primitives those cards and
   the phone-only scripts sit on.

   ── THE CLASS CONTRACT, READ FROM THE SCRIPTS, NOT INVENTED HERE ───────────
   Four phone scripts render the markup styled below. This block used to declare
   a .ddm-* contract that not one of those scripts had ever emitted, so every
   rule under it matched nothing and nobody saw an error: three of the four
   components inject their own baseline styles, so the screen looked plausible
   while the 44px floors, the dark theme and the stack layout this file exists to
   give them were silently absent. The names here are now copied out of the
   scripts. Verify a name against its script before adding a rule for it, and
   remember the .ddm-* prefix is NOT what they use.

     window.ddMobileBar   (dd-mobile-bar.js)
       .dd-mobile-bar             the fixed bar, one per page
       .dd-mobile-bar.is-yielded  keyboard is up, the bar is off screen, rule 11
       .dd-mobile-bar-readout     row 1, the muted gate line, omitted when no gate
       .dd-mobile-bar-actions     row 2
       .dd-mobile-bar-btn         every button in the bar
       .dd-mobile-bar-primary     exactly one, 60%+ width, 48px+ tall
       .dd-mobile-bar-secondary   at most one
       .dd-status-button          the labelled status button, its own textContent
       The bar PAINTS ITSELF: ensureStyles() injects the whole grammar on first
       mount and dd-mobile-bar.js:127 states outright that this file styles the
       page at 390px and not that bar. So the only bar rules here are the two the
       PAGE owes it, the scroll padding and the status button width. Do not
       re-declare its look: an injected stylesheet is appended to head at runtime,
       therefore it sits AFTER this link tag, therefore a one-class rule here
       loses the tie and the 60% rule ends up forked across two files.
       --dd-mobile-bar-h is published on the html element by applyPad from the
       measured height. The 84px below is only the value before the first measure.

     window.ddCollapse    (dd-mobile-collapse.js)
       .dd-collapse               on the card element
       .dd-collapse-open          expanded, .dd-collapse-closed collapsed. Both
                                  are set explicitly, so neither state needs :not()
       .dd-collapse-head          the 48px summary row, a <button>
       .dd-collapse-title         the small uppercase card name inside the head
       .dd-collapse-summary       the initials or the count, right of the title
       .dd-collapse-chev          the chevron, rotates when open
       .dd-collapse-body          everything hidden while collapsed
       [data-ddm-order="first"|"last"] on any card, to pin it in the stack. This
                                  one really is a ddm- name: the mount code stamps
                                  it and dd-mobile-collapse.js:78 depends on the
                                  order rules below reading it.
       This is the one component with no injected styles, so these rules are the
       whole look. It does set inline floors on the head and on show or hide, and
       an inline value beats anything here, so match those floors rather than
       fighting them.

     window.ddMoveMode    (dd-move-mode.js)
       .dd-move-btn               the one 44px per row control
       .dd-move-on                on the CONTAINER, not on body, while mode is open
       .dd-move-picked            the picked-up row, pinned and at full contrast
       .dd-move-dim               every other row, collapsed to one line
       .dd-move-bar               the host row or div of a Move here slot
       .dd-move-bar-btn           the full width Move here button inside it
       .dd-move-bar-when          the date that slot holds, inside the button
       .dd-move-strip             the up one / down one / Done row, inside the
                                  picked row, with .dd-move-strip-btn buttons
       .dd-move-moving            the MOVING pill beside the picked row's control

     window.ddNeedsWork   (dd-needs-work.js)
       .dd-needs-work             the header row wrapping the chip and the count
       .dd-nw-chip                the Only what needs work toggle, a <button>.
                                  Its on state is aria-pressed="true", not a class
       .dd-nw-count               the count. A SIBLING of the chip, not a child
       .dd-nw-note                the "10 posts are done" line under the list

   dd-move-mode.js and dd-needs-work.js do invite this file to restyle them, and
   both say a later stylesheet wins without !important. That is not true of an
   injected sheet, which is appended last. Their rules below therefore carry an
   `html ` prefix, one type selector, which is the cheapest way to outrank a
   one-class injected rule without reaching for !important.

   Shared utilities any card may use. These are opt-in, no script emits them, and
   they keep the ddm- prefix because they belong to this file rather than to a
   component: .ddm-field, .ddm-label, .ddm-head-line, .ddm-stack-actions,
   .ddm-scroll-x, .ddm-strip, .ddm-thumb-pending, .ddm-empty, .ddm-skeleton,
   .ddm-skeleton-line, .ddm-skeleton-block, .ddm-card-head, .ddm-card-state,
   .ddm-overflow-btn, .ddm-hover-only, .ddm-stack-table, .ddm-summary-row.

   ── THEME ──────────────────────────────────────────────────────────────────
   Dark mode follows the app convention, html[data-theme="dark"] overrides at the
   bottom, never a media query: the theme is a user setting in this app, not the
   operating system's. */

/* ── The scale ──────────────────────────────────────────────────────────────
   Declared unconditionally so a script can read them with getComputedStyle at
   any width, and so the three thumbnail sizes exist as three names rather than
   as seven numbers scattered through the file. Rule 15 is enforced by there
   being nothing else to reach for. */
:root {
  --ddm-tap: 44px;            /* minimum touch target, rule 6 */
  --ddm-tap-lg: 48px;         /* the bar's primary and the collapse head */
  --ddm-thumb-id: 40px;       /* identifies a row inside a summary line */
  --ddm-thumb-strip: 96px;    /* the browsable horizontal snap strip */
  --ddm-summary-h: 56px;      /* a collapsed row */
  --dd-mobile-bar-h: 84px;          /* the sticky bar, overridden by the bar itself */
  --ddm-gap: 12px;
  --ddm-radius: 10px;
  --ddm-motion: 180ms;        /* inside the 150ms to 300ms band, rule 10 */
}

@media (max-width: 760px) {

  /* ── Rule 1, no horizontal page scroll, ever ─────────────────────────────
     The document never scrolls sideways at 390px. .dd-wrap already owns the
     vertical scroll of the whole panel, so it is also the element that has to
     refuse the horizontal one. Everything under it is then given permission to
     shrink: without min-width:0 a flex or grid child sizes to its content and
     one long unbroken URL in a Notes card pushes the entire page 200px wide,
     which reads to a user as the app being broken rather than as one bad
     string. .dd-col-left and .dd-col-right already carry min-width:0
     (deliverable-dashboard.css:38); the cards inside them do not. */
  .dd-wrap {
    overflow-x: hidden;
    padding: 14px 12px 24px;
  }
  .dd-cols,
  .dd-card,
  .dd-posts-card {
    min-width: 0;
    max-width: 100%;
  }
  .dd-card img,
  .dd-card video,
  .dd-card iframe,
  .dd-posts-card img,
  .dd-posts-card video {
    max-width: 100%;
  }
  /* Long values break rather than push. anywhere, not break-all: it only breaks
     a token when the token cannot fit, so ordinary prose still wraps on spaces. */
  .dd-info-value,
  .dd-focus-value,
  .dd-cr-body,
  .dd-wd-linkurl,
  .dd-banner-link-url,
  .dd-team-name {
    overflow-wrap: anywhere;
  }
  .dd-wrap pre,
  .dd-wrap code {
    overflow-x: auto;
    max-width: 100%;
  }

  /* Anything genuinely wider than the viewport scrolls inside its own
     container, with a visible edge fade so it reads as scrollable rather than
     as cut off. overscroll-behavior contains the gesture so swiping a strip to
     its end does not drag the page or fire a pull-to-refresh, rule 10. */
  .ddm-scroll-x {
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
    mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
  }
  .ddm-scroll-x::-webkit-scrollbar { display: none; }

  /* ── Rule 3, the working card is first in the stack ──────────────────────
     .dd-cols collapses to one column at 1024px already, but DOM order then
     puts col 0 (Team, Brief) above col 1 above the full width cards, so the
     card the person opened the dashboard for is last. display:contents lifts
     the two column wrappers out of the box tree so every card becomes a direct
     flex child of .dd-cols and `order` can move any of them anywhere in the
     stack, which is impossible while they are trapped in a column div.
     Default order is 0, so DOM order is preserved for everything except the two
     reference cards, which are demoted by name. Anything else that needs
     pinning is stamped with data-ddm-order by the mount code. */
  .dd-cols {
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  .dd-col-left,
  .dd-col-right {
    display: contents;
  }
  /* Descendant selectors, not `.dd-cols > *`. display:contents changes the box
     tree, it does NOT change the DOM tree, so a card inside .dd-col-left is
     still a grandchild of .dd-cols and a child combinator would never match it.
     That is the whole reason a card stamped data-ddm-order="first" inside the
     left column would otherwise stay exactly where it was, with nothing to see
     in the styles panel to explain it. */
  .dd-cols > *,
  .dd-col-left > *,
  .dd-col-right > * { order: 0; }
  .dd-team-card { order: 6; }
  .dd-notes-activity { order: 7; }
  .dd-cols [data-ddm-order="first"] { order: -1; }
  .dd-cols [data-ddm-order="last"] { order: 8; }

  /* The demotion above is written for a FULL reference card and it demotes BY
     NAME, which is why it split a pair that is supposed to behave as one. On a
     configured Content Calendar layout the two reference cards render as
     .dd-team-card and as a plain .dd-card titled Notes, so only the Team name
     matched: measured at 390px on deliverable 12187, the Notes chip sat at
     y=211, above the working card, and the identical Team chip sat at y=7332,
     under a 7,043px Posts card, at the very bottom of the document. Two chips
     of the same 50px height, drawn by the same primitive, 7,000px apart.
     A collapse chip is not the card the demotion was written about. It is a
     48px summary row, it costs the stack almost nothing, and the whole point of
     collapsing it is that it stays reachable. So the state, not the name,
     decides: anything ddCollapse has turned into a chip is pinned above the
     working card, and the two chips end up adjacent where they belong.
     -2 is not an arbitrary number. dd-mobile-collapse.js:420-427 reserves the
     band between -100 and -1 as headroom for exactly this, so every card a
     caller names through ddMobileOrder (inline, -100 upward) still outranks
     this pin and rule 3 survives.
     Deliberately NOT scoped to .dd-collapse-closed. Scoping it to the closed
     state would send the card 7,000px down the page the instant somebody taps
     it open, which is the same defect with a tap in front of it. It expands
     where it was tapped, and the default state is the 50px chip. */
  .dd-cols .dd-card.dd-collapse { order: -2; }

  /* ── Rule 5, room for the sticky bar ─────────────────────────────────────
     The scroll container is padded by the bar height so the last row is never
     covered. Gated on the bar actually existing: an unconditional 96px of dead
     space at the foot of every dashboard that has no bar is a bug of its own.
     This is the pre-measure fallback and nothing more. applyPad() in
     dd-mobile-bar.js writes the real padding as an INLINE style on the same
     .dd-wrap, and an inline value replaces this one rather than adding to it, so
     the two cannot stack into double clearance. The 84px default only shows for
     the frame between the bar mounting and its first measurement, and on a page
     where the script published a bar it could not measure. */
  body:has(.dd-mobile-bar) .dd-wrap {
    padding-bottom: calc(var(--dd-mobile-bar-h) + env(safe-area-inset-bottom, 0px) + 16px);
  }

  /* ── Rule 6, 44px targets ────────────────────────────────────────────────
     .dd-col-act .dd-btn is 32x32 on desktop (deliverable-dashboard.css:518) and
     is the single most-missed row action on a phone. Everything that is a
     control gets the floor, and icon-only controls get it in both axes. */
  .dd-btn,
  .dd-media-upload,
  .dd-cr-addbtn,
  .dd-team-add,
  .dd-wd-note-add {
    min-height: var(--ddm-tap);
    padding: 10px 14px;
  }
  .dd-btn-sm {
    min-height: var(--ddm-tap);
    padding: 10px 12px;
    font-size: 13px;
  }
  .dd-col-act .dd-btn {
    width: var(--ddm-tap);
    height: var(--ddm-tap);
    padding: 0;
  }
  .dd-media-chip-x,
  .dd-popup-x,
  .dd-design-chip-x,
  .dd-team-del,
  .dd-wd-del,
  .dd-oa-del,
  .dd-cr-actions button {
    min-width: var(--ddm-tap);
    min-height: var(--ddm-tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .dd-team-member { min-height: var(--ddm-tap); }

  /* A destructive control never shares a row with a primary, and where the
     desktop layout puts them on one line the phone layout drives them to
     opposite ends of the block's header. .ddm-head-line is that header. */
  .ddm-head-line {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ddm-gap);
    min-height: var(--ddm-tap);
  }
  .ddm-head-line > .dd-btn-danger,
  .ddm-head-line > .ddm-danger { margin-left: auto; }
  /* Where they cannot be separated horizontally, they are separated
     vertically: a full width primary with the destructive control above it. */
  .ddm-stack-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  .ddm-stack-actions > * { width: 100%; justify-content: center; }

  /* ── Rule 8, a visible label above every input ───────────────────────────
     A native input renders the OS locale format and its own rendering is not a
     label, so `09/02/2026` beside `09:00` is genuinely ambiguous. The label
     sits above rather than beside it because a label beside a 170px input at
     390px costs the input the width it needs. The small caps treatment is the
     one the stacked posts table already produces through td[data-label]::before
     (deliverable-dashboard.css:553), copied here so a focus point card, a post
     card and a schedule row read as one design. */
  .ddm-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
  }
  .ddm-label,
  .ddm-field > label {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: #6b7280;
  }
  .ddm-field > input,
  .ddm-field > select,
  .ddm-field > textarea,
  .ddm-field > .dd-date-cell,
  .ddm-field > .dd-date-btn { width: 100%; }

  /* 16px is not a style choice. iOS Safari zooms the whole page in on focus of
     any input whose font-size is under 16px, and it never zooms back out, which
     leaves the person on a horizontally scrolled page rule 1 was written to
     prevent. Every text-entry control in the dashboard gets 16px. */
  .dd-wrap input,
  .dd-wrap select,
  .dd-wrap textarea {
    font-size: 16px;
    min-height: var(--ddm-tap);
    max-width: 100%;
  }
  .dd-wrap input[type="checkbox"],
  .dd-wrap input[type="radio"] {
    min-height: 0;
    width: 22px;
    height: 22px;
  }
  /* Label-beside-value rows become label-above-value blocks. At 390px the
     130px label column (deliverable-dashboard.css:197) leaves the value 200px
     and every second value truncates. */
  .dd-info-row,
  .dd-auction-detail-row,
  .dd-banner-sched-row {
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
  }
  .dd-info-label,
  .dd-auction-detail-label { min-width: 0; }

  /* ── Rule 2, tables stack, they do not shrink ────────────────────────────
     The posts table (deliverable-dashboard.css:524), the focus points table and
     the schedule line items each already stack at this breakpoint in their own
     stylesheet. .ddm-stack-table is the same transform as a reusable opt-in for
     every other table in a card, so a new table never has to reinvent it or,
     worse, be left as a 900px table inside a 366px card. The td label comes
     from data-label exactly as the posts table's does. */
  .ddm-stack-table,
  .ddm-stack-table tbody,
  .ddm-stack-table tr,
  .ddm-stack-table td {
    display: block;
    width: 100%;
    min-width: 0;
  }
  .ddm-stack-table thead { display: none; }
  .ddm-stack-table tr {
    position: relative;
    border: 1px solid #e5e7eb;
    border-radius: 12px;
    background: #fff;
    padding: 12px;
    margin-bottom: 12px;
  }
  .ddm-stack-table td {
    padding: 0 0 12px 0;
    border-bottom: 1px solid #f1f5f9;
  }
  .ddm-stack-table tr td:last-child {
    padding-bottom: 0;
    border-bottom: none;
  }
  .ddm-stack-table td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: #6b7280;
    margin-bottom: 7px;
  }
  /* The safety net under rule 1 for a table nobody remembered to stack: it is
     held to the card width and scrolls inside itself rather than widening the
     page. */
  .dd-posts-scroll,
  .dd-wrap table { max-width: 100%; }

  /* The stacked posts card: the delete pinned to the card's top right needs the
     full target, and the row heading has to clear it or a two-line post title
     runs underneath it. */
  .dd-posts-table td.dd-col-act .dd-btn {
    width: var(--ddm-tap);
    height: var(--ddm-tap);
  }
  .dd-posts-table td.dd-col-act {
    top: 8px;
    right: 8px;
  }
  .dd-posts-table td.dd-col-num { padding-right: 52px; }

  /* ── Rule 9, rich text stays short ───────────────────────────────────────
     The Quill snow toolbar is nine groups (ui/js/rich-editor.js:73): headers, a
     colour picker, a background picker, indent, link, blockquote, code block,
     clean. At 390px it wraps to three lines and the editor collapses to one
     visible line, so the row is mostly toolbar. The reduced set is bold,
     italic, list and emoji, which is not an arbitrary choice: it is exactly the
     set the legacy contenteditable toolbar has always offered
     (deliverable-dashboard.js:6486-6492), so the phone toolbar and the fallback
     toolbar are the same four controls.
     Hidden, not removed: the formats stay registered, so a caption pasted in
     with a heading or a link still RENDERS with it. Only the button to author a
     new one is gone. */
  /* MEASURED, not estimated. At 390px the caption card gives the toolbar a
     232px content box. Four controls in three .ql-formats groups is five 44px
     buttons, 220px, which fits. What did not fit was the CHROME around them:
     6px of horizontal padding a side and a 6px margin-right on each of the
     three groups is another 30px, so the row measured 282px in a 232px box.
     With `flex-wrap: nowrap` and no overflow rule the surplus had nowhere to
     go, so the last button, the emoji one, hung 15px past the card's right
     edge, and .dd-wrap's own `overflow-x: hidden` from rule 1 then CLIPPED it.
     The page did not scroll sideways, which is why a scrollWidth check passed
     while a real control was unreachable.
     So the chrome goes rather than the buttons: no horizontal padding on the
     row, no per-group margin, and the 4px gap does the separating. That is
     228px in a 244px box and it stays one line, which is what rule 9 wanted.
     `flex-wrap: wrap` is the safety net under that arithmetic, not a second
     design: a card that hands the toolbar a narrower box drops a group to a
     second line, which is longer but never hides a control. overflow-x is the
     net under THAT, for the one case wrapping cannot solve, a single group
     wider than the box, where it scrolls inside its own row instead of
     escaping the card. Clipping is what we are fixing, so it is never the
     answer here. */
  .dd-wrap .ql-toolbar.ql-snow {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 4px 0;
    box-sizing: border-box;
    max-width: 100%;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .dd-wrap .ql-toolbar.ql-snow::-webkit-scrollbar { display: none; }
  .dd-wrap .ql-toolbar.ql-snow .ql-formats {
    margin-right: 0;
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
  }
  .dd-wrap .ql-toolbar.ql-snow .ql-header,
  .dd-wrap .ql-toolbar.ql-snow .ql-underline,
  .dd-wrap .ql-toolbar.ql-snow .ql-strike,
  .dd-wrap .ql-toolbar.ql-snow .ql-indent,
  .dd-wrap .ql-toolbar.ql-snow .ql-link,
  .dd-wrap .ql-toolbar.ql-snow .ql-color,
  .dd-wrap .ql-toolbar.ql-snow .ql-background,
  .dd-wrap .ql-toolbar.ql-snow .ql-blockquote,
  .dd-wrap .ql-toolbar.ql-snow .ql-code-block,
  .dd-wrap .ql-toolbar.ql-snow .ql-clean {
    display: none;
  }
  /* The group a hidden control leaves behind is NOT :empty, and that distinction
     cost the row a whole second line. `display: none` removes a child from the
     BOX tree, never from the DOM, so the header group still holds two element
     children and `:empty` matches none of the nine groups. Measured at 390px on
     deliverable 12187: six groups (header, indent, link, colour, blockquote,
     clean) each drew 0px wide and each still claimed its 4px flex gap, so the
     row needed 220px of buttons + 32px of gap = 252px in a 232px box. It wrapped
     to two 44px rows, 102px tall, with the emoji button alone on the second row,
     which is the toolbar-taller-than-the-editor shape rule 9 exists to stop.
     :has() asks the real question the rules above imply, does this group still
     hold one of the four controls we kept, so the six dead groups leave the flex
     line entirely and their gaps go with them: 220px + 2 gaps = 228px in 232px,
     one row. :empty stays beside it because it costs nothing and still catches a
     genuinely childless group. A browser without :has() simply keeps today's
     two-row wrap, which is longer but still hides no control. */
  .dd-wrap .ql-toolbar.ql-snow .ql-formats:empty { display: none; margin: 0; }
  .dd-wrap .ql-toolbar.ql-snow
    .ql-formats:not(:has(.ql-bold, .ql-italic, .ql-list, .ql-emoji)) {
    display: none;
    margin: 0;
  }
  /* box-sizing is the other half of the overflow fix and it is the half that
     is easy to miss. ql-snow ships `padding: 3px 5px` on every toolbar button
     and the sheet is content-box, so `width: 44px` was really 54px on screen.
     Five of those is 270px in a 232px box, which is where the 282px measured
     row came from and why trimming only the padding and the group margins left
     it wrapping to two lines. With border-box the declared 44px IS the drawn
     44px, rule 6 is still satisfied to the pixel, and 5 x 44 + 2 x 4px gaps is
     228px, which fits 232px on one line. */
  .dd-wrap .ql-toolbar.ql-snow button,
  .dd-wrap .dd-cap-tool {
    box-sizing: border-box;
    padding: 0;
    width: var(--ddm-tap);
    height: var(--ddm-tap);
    flex: 0 0 auto;
  }
  /* 96px is the rule 9 floor for an editor. The caption editor at status 8 is
     the one field somebody genuinely composes on a phone and the spec allows it
     140px, which its own card sets; this floor does not fight that. */
  .dd-wrap .ql-editor,
  .dd-cap-quill .ql-editor,
  .dd-note-quill .ql-editor,
  .dd-cap-editor {
    min-height: 96px;
    font-size: 15px;
    line-height: 1.55;
  }
  /* The legacy contenteditable toolbar carries the same four controls, so it
     gets the same treatment and for the same reason: nowrap on a row that does
     not fit does not make it fit, it just decides which button leaves the card. */
  .dd-cap-toolbar {
    flex-wrap: wrap;
    gap: 4px;
    max-width: 100%;
    box-sizing: border-box;
  }
  /* The save tag is the only confirmation a person gets that their typing
     persisted, so it reads at the bottom of the field it reports on and never
     at the foot of a 3,600px card. */
  .dd-saving-tag {
    font-size: 12px;
    min-height: 16px;
    text-align: right;
  }

  /* ── Rule 15, three thumbnail sizes and no others ────────────────────────
     40px identifies a row inside a summary line. 96px is a browsable strip,
     always a horizontal snap strip and never a wrapping grid, so six images are
     one swipe rather than four rows of scroll. Full width is for reviewing, and
     that is the preview overlay and the client pages only.
     The build today has 40px, 56px, 120px and a handful of aspect-ratio grid
     tiles. The strays are normalised here rather than in six card files. */
  .dd-cr-shot-thumb,
  .dd-wd-a-thumb,
  .dd-focus-thumb,
  .dd-cr-shots img {
    width: var(--ddm-thumb-id);
    height: var(--ddm-thumb-id);
  }

  /* The media list becomes the 96px strip. The chip's filename goes: truncated
     to 60px it identifies nothing, and the thumbnail is the identity. The
     remove control overlays the tile's corner at full size rather than sitting
     beside a name that is no longer there. */
  .dd-media-items,
  .dd-post-images-grid,
  .ddm-strip {
    display: flex;
    flex-wrap: nowrap;
    gap: 8px;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .dd-media-items::-webkit-scrollbar,
  .dd-post-images-grid::-webkit-scrollbar,
  .ddm-strip::-webkit-scrollbar { display: none; }
  .dd-media-chip,
  .dd-post-image-cell,
  .ddm-strip > * {
    flex: 0 0 auto;
    scroll-snap-align: start;
  }
  .dd-media-chip {
    position: relative;
    padding: 0;
    border: none;
    background: transparent;
    box-shadow: none;
  }
  .dd-media-chip img,
  .dd-media-chip video,
  .dd-media-vico,
  .dd-media-dead-ico,
  .a4apu-img-thumb {
    width: var(--ddm-thumb-strip);
    height: var(--ddm-thumb-strip);
    flex: 0 0 var(--ddm-thumb-strip);
    max-height: var(--ddm-thumb-strip);
  }
  .dd-media-chip .dd-media-name { display: none; }
  .dd-media-chip .dd-media-chip-x {
    position: absolute;
    top: 0;
    right: 0;
    border-radius: 0 var(--ddm-radius) 0 var(--ddm-radius);
    background: rgba(255, 255, 255, .92);
    color: #b91c1c;
    box-shadow: 0 1px 3px rgba(15, 23, 42, .16);
  }
  /* A tile that is still uploading holds the slot at its final size, so the
     strip does not reflow under a thumb that has already started scrolling. */
  .ddm-thumb-pending {
    width: var(--ddm-thumb-strip);
    height: var(--ddm-thumb-strip);
    border: 1px dashed #cbd5e1;
    border-radius: var(--ddm-radius);
    background: #f8fafc;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    color: #94a3b8;
    flex: 0 0 var(--ddm-thumb-strip);
  }

  /* ── Rule 16, a real empty line and no bare spinner ──────────────────────
     A card with no rows renders one muted sentence naming the thing to do, in
     the card body rather than a blank box. A card still loading reserves the
     height its content will take, because an indefinite spinner on a phone is
     indistinguishable from a broken screen and a box that grows under a thumb
     mid-scroll moves what the thumb was aiming at. */
  .ddm-empty,
  .dd-media-empty,
  .dd-cr-empty,
  .dd-team-empty {
    display: block;
    width: 100%;
    padding: 14px 12px;
    border: 1px dashed #e5e7eb;
    border-radius: var(--ddm-radius);
    background: #fafbfc;
    color: #6b7280;
    font-size: 13px;
    line-height: 1.45;
    text-align: center;
  }
  .ddm-skeleton {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 4px 0;
  }
  .ddm-skeleton-line {
    height: 14px;
    border-radius: 7px;
    background: linear-gradient(90deg, #eef1f5 25%, #f6f8fa 37%, #eef1f5 63%);
    background-size: 400% 100%;
    animation: ddm-shimmer 1.4s ease-in-out infinite;
  }
  .ddm-skeleton-line.is-title { height: 18px; width: 45%; }
  .ddm-skeleton-line.is-short { width: 70%; }
  .ddm-skeleton-block {
    height: var(--ddm-thumb-strip);
    border-radius: var(--ddm-radius);
    background: #eef1f5;
  }
  /* .dd-loading is the build's existing one-line "Loading…" and it renders as a
     bare 14px sentence in an otherwise empty card. It keeps its sentence and
     gains the reserved height under it, so no card is a blank box while it
     waits and nothing jumps when the rows land. */
  .dd-loading {
    display: block;
    padding: 10px 0 0;
  }
  .dd-loading::after {
    content: "";
    display: block;
    margin-top: 10px;
    height: 52px;
    border-radius: var(--ddm-radius);
    background: linear-gradient(90deg, #eef1f5 25%, #f6f8fa 37%, #eef1f5 63%);
    background-size: 400% 100%;
    animation: ddm-shimmer 1.4s ease-in-out infinite;
  }

  /* ── Rule 4 and 13, the collapse primitive ───────────────────────────────
     Team and Notes render as a 48px summary row that expands on tap. The state
     itself lives in a module map in dd-mobile-collapse.js, outside the DOM,
     because these dashboards re-render on a debounced save and on a poll: state
     held in the DOM is destroyed on every re-render, and a caption autosave
     that slams every card shut while somebody is typing into one of them is the
     bug that arrangement exists to prevent. This file only draws it. */
  .dd-collapse { padding: 0; overflow: hidden; }
  .dd-collapse-head {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: var(--ddm-tap-lg);
    padding: 12px 14px;
    border: none;
    background: transparent;
    text-align: left;
    cursor: pointer;
    font: inherit;
    color: #111827;
  }
  .dd-collapse-head .dd-card-title { margin: 0; }
  .dd-collapse-summary {
    margin-left: auto;
    font-size: 12px;
    color: #6b7280;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 55%;
  }
  .dd-collapse-chev {
    flex: 0 0 auto;
    color: #9ca3af;
    transition: transform var(--ddm-motion) ease;
  }
  .dd-collapse-open .dd-collapse-chev { transform: rotate(180deg); }
  .dd-collapse-body { padding: 0 14px 14px; }
  .dd-collapse-closed > .dd-collapse-body { display: none; }

  /* The Team card expanded: chips stack one per row. Three across at 390px
     gives each chip about 110px and truncates every name to two characters. */
  .dd-collapse .dd-team-row { flex-direction: column; gap: 8px; }
  .dd-collapse .dd-team-member { width: 100%; }
  .dd-collapse .dd-team-controls { flex-direction: column; align-items: stretch; gap: 8px; }
  .dd-collapse .dd-team-add { width: 100%; justify-content: center; }

  /* A collapsed row inside a list, as opposed to a collapsed card. 56px, one
     line, tap to expand to the full card. */
  .ddm-summary-row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: var(--ddm-summary-h);
    padding: 8px 12px;
    border: 1px solid #e5e7eb;
    border-radius: var(--ddm-radius);
    background: #fff;
    text-align: left;
    font: inherit;
    font-size: 13px;
    color: #111827;
    cursor: pointer;
  }
  .ddm-summary-row img {
    width: var(--ddm-thumb-id);
    height: var(--ddm-thumb-id);
    object-fit: cover;
    border-radius: 7px;
    flex: 0 0 auto;
  }
  .ddm-summary-row .ddm-summary-meta {
    margin-left: auto;
    color: #6b7280;
    font-size: 12px;
    white-space: nowrap;
  }

  /* ── Rule 5, the sticky bottom bar ───────────────────────────
     The bar's whole look, the readout line, the actions row, the 60% primary,
     the yielded transform, the safe-area inset and its dark theme, lives in
     dd-mobile-bar.js ensureStyles(). It used to be restated here under names the
     script never emitted, which read as a second source of truth for the 60%
     rule while actually being inert. Restating it correctly would be worse: the
     injected sheet is appended to head on first mount, so it comes after this
     file and wins every one-class tie anyway.
     What is left is the one thing the PAGE owes the bar and the script does not
     own: the status button never gets wider than 40% of the row, so the primary
     keeps its 60%. The label is the button's own textContent, there is no inner
     span, so the ellipsis belongs on the button. */
  .dd-status-button {
    max-width: 40%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* ── The move mode, the reorder path for touch ───────────────────────────
     There is no touch drag support anywhere in this folder and HTML5 draggable
     never fires on a touch screen, so every reorderable list here needs a
     non-drag path. It is one control per row, not three: a grip plus an up plus
     a down plus a delete plus a number does not fit a 350px header line, and it
     is two mechanisms for one job. Never long press, which on iOS opens the
     system selection or share menu over any rich text, and there is an editor
     inside nearly every row this applies to. */
  /* Every rule from here to the end of the needs-work block carries a bare
     `html ` prefix. dd-move-mode.js and dd-needs-work.js both inject a one-class
     baseline sheet on first use and both invite this file to restyle them, on
     the belief that a later stylesheet wins without !important. It does not: an
     injected sheet is appended to head at runtime and therefore sits after this
     link tag, so a one-class rule here ties and loses. One type selector breaks
     the tie without an !important and without touching their files. */
  html .dd-move-btn {
    min-width: var(--ddm-tap);
    min-height: var(--ddm-tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 0 12px;
    border: 1px solid #d1d5db;
    border-radius: 8px;
    background: #fff;
    color: #374151;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
  }
  /* While the mode is open the page still scrolls vertically, because finding a
     drop bar means scrolling, but nothing sideways competes with it. */
  html .dd-move-on { touch-action: pan-y; }
  html .dd-move-picked {
    position: sticky;
    top: 0;
    z-index: 30;
    opacity: 1;
    border: 2px solid #2563eb;
    border-radius: var(--ddm-radius);
    background: #fff;
    box-shadow: 0 6px 18px rgba(37, 99, 235, .18);
  }
  /* Dimmed rows collapse to one line. Twelve full row cards with a drop bar
     between them is 4,000px of scroll to find a destination three rows away,
     and somebody in this mode is choosing a position, not reading descriptions. */
  html .dd-move-dim {
    opacity: .55;
    max-height: var(--ddm-summary-h);
    overflow: hidden;
    pointer-events: none;
  }
  html .dd-move-strip {
    display: flex;
    gap: 8px;
    margin-top: 10px;
  }
  html .dd-move-strip-btn {
    flex: 1 1 0;
    min-height: var(--ddm-tap);
    justify-content: center;
  }
  html .dd-move-bar-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    min-height: var(--ddm-tap);
    margin: 6px 0;
    border: 1px dashed #93c5fd;
    border-radius: 8px;
    background: #f8faff;
    color: #1d4ed8;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
  }
  html .dd-move-bar-btn:active { background: #eef4ff; }
  /* The slot is named by the date it holds, because "between row 4 and row 5"
     is not something anybody can check against a month plan. */
  html .dd-move-bar-when {
    font-weight: 500;
    color: #64748b;
  }

  /* ── Rule 14, the one "Only what needs work" chip ────────────────────────
     Same wording and same place on all five work-list statuses. On by default
     whenever the gate reports anything outstanding. The hidden rows come back
     as 56px summary rows rather than vanishing, which is what keeps it a filter
     rather than a lie about how many posts exist. */
  html .dd-nw-chip {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: var(--ddm-tap);
    padding: 0 14px;
    border: 1px solid #d1d5db;
    border-radius: 999px;
    background: #fff;
    color: #374151;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
  }
  html .dd-nw-chip[aria-pressed="true"] {
    border-color: #2563eb;
    background: #eff6ff;
    color: #1d4ed8;
  }
  html .dd-nw-count {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }
  html .dd-nw-note {
    padding: 12px 4px 2px;
    font-size: 12px;
    color: #9ca3af;
    text-align: center;
  }

  /* ── Card header on a phone ──────────────────────────────────────────────
     Title, state line and the overflow control. Anything that would be a third
     control in the sticky bar lives behind this card header's overflow. */
  .dd-posts-head,
  .ddm-card-head {
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
  }
  .ddm-card-state {
    flex: 1 0 100%;
    font-size: 12px;
    color: #6b7280;
    overflow-wrap: anywhere;
  }
  .ddm-overflow-btn {
    margin-left: auto;
    min-width: var(--ddm-tap);
    min-height: var(--ddm-tap);
  }

  /* ── Rule 10, motion ─────────────────────────────────────────────────────
     Internal scrollers keep their gesture to themselves so a strip does not
     drag the page or fire a pull-to-refresh. */
  .dd-posts-scroll,
  .ddm-scroll-x,
  .ddm-strip { overscroll-behavior: contain; }

  /* ── Rule 3 inside the post card: field order is a MODE, not DOM order ────
     8.5 puts media above captions, 8.7 puts change requests above media and 8.8
     wants the artwork first with the caption third. buildPostRow appends one
     fixed sequence (num, date, caption, media, change requests,
     deliverable-dashboard.js:6608 onward) and the stacked row is display:block
     (deliverable-dashboard.css:539), which makes `order` inert, so none of the
     three orders could be expressed at all. Making the stacked row a flex
     column is the whole fix: the cells are already display:block, they become
     flex items unchanged, and `order` finally applies.

     WHY THE DEFAULT IS THE DESIGN ORDER AND NOT DOM ORDER.
     Media above captions is what BOTH the design statuses (8.5) and the
     editorial ones (8.8) ask for: the designer scrolls past the caption editor
     every time to reach the upload block, and the writer needs the artwork
     above the caption they are writing against it. So the default here is that
     order and it needs no class, which means it is right even on a status
     nobody has stamped. Only the change statuses inverting change requests over
     media needs to be said out loud.

     The focus point button is NOT orderable from here: ddFocusPointPop mounts
     it inside the caption cell (deliverable-dashboard.js:6749-6766), so it
     travels with the caption. That satisfies 8.8 exactly, where it is meant to
     sit directly above the caption editor, and leaves 8.5 with the button one
     field lower than its wireframe draws it. Moving it is a DOM change in a
     file this block does not own.

     CONTRACT CONSUMED, and it is one class: `.is-changes` on the
     .dd-posts-table at the three statuses buildPostRow already computes as
     CR_OPEN_STATUSES (deliverable-dashboard.js:6818). Absent, the default
     order stands and nothing breaks; present, 8.7's inversion lands. */
  .dd-posts-table tbody tr {
    display: flex;
    flex-direction: column;
  }
  .dd-posts-table td.dd-col-num { order: 1; }
  .dd-posts-table td.dd-col-date { order: 2; }
  .dd-posts-table td.dd-col-media { order: 3; }
  .dd-posts-table td.dd-col-cap { order: 4; }
  .dd-posts-table td.dd-col-cr { order: 5; }
  .dd-posts-table td.dd-col-sched { order: 6; }
  /* The delete cell is position:absolute in the stacked layout
     (deliverable-dashboard.css:578), so it is out of flow and out of the order
     conversation entirely. It is listed anyway so a future change that puts it
     back in flow does not land it in the middle of the card. */
  .dd-posts-table td.dd-col-act { order: 0; }

  /* 8.7: the designer is here to clear a list, so the list comes first. The
     change request block already defaults open at these statuses and already
     scrolls its first unresolved request into view; this puts it where that
     scroll lands, directly under the post header rather than under three
     collapsible media blocks. */
  .dd-posts-table.is-changes td.dd-col-cr { order: 3; }
  .dd-posts-table.is-changes td.dd-col-media { order: 4; }
  .dd-posts-table.is-changes td.dd-col-cap { order: 5; }

  /* The divider rules in deliverable-dashboard.css:586 assume the change
     request cell is visually last and strip its border. Reordered, the last
     cell is the caption, so the border follows the position rather than the
     DOM: a card whose final field carries a rule under it reads as an unclosed
     block. */
  .dd-posts-table.is-changes td.dd-col-cr {
    padding-bottom: 14px;
    border-bottom: 1px solid #f1f5f9;
  }
  .dd-posts-table.is-changes td.dd-col-cap {
    padding-bottom: 0;
    border-bottom: none;
  }

  /* ── 8.8, the caption editor gets 140px at the editorial statuses ─────────
     Rule 9's floor is 96px and this is the one field on the one status where
     somebody is genuinely composing on a phone, so the spec raises it here and
     only here. The general floor above says "which its own card sets" and no
     card set it.

     The editorial statuses are detected STRUCTURALLY rather than by status
     token, for the same reason dd-posts-mode.js refuses a status list: the
     chain is editable and a literal list rots the moment somebody adds an
     editorial status. Editorial mode is the only path that drops the actions
     cell from a row (deliverable-dashboard.js:6874, header <th> included at
     :6267), so a row with no .dd-col-act IS an editorial row. :has() is already
     relied on in this file for the Quill toolbar sweep, so this needs no
     support the block above does not.

     .dd-mode-editorial is honoured as well and wins nothing extra: if the main
     thread stamps the mode on the table, this keeps working off that instead of
     off the missing cell, and neither reading can contradict the other. */
  .dd-posts-table tbody tr:not(:has(> td.dd-col-act)) .dd-cap-editor,
  .dd-posts-table tbody tr:not(:has(> td.dd-col-act)) .dd-cap-quill .ql-editor,
  .dd-posts-table.dd-mode-editorial .dd-cap-editor,
  .dd-posts-table.dd-mode-editorial .dd-cap-quill .ql-editor {
    min-height: 140px;
  }
}

/* ── Rule 7, hover does not exist ─────────────────────────────────────────
   Every hover-revealed control renders permanently on a touch device. This is
   the baseline block; the per-control sweep over the dashboard's own hover
   surfaces is appended here by the hover-free pass, which owns this file after
   this point. Keep new hover-free rules inside this block so there is one place
   to look.
   @media (hover: none) rather than the width query: a touch laptop at 1200px
   has the same problem and a mouse at 390px does not. */
@media (hover: none) {
  .ddm-hover-only {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
  }
  /* A control that only exists on hover of its row is a control that does not
     exist on a phone. These are the row-level reveals in the dashboard today. */
  .dd-media-chip-x,
  .dd-cr-shot-ov,
  .dd-design-chip-x {
    opacity: 1;
  }
}

/* ── Reduced motion ───────────────────────────────────────────────────────
   The shimmer is the one thing here that animates indefinitely, so it stops
   outright rather than slowing down; a still block still reserves the height,
   which is the part of rule 16 that matters. Sheets drop their slide to a fade
   in dd-sheet.js, not here. */
@media (prefers-reduced-motion: reduce) {
  .ddm-skeleton-line,
  .dd-loading::after {
    animation: none;
    background: #eef1f5;
  }
  /* The bar is not listed here: dd-mobile-bar.js ships its own reduced-motion
     rule in the sheet it injects, and that sheet lands after this file. */
  .dd-collapse-chev { transition: none; }
}

@keyframes ddm-shimmer {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* ── Dark theme ───────────────────────────────────────────────────────────
   html[data-theme="dark"] and never prefers-color-scheme: the theme is a user
   setting in this app. Every override is nested inside the same width query as
   the rule it overrides, or it would apply at desktop widths where the rule it
   is correcting does not exist. */
@media (max-width: 760px) {
  html[data-theme="dark"] .ddm-label,
  html[data-theme="dark"] .ddm-field > label,
  html[data-theme="dark"] .ddm-stack-table td[data-label]::before { color: #94a3b8; }
  html[data-theme="dark"] .ddm-stack-table tr {
    background: #0f172a;
    border-color: #334155;
  }
  html[data-theme="dark"] .ddm-stack-table td { border-bottom-color: #1e293b; }
  html[data-theme="dark"] .ddm-empty,
  html[data-theme="dark"] .dd-media-empty,
  html[data-theme="dark"] .dd-cr-empty,
  html[data-theme="dark"] .dd-team-empty {
    background: #111c30;
    border-color: #334155;
    color: #94a3b8;
  }
  html[data-theme="dark"] .ddm-skeleton-line,
  html[data-theme="dark"] .dd-loading::after {
    background: linear-gradient(90deg, #1e293b 25%, #26364f 37%, #1e293b 63%);
    background-size: 400% 100%;
  }
  html[data-theme="dark"] .ddm-skeleton-block { background: #1e293b; }
  html[data-theme="dark"] .dd-collapse-head { color: #e2e8f0; }
  html[data-theme="dark"] .dd-collapse-summary { color: #94a3b8; }
  html[data-theme="dark"] .dd-collapse-chev { color: #64748b; }
  html[data-theme="dark"] .ddm-summary-row {
    background: #0f172a;
    border-color: #334155;
    color: #e2e8f0;
  }
  html[data-theme="dark"] .ddm-summary-row .ddm-summary-meta { color: #94a3b8; }
  /* No dark rules for the bar. It injects its own html[data-theme="dark"]
     overrides, at the same specificity and later in the document, so anything
     written here would lose the tie and read as a broken theme rather than as a
     losing rule. Its dark palette lives in dd-mobile-bar.js:170. */
  html[data-theme="dark"] .dd-move-btn {
    background: #0f172a;
    border-color: #334155;
    color: #e2e8f0;
  }
  html[data-theme="dark"] .dd-move-picked {
    background: #0f172a;
    border-color: #60a5fa;
    box-shadow: 0 6px 18px rgba(96, 165, 250, .22);
  }
  html[data-theme="dark"] .dd-move-bar-btn {
    background: #14243d;
    border-color: #3b82f6;
    color: #93c5fd;
  }
  html[data-theme="dark"] .dd-move-bar-when { color: #94a3b8; }
  html[data-theme="dark"] .dd-nw-chip {
    background: #0f172a;
    border-color: #334155;
    color: #e2e8f0;
  }
  html[data-theme="dark"] .dd-nw-chip[aria-pressed="true"] {
    background: #14243d;
    border-color: #3b82f6;
    color: #93c5fd;
  }
  html[data-theme="dark"] .dd-nw-note,
  html[data-theme="dark"] .ddm-card-state { color: #7d8a9c; }
  html[data-theme="dark"] .ddm-thumb-pending {
    background: #0f172a;
    border-color: #334155;
    color: #64748b;
  }
  html[data-theme="dark"] .dd-media-chip .dd-media-chip-x {
    background: rgba(15, 23, 42, .92);
    color: #f87171;
  }
}
