/* ==========================================================================
   Design System v2.1 — SECTION VIEW ("Overlays")
   The panels the dock opens, and the nav that replaces the dock.
   --------------------------------------------------------------------------
   Source: Figma "ℹ️ Overlays" canvas (841:1061) for the panels, and the
   🖥️ Lead Times canvas (1300:1639) for the surrounding layout.

   THESE ARE NOT MODALS. That was my first read and it was wrong.

   Opening a section swaps the shell's contents: the dock is replaced by a 230px
   left nav, and the panel sits beside it. You never dismiss a panel — you move
   between sections from the nav, which stays live the whole time. So there is no
   close button, and a modal would be actively wrong: showModal() makes
   everything behind it inert, which would disable the very nav you navigate with.

   The geometry is the giveaway that this is one layout rather than a thing on
   top of another. From the Lead Times frame:

       nav      x=120  w=230
       gap             40
       panel    x=390  w=930
       right edge      1320

   230 + 40 + 930 = 1200 — the same measure as the dock and the status bar. The
   section view sits in the shell's existing column, not over it.

   Container spec from Request_Patient_Order/Desktop/Light (920:5923):
     background  --colour/seafoam (#EFF7F5) — OPAQUE, not glass
     radius      24px  (--radius-panel)
     padding     60px 40px
     gap         30px

   Only the dock, status bar and pills are glass. Inside a panel we are on a
   normal surface and use the normal semantic roles — --surface-page,
   --text-body — not the on-wallpaper ones.
   ========================================================================== */

/* ── View switching ──────────────────────────────────────────────────────
   One attribute on the shell decides which contents are live: data-v2-view is
   "home" or "section". Driving it from a single attribute rather than toggling
   classes on four elements means the two states can't half-apply. */
.v2-shell[data-v2-view="home"] .v2-section { display: none; }

.v2-shell[data-v2-view="section"] .v2-greeting,
.v2-shell[data-v2-view="section"] .v2-dash-nav { display: none; }

/* THE POWERED-BY FOOTER IS DASHBOARD-ONLY, and this is measured, not a preference.
   -----------------------------------------------------------------------------
   The Homepage frames carry a poweredByPC instance (808:6006 dark, 808:6078
   light). NOT ONE section frame does — zero across the Lead Times and Resources
   canvases, which between them hold twelve section frames. Their top-level
   children are only ever wallpaper, StatusBar, Frame 463 and the LeftNav items.

   Carrying it through was costing the panel ~150px (96px of padding plus a 27px
   wordmark plus its share of the row), which is what made Lead Times scroll. I had
   started shaving the top padding and the footer's own padding to claw the space
   back — treating the symptom. Daniel spotted the actual cause (14-Aug); that
   tightening has been reverted with this, and the composition he asked for on the
   dashboard is untouched.

   Budget at his ~853px viewport, with the footer gone and nothing else tightened:
   77 top + 45 bar + 85 gap = 207 of chrome, leaving 646 for a panel that needs
   527. Clears by ~119px. Below roughly 710px it will scroll again, which is
   correct — that is what the in-panel scrollbar is for, and the frames' own
   Resources panel is 1346px tall and scrolls by design. */
.v2-shell[data-v2-view="section"] .v2-poweredby { display: none; }

.v2-section {
    display: grid;
    /* THE PANEL COLUMN IS A MAXIMUM, NOT A FIXED WIDTH — and it was fixed,
       which left a broken band between the breakpoints.
       -------------------------------------------------------------------------
       230 + 40 + 930 is exactly 1200px, and the two-column layout only gives way
       to the single-column one at 1024px. So every viewport from 1025 to 1199
       asked for 1200px of tracks in less than 1200px of container, and with
       justify-content: center the surplus is split BOTH ways — so the overflow
       went off the left edge as well as the right, where no amount of scrolling
       reaches it (Daniel, 2-Sep: "for a small window the desktop view could be
       spanning off page?").

       It is not a hypothetical width either: iPad Pro 11" in landscape reports
       1194px, which lands squarely in the band, and any browser window that
       isn't maximised on a 1440p display can sit there too.

       minmax(0, 930px) makes 930 the ceiling rather than the demand: at the
       frame's width nothing changes at all, and below it the panel gives back
       what the nav and the gap need instead of overflowing. The 0 lower bound is
       load-bearing — `minmax(auto, 930px)` would floor the track at its content's
       min-content width and reintroduce the overflow through the back door.

       This is a SHELL fix, so it changes all eight panels, not just My Orders.
       Everything inside them is already fluid (the record grid is auto-fill, the
       filter row wraps, the head stacks), so narrowing is handled — but the other
       panels are worth a look at ~1100px now that they will actually render
       there. */
    grid-template-columns: 230px minmax(0, 930px);
    gap: 40px;
    justify-content: center;
    align-items: start;
    width: 100%;
}

/* Breathing room between the status bar and the content, which the frame has and
   I had collapsed to the shell's default 32px.
   The frame: status bar bottom at y=165, panel top at y=265 — a 100px gap.
   Held as a proportion with a 100px ceiling so it doesn't eat a short laptop
   screen, where a fixed 100px would push the panel below the fold. */
.v2-shell[data-v2-view="section"] .v2-dash-mid {
    padding-top: clamp(var(--space-8), 10vh, 100px);

    /* ONE ROW IN SECTION VIEW, and this is why the panel stopped short of the
       bottom (Daniel, 14-Aug: "the panel isnt expanded, its collapsed").

       .v2-dash-mid is `1fr auto` with a 24px row-gap, because on the dashboard
       row 2 holds the dock. In section view the dock is display: none — so row 2
       collapses to zero, BUT THE ROW GAP STILL APPLIES. The panel was losing 24px
       off its bottom edge to a gap between a row of content and a row of nothing.

       Collapsing the template to a single row removes the phantom outright, which
       is better than zeroing the gap: there genuinely is only one row here. */
    grid-template-rows: 1fr;
    row-gap: 0;
}

/* The nav's first item is FLUSH with the panel's top edge.
   -----------------------------------------------------------------------------
   This is the one place the file contradicts itself, so the reasoning is
   recorded rather than the value alone.

   Every production section frame offsets the nav by 60px — panel at y=265, nav
   (Frame 463) at y=325 — which is exactly the panel's own top padding, so the
   nav's first LABEL lines up with the panel's TITLE. That was implemented first
   and it is what those frames say:

       Register a patient   1127:9218 / 1164:9555     nav y=325, panel y=265
       Resources            1195:10714 / 1195:10923   nav y=325, panel y=265
       Lead Times           1300:1709 / 1300:1720     nav y=325, panel y=265
       Lead Times/Category  1300:2371 / 1300:2429     nav y=325, panel y=265

   The Dashboard_One_Dark_menu comp puts both at y≈243 — flush top edges, no
   label/title alignment. Daniel called it against the render on 2026-08-11 and
   that is what's built here: two hard edges agreeing reads as deliberate, where
   a 60px drop reads as the nav having slipped.

   OPEN with Justin — six production frames say otherwise, and if they win this
   is one line back. Deliberately expressed as a single declaration for exactly
   that reason. */


/* ── Left nav ────────────────────────────────────────────────────────────
   Four primary sections, then Back to dashboard, then the secondary group.
   Items are 230×44 with a 12px radius, a 15px Medium Seafoam label and an icon
   (LeftNavSelection 979:297, LeftNavSecondary 985:3).

   The items sit directly on the wallpaper — no panel behind them — so they take
   the on-wallpaper text roles, not --text-body. */
/* THE SECONDARY GROUP IS ANCHORED TO THE BOTTOM, not spaced off the primary one.
   -----------------------------------------------------------------------------
   Daniel checked the comp again (14-Aug): Resources / Lead times / Latest updates
   sit low on the page, near the foot of the column. In Dashboard_One_Dark_menu the
   last of them ends around 87% of the frame height; the production frames agree,
   putting them at y=772–904 in a 1024 frame — the bottom fifth.

   This was a margin-top off the primary group, so their position depended on where
   that group ENDED and they floated mid-column on a tall screen — about 62–72% in
   Daniel's render rather than the comp's 80–87%.

   space-between plus a stretched column anchors them instead: primary at the top,
   secondary at the foot, correct at any height and with no magic gap. It needs
   align-self: stretch because .v2-section is align-items: start, so the nav would
   otherwise size to its content. */
.v2-sidenav {
    display: grid;
    gap: 10px;                        /* Frame 463's own gap */
    align-content: space-between;
    align-self: stretch;
}

.v2-sidenav-group {
    display: grid;
    gap: 10px;
}

/* The secondary group differs from the primary one twice over.
   -----------------------------------------------------------------------------
   It sits TIGHTER internally. The two groups do not share a rhythm, which is
   easy to miss and reads as an error once you've seen it:

       primary    Frame 463 children at y = 0, 54, 108, 162   → 44 + 10 gap
       secondary  LeftNav/Secondary at y = 772, 816, 860      → 44, i.e. NO gap

   A 44px pitch on a 44px item means the three sit flush against each other.

   And it sits a long way DOWN — y=325 for the primary block against y=772 for
   these, i.e. the bottom fifth of a 1024 frame. That distance is no longer a
   margin: the column is stretched and align-content: space-between anchors this
   group to the foot, which holds at any height. See the note on .v2-sidenav.

   The margin-bottom keeps it clear of the column's bottom edge rather than sitting
   flush with it. The frame puts these items at y=772–904 in a 1024 frame — ending
   at 88%, with ~120px below — so 13vh capped at 120 reproduces both ends: 120px at
   the design height, 111px at Daniel's 853, which lands the group at 77–88%
   against the comp's 80–87%. */
.v2-sidenav-group--secondary {
    gap: 0;
    margin-bottom: clamp(var(--space-5), 13vh, 120px);
}

.v2-sidenav-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);              /* 12px */
    width: 100%;
    min-height: 44px;
    padding: var(--space-2) var(--space-4);
    border: 0;
    border-radius: var(--radius-button);   /* 12px */
    background: transparent;
    color: var(--on-glass-label);
    font-size: var(--type-body-size);      /* 15px */
    font-weight: 500;                      /* Medium */
    text-align: left;
    text-decoration: none;
    cursor: pointer;
    transition: background-color 120ms ease, color 120ms ease;
}

/* Hover takes the themed glass fill. It was a hardcoded white wash, which is the
   wrong direction on dark glass — there the fill deepens rather than lightens.
   Same token the pills use, so the two hover identically; see --glass-fill-hover
   in components.css, which nothing had defined. */
.v2-sidenav-item:hover {
    background-color: var(--glass-fill-hover);
    color: var(--on-wallpaper);
}

/* The selected section — now MEASURED, not inferred.
   -----------------------------------------------------------------------------
   The previous pass used the glass treatment because the codegen had only
   returned the "Unselected" variant. The Selected variants do exist
   (979:296 AddNewPatient, 979:302 RequestPatientOrder) and say something else:
   an OPAQUE Flint pill with the section's accent as both label and icon, and no
   weight change — the label stays Roboto Medium in both states, so colour and
   fill carry the whole signal.

       background   colour/flint  #3A4A50   opaque, no backdrop-filter
       radius       12px
       label        15px Medium, section accent
       icon         same accent

   --nav-accent is set per item by the view, the same way --heading-colour is
   set per panel, so nothing here needs to know which section it is in. The
   weights behind it are stepped for contrast — see semantic.css.

   aria-current stays the hook: the style follows the accessible state rather
   than a class kept in parallel with it, so they cannot disagree.

   :hover is included so the generic hover above can't paint white over the
   Flint pill — the selected item is already at its final state, so there is
   nothing for hover to add. */
.v2-sidenav-item[aria-current="page"],
.v2-sidenav-item[aria-current="page"]:hover {
    background-color: var(--nav-selected-surface);
    color: var(--nav-accent, var(--nav-selected-default));
    --nav-glyph: var(--nav-accent, var(--nav-selected-default));   /* the icon
                                     below is masked, so it takes the same
                                     value through its background-color */
}

/* ── Nav icons ───────────────────────────────────────────────────────────
   Rendered as a MASK rather than an <img>, because the selected state recolours
   them and an <img> can't be tinted. The exported SVG is still the only source
   of the shape — it's the mask image — so no glyph is redrawn, but the colour
   now comes from the cascade and follows both the theme and the accent.

   That also fixes a latent bug: the exports are hard-coded #EFF7F5, so before
   this the labels themed and the icons could not.

   ONE 28px BOX for every icon, with the glyph fitted inside it. The exports are
   deliberately unlike each other — 27×20, 24×24, 26×26, 38.8×18.5, 26.7×30.6 —
   and the Figma component absorbs that by giving each section its own padding
   and gap (AddNewPatient px16/gap12, RequestPatientOrder pl19/gap9). Carried
   over literally that leaves the labels in a vertical list starting at five
   different x positions, which is an artefact of auto-layout hugging different
   vectors, not a design intent. `contain` keeps every aspect ratio honest inside
   a common optical column instead.

   THREE THINGS THIS GOT WRONG, all of which made every icon vanish rather than
   look wrong — which is a far harder symptom to read back.

   1. THE MASK URL MUST LIVE IN THIS STYLESHEET, not in a custom property set by
      an inline style attribute. That was the real cause. A relative url() inside
      a custom property has no single agreed base: the spec resolves it against
      the stylesheet where the property was DECLARED (for an inline style, the
      document — /portal/, making the path correct), but engines have long
      resolved it against the stylesheet where the var() is USED. Resolved that
      second way, `assets/v2/img/nav/x.svg` becomes
      /portal/assets/v2/ + assets/v2/img/nav/x.svg — a 404, and a mask-image
      that fails to load masks the element out completely.

      Declared here, the base is this file and the path is unambiguous. It costs
      the filemtime cache-bust the inline version had, which is no loss: the
      icons are immutable per filename, unlike the CSS.

      SINCE SUPERSEDED, and the reasoning is worth keeping because it explains
      the shape of the fix. Every mask here is now an INLINE DATA URI (26-Aug),
      because a file reference had a second failure mode with the same symptom:
      the browser cannot even discover a mask URL until the CSS has parsed and
      the rule has matched an element, and until the SVG arrives the element is
      masked out — invisible, not unstyled. Twelve of those on first paint is
      what Daniel kept seeing (26-Aug: "the landing lag where icons etc dont
      seem to generate on web page land"). A data URI arrives WITH the
      stylesheet, so there is no window in which the mask is missing.

      It cost 18KB of CSS for 12 icons — after stripping their comments and
      collapsing the prefixed/unprefixed pair onto one --v2-mask custom
      property, which is only safe now: a data URI has no relative base to
      resolve, which is the whole hazard point 1 above is about. The source SVGs
      stay in img/ — several are still used as <img src>, and those requests
      start during HTML parse rather than after CSS, so they were never the
      problem.

   2. `display: block` is not optional. The old markup was an <img> — a REPLACED
      element, where width and height always apply. A <span> is not, so it only
      takes a size once something blockifies it. These are flex items of the nav
      button, which does blockify them, but relying on a parent's display for a
      child's box to exist at all breaks the moment the parent changes.

   3. Masks are set as LONGHANDS, never the `mask` shorthand. A shorthand
      containing var() is all-or-nothing at computed-value time: one bad custom
      property invalidates the whole thing and silently resets mask-position,
      mask-size and mask-repeat with it. */
.v2-sidenav-icon {
    display: block;
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    background-color: var(--nav-glyph, var(--on-wallpaper));
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-position: center;
            mask-position: center;
    -webkit-mask-size: contain;
            mask-size: contain;
}

/* Keyed off data-v2-overlay, which the markup already carries to name the
   section — so the icon needs no second source of truth for which one it is.
   Paths are relative to THIS FILE (assets/v2/), per note 1 above. */
.v2-sidenav-item[data-v2-overlay="add-patient"]   .v2-sidenav-icon { --v2-mask: url("data:image/svg+xml,%3Csvg preserveAspectRatio='none' overflow='visible' style='display: block;' width='27' height='20' viewBox='0 0 27 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath id='Subtract' d='M23 0C25.2091 0 27 1.79086 27 4V16C27 18.2091 25.2091 20 23 20H4C1.79086 20 3.22133e-08 18.2091 0 16V4C0 1.79086 1.79086 1.61064e-08 4 0H23ZM13.6875 13C10.2153 13 7.2606 14.6334 6.1543 16.916C5.88026 17.4816 6.35308 18.0622 6.98145 18.0625H20.3936C21.0219 18.0622 21.4947 17.4816 21.2207 16.916C20.1144 14.6334 17.1597 13 13.6875 13ZM13.5 3C11.2909 3 9.5 4.79086 9.5 7C9.5 9.20914 11.2909 11 13.5 11C15.7091 11 17.5 9.20914 17.5 7C17.5 4.79086 15.7091 3 13.5 3Z' fill='%23EFF7F5'/%3E%3C/svg%3E");   -webkit-mask-image: var(--v2-mask);   mask-image: var(--v2-mask); }
.v2-sidenav-item[data-v2-overlay="request-order"] .v2-sidenav-icon { --v2-mask: url("data:image/svg+xml,%3Csvg preserveAspectRatio='none' overflow='visible' style='display: block;' width='26.7342' height='30.6027' viewBox='0 0 26.7342 30.6027' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath id='&%23244;&%23130;&%23189;&%23177;' d='M10.6734 24.1613L9.02849 26.1229H3.7771C1.26716 26.1229 0 24.8558 0 22.3093V3.82584C0 1.29152 1.26716 0 3.7771 0H16.7167C19.2389 0 20.4938 1.29152 20.4938 3.82584V12.501L18.5322 14.8282V3.86239C18.5322 2.64397 17.8864 1.96166 16.6192 1.96166H3.88676C2.6196 1.96166 1.96166 2.64397 1.96166 3.86239V22.2727C1.96166 23.4911 2.6196 24.1613 3.88676 24.1613H10.6734ZM5.64128 7.05465C5.19047 7.05465 4.87368 6.72568 4.87368 6.29923C4.87368 5.88497 5.19047 5.55599 5.64128 5.55599H14.8769C15.3034 5.55599 15.6323 5.88497 15.6323 6.29923C15.6323 6.72568 15.3034 7.05465 14.8769 7.05465H5.64128ZM5.64128 11.3069C5.19047 11.3069 4.87368 10.978 4.87368 10.5515C4.87368 10.1373 5.19047 9.80828 5.64128 9.80828H14.8769C15.3034 9.80828 15.6323 10.1373 15.6323 10.5515C15.6323 10.978 15.3034 11.3069 14.8769 11.3069H5.64128ZM25.2335 14.1215L22.9185 12.1842L24.0272 10.8805C24.5755 10.2469 25.2335 10.1251 25.8183 10.6246L26.2691 11.0023C26.8662 11.5019 26.8905 12.1598 26.3301 12.8178L25.2335 14.1215ZM12.4644 29.2786L9.80828 30.5702C9.52804 30.7042 9.19907 30.3996 9.28436 30.1072L10.186 27.3535L21.9803 13.3051L24.2709 15.2302L12.4644 29.2786ZM5.64128 15.547C5.19047 15.547 4.87368 15.2181 4.87368 14.7916C4.87368 14.3652 5.19047 14.0484 5.64128 14.0484H14.8769C15.3034 14.0484 15.6323 14.3652 15.6323 14.7916C15.6323 15.2181 15.3034 15.547 14.8769 15.547H5.64128ZM14.8769 18.3007C15.0962 18.3007 15.279 18.386 15.413 18.5322L14.353 19.7993H5.64128C5.19047 19.7993 4.87368 19.4704 4.87368 19.0561C4.87368 18.6296 5.19047 18.3007 5.64128 18.3007H14.8769Z' fill='%23EFF7F5'/%3E%3C/svg%3E"); -webkit-mask-image: var(--v2-mask); mask-image: var(--v2-mask); }
.v2-sidenav-item[data-v2-overlay="my-patients"]   .v2-sidenav-icon { --v2-mask: url("data:image/svg+xml,%3Csvg preserveAspectRatio='none' overflow='visible' style='display: block;' width='38.764' height='18.5393' viewBox='0 0 38.764 18.5393' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath id='&%23244;&%23128;&%23157;&%23138;' d='M19.382 9.26966C16.9694 9.26966 15.013 7.16747 15.0028 4.59479C14.9927 2.05214 16.9694 0 19.382 0C21.8149 0 23.7612 2.0121 23.7612 4.57477C23.7612 7.15746 21.7946 9.26966 19.382 9.26966ZM7.70415 9.35976C5.61592 9.35976 3.90276 7.51784 3.90276 5.25548C3.89262 3.04317 5.61592 1.22127 7.70415 1.22127C9.81266 1.22127 11.5055 3.01314 11.5055 5.23546C11.5055 7.50783 9.79238 9.35976 7.70415 9.35976ZM31.0599 9.35976C28.9717 9.35976 27.2585 7.50783 27.2585 5.23546C27.2585 3.01314 28.9514 1.22127 31.0599 1.22127C33.1481 1.22127 34.8714 3.04317 34.8613 5.25548C34.8613 7.51784 33.1481 9.35976 31.0599 9.35976ZM19.382 7.75809C20.8722 7.75809 22.1292 6.34662 22.1292 4.57477C22.1292 2.82294 20.8924 1.51158 19.382 1.51158C17.8716 1.51158 16.6247 2.85297 16.6349 4.59479C16.6349 6.36664 17.902 7.75809 19.382 7.75809ZM7.70415 7.8682C8.95101 7.8682 10.0053 6.69698 10.0053 5.23546C10.0053 3.82399 8.98142 2.71283 7.70415 2.71283C6.44716 2.71283 5.41318 3.84401 5.41318 5.25548C5.41318 6.69698 6.4573 7.8682 7.70415 7.8682ZM31.0599 7.8682C32.2966 7.8682 33.3509 6.69698 33.3509 5.25548C33.3509 3.84401 32.3169 2.71283 31.0599 2.71283C29.7826 2.71283 28.7588 3.82399 28.7588 5.23546C28.7588 6.69698 29.813 7.8682 31.0599 7.8682ZM2.05782 18.5393C0.689319 18.5393 0 17.9888 0 16.8676C0 13.5441 3.44659 10.5009 7.69401 10.5009C9.36663 10.5009 10.9379 10.9414 12.2455 11.7823C11.7894 12.1026 11.4143 12.453 11.1001 12.8534C10.1168 12.2828 8.96114 11.9825 7.69401 11.9825C4.3792 11.9825 1.51042 14.2949 1.51042 16.7775C1.51042 16.9777 1.60165 17.0578 1.81453 17.0578H9.23484C9.16388 17.6684 9.52882 18.269 10.0154 18.5393H2.05782ZM36.7062 18.5393H28.7486C29.2251 18.269 29.6002 17.6684 29.5292 17.0578H36.9495C37.1624 17.0578 37.2536 16.9777 37.2536 16.7775C37.2536 14.2949 34.3848 11.9825 31.07 11.9825C29.7928 11.9825 28.6473 12.2828 27.664 12.8534C27.3497 12.453 26.9747 12.1026 26.5185 11.7823C27.8262 10.9414 29.3974 10.5009 31.07 10.5009C35.3175 10.5009 38.764 13.5441 38.764 16.8676C38.764 17.9888 38.0747 18.5393 36.7062 18.5393ZM13.0058 18.5393C11.3231 18.5393 10.5222 18.0388 10.5222 16.9377C10.5222 14.3149 13.8776 10.521 19.3719 10.521C24.8763 10.521 28.2317 14.3149 28.2317 16.9377C28.2317 18.0388 27.4308 18.5393 25.7379 18.5393H13.0058ZM12.5192 17.0278H26.2245C26.4881 17.0278 26.5996 16.9577 26.5996 16.7475C26.5996 15.0957 24.0248 12.0325 19.3719 12.0325C14.7291 12.0325 12.1543 15.0957 12.1543 16.7475C12.1543 16.9577 12.2557 17.0278 12.5192 17.0278Z' fill='%23EFF7F5'/%3E%3C/svg%3E");   -webkit-mask-image: var(--v2-mask);   mask-image: var(--v2-mask); }
.v2-sidenav-item[data-v2-overlay="my-orders"]     .v2-sidenav-icon { --v2-mask: url("data:image/svg+xml,%3Csvg preserveAspectRatio='none' overflow='visible' style='display: block;' width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath id='Icon/box' d='M23.04 5.37255L12.8862 0.212268C12.6149 0.0730339 12.31 0 12 0C11.69 0 11.3851 0.0730339 11.1138 0.212268L0.96 5.37469C0.670025 5.52206 0.427965 5.73903 0.259098 6.00295C0.090231 6.26688 0.000750297 6.56807 0 6.87509V17.1249C0.000750297 17.4319 0.090231 17.7331 0.259098 17.997C0.427965 18.261 0.670025 18.4779 0.96 18.6253L11.1138 23.7877C11.3851 23.927 11.69 24 12 24C12.31 24 12.6149 23.927 12.8862 23.7877L23.04 18.6253C23.33 18.4779 23.572 18.261 23.7409 17.997C23.9098 17.7331 23.9992 17.4319 24 17.1249V6.87616C23.9999 6.5686 23.9107 6.26673 23.7418 6.00219C23.5729 5.73765 23.3305 5.52018 23.04 5.37255ZM12 1.71266L21.2712 6.42818L17.835 8.174L8.56385 3.45848L12 1.71266ZM12 11.1437L2.72885 6.42818L6.64154 4.43802L15.9127 9.15354L12 11.1437ZM22.1538 17.1292L12.9231 21.8222V12.6259L16.6154 10.7493V14.5732C16.6154 14.8006 16.7126 15.0186 16.8857 15.1794C17.0589 15.3402 17.2936 15.4305 17.5385 15.4305C17.7833 15.4305 18.0181 15.3402 18.1912 15.1794C18.3643 15.0186 18.4615 14.8006 18.4615 14.5732V9.8105L22.1538 7.93393V17.1249V17.1292Z' fill='%23EFF7F5'/%3E%3C/svg%3E");     -webkit-mask-image: var(--v2-mask);     mask-image: var(--v2-mask); }
.v2-sidenav-item[data-v2-overlay="resources"]     .v2-sidenav-icon { --v2-mask: url("data:image/svg+xml,%3Csvg preserveAspectRatio='none' overflow='visible' style='display: block;' width='26' height='26' viewBox='0 0 26 26' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Group 18'%3E%3Cpath id='Vector' d='M13 0C10.4288 0 7.91543 0.762437 5.77759 2.1909C3.63975 3.61935 1.97351 5.64968 0.989572 8.02512C0.0056327 10.4006 -0.251811 13.0144 0.249797 15.5362C0.751405 18.0579 1.98953 20.3743 3.80762 22.1924C5.6257 24.0105 7.94208 25.2486 10.4638 25.7502C12.9856 26.2518 15.5995 25.9944 17.9749 25.0104C20.3503 24.0265 22.3807 22.3603 23.8091 20.2224C25.2376 18.0846 26 15.5712 26 13C25.9964 9.5533 24.6256 6.24882 22.1884 3.81163C19.7512 1.37445 16.4467 0.00363977 13 0ZM19.4475 7.22375L15.4475 15.2237C15.3985 15.3202 15.3202 15.3985 15.2238 15.4475L7.22376 19.4475C7.12984 19.4947 7.02344 19.5111 6.91967 19.4943C6.81591 19.4776 6.72005 19.4286 6.64573 19.3543C6.57141 19.2799 6.5224 19.1841 6.50567 19.0803C6.48893 18.9766 6.50532 18.8702 6.55251 18.7763L10.5525 10.7762C10.6015 10.6798 10.6798 10.6015 10.7763 10.5525L18.7763 6.5525C18.8702 6.50531 18.9766 6.48892 19.0803 6.50566C19.1841 6.5224 19.28 6.57141 19.3543 6.64573C19.4286 6.72005 19.4776 6.8159 19.4943 6.91967C19.5111 7.02343 19.4947 7.12983 19.4475 7.22375Z' fill='%23EFF7F5'/%3E%3Ccircle id='Ellipse 32' cx='13' cy='13' r='1.5' fill='%23EFF7F5'/%3E%3C/g%3E%3C/svg%3E");     -webkit-mask-image: var(--v2-mask);     mask-image: var(--v2-mask); }
.v2-sidenav-item[data-v2-overlay="lead-times"]    .v2-sidenav-icon { --v2-mask: url("data:image/svg+xml,%3Csvg width='26' height='26' viewBox='0 0 26 26' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate(26 0) scale(-1 1)'%3E%3Cpath id='Vector' d='M13 0C10.4288 0 7.91543 0.762437 5.77759 2.1909C3.63975 3.61935 1.97351 5.64968 0.989572 8.02512C0.00563269 10.4006 -0.251811 13.0144 0.249797 15.5362C0.751405 18.0579 1.98953 20.3743 3.80762 22.1924C5.6257 24.0105 7.94208 25.2486 10.4638 25.7502C12.9856 26.2518 15.5995 25.9944 17.9749 25.0104C20.3503 24.0265 22.3807 22.3603 23.8091 20.2224C25.2376 18.0846 26 15.5712 26 13C25.9964 9.5533 24.6256 6.24882 22.1884 3.81163C19.7512 1.37445 16.4467 0.00363977 13 0ZM20 14H13C12.7348 14 12.4804 13.8946 12.2929 13.7071C12.1054 13.5196 12 13.2652 12 13V6C12 5.73478 12.1054 5.48043 12.2929 5.29289C12.4804 5.10536 12.7348 5 13 5C13.2652 5 13.5196 5.10536 13.7071 5.29289C13.8946 5.48043 14 5.73478 14 6V12H20C20.2652 12 20.5196 12.1054 20.7071 12.2929C20.8946 12.4804 21 12.7348 21 13C21 13.2652 20.8946 13.5196 20.7071 13.7071C20.5196 13.8946 20.2652 14 20 14Z' fill='%23EFF7F5'/%3E%3C/g%3E%3C/svg%3E");    -webkit-mask-image: var(--v2-mask);    mask-image: var(--v2-mask); }

/* ── WHOLE PIXELS, OR THE GLYPH IS SOFT ────────────────────────────────────
   Daniel, 15-Sep: "the icons on the dasboard nav bar slightly blurry? … they
   look blurry on desktop and not on mobile."

   mask-size: contain fits the artwork's OWN ratio inside the 28x28 box, and
   three of these six are not square — 27x20, 26.7342x30.6027, 38.764x18.5393.
   So the painted mask came out on fractions: My Patients at 28 x 13.39, Request
   an Order at 24.46 x 28. mask-position: center then halves whatever is left
   over, putting the glyph on a half-pixel as well.

   A mask is rasterised at the device pixel ratio, which is why the same icon is
   fine on a phone and soft on a desktop: at DPR 2-3 those fractions round to
   something close enough, at DPR 1 they land between pixels and the edges smear.

   So each non-square icon is given an explicit EVEN size. Even matters as much
   as integer: 28 minus an odd width leaves an odd remainder, and centring halves
   it straight back onto a .5. The ratios drift by up to 4%, which is invisible
   on a monochrome glyph and the exports carry preserveAspectRatio='none'
   anyway — they are drawn to fill whatever box they are given. Blur is visible;
   4% is not.

   The three square exports (My Orders, Resources, Lead Times) already resolve to
   a clean 28x28 under contain and are deliberately left alone. */
.v2-sidenav-item[data-v2-overlay="add-patient"] .v2-sidenav-icon {
    -webkit-mask-size: 28px 20px;            /* 27x20 artwork, offset 0 / 4 */
            mask-size: 28px 20px;
}

.v2-sidenav-item[data-v2-overlay="request-order"] .v2-sidenav-icon {
    -webkit-mask-size: 24px 28px;            /* 26.73x30.60,  offset 2 / 0 */
            mask-size: 24px 28px;
}

.v2-sidenav-item[data-v2-overlay="my-patients"] .v2-sidenav-icon {
    -webkit-mask-size: 28px 14px;            /* 38.76x18.54,  offset 0 / 7 */
            mask-size: 28px 14px;
}

/* Spacer for an item with no glyph — Updates, which carries a badge instead (see
   below). No mask-image is set for it above, and a masked span with no mask
   paints as a solid 28px block, so the empty case has to be explicit. It still
   holds the column so the labels stay aligned. */
.v2-sidenav-icon--none { background-color: transparent; }

/* QUERIES AND UPDATES HAVE GLYPHS NOW, SHOWN ONLY AT ZERO (Daniel, 13-Oct,
   overriding 18-Aug's "keeping just the number or nothing": "i only want
   to see these if they is 0 queries or updates, if there is, the icon is
   replaced with our current circular number icon"). From the components
   page (799:1582): Queries, in the top group with the plain glyphs, takes
   the plain speech bubbles (2879:21017); Updates, beside Resources' and
   Lead Times' circles, takes the megaphone circle (2879:21022). Both are in
   the markup with the badge; whichever applies shows. */
.v2-sidenav-item[data-v2-overlay="order-queries"]  .v2-sidenav-icon { --v2-mask: url("data:image/svg+xml,%3Csvg width='23' height='21' viewBox='0 0 23 21' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21.3117 16.0345C21.8569 14.9037 22.1245 13.659 22.0921 12.4039C22.0597 11.1489 21.7283 9.91969 21.1254 8.81844C20.5226 7.71719 19.6657 6.77568 18.6258 6.07211C17.586 5.36855 16.3934 4.92324 15.1469 4.77319C14.7322 3.80888 14.1292 2.93702 13.3733 2.20865C12.6174 1.48029 11.7237 0.910067 10.7447 0.531375C9.76568 0.152683 8.72093 -0.0268639 7.67165 0.00324735C6.62236 0.0333586 5.58963 0.272523 4.63393 0.706736C3.67823 1.14095 2.81877 1.76148 2.10588 2.53199C1.39299 3.30251 0.841002 4.20752 0.482236 5.19402C0.12347 6.18053 -0.0348605 7.2287 0.0165167 8.27715C0.067894 9.32561 0.327946 10.3533 0.781444 11.3L0.0643549 13.7373C-0.0158218 14.0094 -0.0211828 14.2981 0.0488358 14.5729C0.118854 14.8478 0.261664 15.0987 0.462241 15.2993C0.662817 15.4999 0.913745 15.6427 1.18863 15.7127C1.46351 15.7827 1.75218 15.7774 2.02427 15.6972L4.46158 14.9801C5.2442 15.3561 6.08326 15.6011 6.94525 15.7051C7.36405 16.6863 7.97747 17.5725 8.74844 18.3099C9.51941 19.0474 10.4319 19.6208 11.4308 19.9956C12.4297 20.3704 13.4942 20.5387 14.56 20.4904C15.6257 20.4422 16.6707 20.1783 17.6316 19.7147L20.0689 20.4318C20.3409 20.5119 20.6294 20.5172 20.9042 20.4472C21.1789 20.3772 21.4298 20.2345 21.6303 20.0341C21.8308 19.8336 21.9737 19.5829 22.0438 19.3081C22.1139 19.0334 22.1087 18.7449 22.0288 18.4728L21.3117 16.0345ZM4.53655 13.3457C4.46114 13.3459 4.38612 13.3565 4.31363 13.3773L1.57744 14.1841L2.3833 11.446C2.44062 11.2481 2.41832 11.0358 2.32116 10.8541C1.60734 9.51918 1.39701 7.97214 1.72852 6.49505C2.06003 5.01797 2.9113 3.70919 4.12715 2.80731C5.343 1.90542 6.84246 1.47049 8.3522 1.5818C9.86193 1.6931 11.2814 2.34323 12.3518 3.41367C13.4223 4.48411 14.0724 5.90358 14.1837 7.41331C14.295 8.92305 13.8601 10.4225 12.9582 11.6384C12.0563 12.8542 10.7475 13.7055 9.27046 14.037C7.79337 14.3685 6.24633 14.1582 4.91137 13.4443C4.79657 13.3808 4.66774 13.3469 4.53655 13.3457ZM19.7059 16.1795L20.5157 18.9187L17.7775 18.1128C17.5797 18.0555 17.3673 18.0778 17.1857 18.175C15.7326 18.9509 14.034 19.1285 12.4518 18.6701C10.8696 18.2117 9.529 17.1534 8.71579 15.7209C9.79623 15.6081 10.8417 15.2733 11.7866 14.7374C12.7316 14.2015 13.5556 13.4761 14.2069 12.6067C14.8583 11.7374 15.323 10.7428 15.5718 9.68534C15.8206 8.6279 15.8482 7.53046 15.6529 6.46185C16.5943 6.68375 17.4721 7.11899 18.2186 7.73397C18.965 8.34896 19.5602 9.12724 19.9582 10.0088C20.3561 10.8903 20.5462 11.8514 20.5138 12.8181C20.4813 13.7847 20.2272 14.7309 19.771 15.5838C19.6728 15.7665 19.6505 15.9805 19.7089 16.1795H19.7059Z' fill='black'/%3E%3C/svg%3E"); -webkit-mask-image: var(--v2-mask); mask-image: var(--v2-mask); }
.v2-sidenav-item[data-v2-overlay="latest-updates"] .v2-sidenav-icon { --v2-mask: url("data:image/svg+xml,%3Csvg width='26' height='26' viewBox='0 0 26 26' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.001 0C20.1805 0.00026388 26.001 5.82046 26.001 13C26.001 20.1795 20.1805 25.9997 13.001 26C5.82127 26 0 20.1797 0 13C0 5.8203 5.82127 0 13.001 0ZM18.4219 5.75977C18.2068 5.78869 18.0041 5.87954 17.8379 6.02148C14.3994 8.95529 10.7887 9.21581 10.5762 9.22852H7.73926C6.86596 9.22086 6.0229 9.55379 5.38281 10.1582C4.74262 10.7629 4.35402 11.5946 4.29688 12.4814C4.23984 13.3681 4.51817 14.2433 5.0752 14.9277C5.6323 15.6123 6.42591 16.0537 7.29297 16.1621L8.13184 19.3789C8.17716 19.5582 8.26364 19.7241 8.38477 19.8623C8.506 20.0006 8.65802 20.1076 8.82812 20.1738C8.99812 20.24 9.18142 20.2637 9.3623 20.2432C9.54333 20.2225 9.71763 20.1576 9.86914 20.0547L10.6523 19.5234C10.8085 19.4174 10.9369 19.2739 11.0254 19.1055C11.1139 18.9369 11.1604 18.7478 11.1602 18.5566V16.2578C12.351 16.442 15.1465 17.1 17.8379 19.3965C18.0041 19.5385 18.2067 19.6293 18.4219 19.6582C18.6371 19.6871 18.856 19.6528 19.0527 19.5596C19.2495 19.4663 19.4165 19.3181 19.5332 19.1318C19.6498 18.9457 19.7118 18.7295 19.7119 18.5088V6.90918C19.7117 6.68841 19.649 6.47225 19.5322 6.28613C19.4155 6.1001 19.2494 5.95158 19.0527 5.8584C18.856 5.76523 18.637 5.73085 18.4219 5.75977ZM10.0195 16.1885V18.5566L9.23633 19.0889V19.0801L8.48047 16.1885H10.0195ZM11.1416 10.3887V15.0283H7.73926C7.13463 15.0283 6.55453 14.7845 6.12695 14.3496C5.69935 13.9146 5.45904 13.3242 5.45898 12.709C5.45898 12.0937 5.69932 11.5034 6.12695 11.0684C6.55455 10.6334 7.13455 10.3887 7.73926 10.3887H11.1416Z' fill='black'/%3E%3C/svg%3E"); -webkit-mask-image: var(--v2-mask); mask-image: var(--v2-mask); }
/* 23x21 artwork: an even box, as the other non-square glyphs (above). */
.v2-sidenav-item[data-v2-overlay="order-queries"] .v2-sidenav-icon {
    -webkit-mask-size: 26px 24px;            /* 23x21 artwork, offset 1 / 2 */
            mask-size: 26px 24px;
}
/* A count takes the glyph's place. */
.v2-sidenav-item:has(> .v2-pill-badge:not([hidden]):not([data-count="0"])) > .v2-sidenav-icon { display: none; }

/* Updates has no icon at all in the design: LeftNavSecondary's LatestUpdates
   variant (985:4) puts a 26×26 "_Notification Badges" frame exactly where the
   other two put a vector. So the badge IS this item's icon, which is why it sits
   in the icon slot rather than trailing the label — and why the item falls back
   to the empty spacer when there is nothing unread.

   Reuses .v2-pill-badge rather than defining a second one, so there is one
   notification red in the system. It only needs resizing: 26px here against the
   pill row's 33px (985:4 vs 808:4189). */
.v2-pill-badge--nav {
    min-width: 26px;
    min-height: 26px;
    font-size: var(--type-caption-size);   /* 12px in a 26px disc */
}

/* UPDATES' COUNT IN THE NAV, BY THEME (Jake, 15-Oct). Dark: the dashboard
   pill's light badge, a Seafoam disc with a Harbour figure. Light: the info
   blue with a white figure, as it was — a Seafoam disc would sit pale on the
   light page. */
.v2-pill-badge--nav.v2-pill-badge--light { background-color: var(--badge-info); color: #fff; }
[data-theme="dark"] .v2-pill-badge--nav.v2-pill-badge--light {
    background-color: var(--colour-seafoam);
    color: var(--colour-harbour);
}

/* THE BACK CONTROL IS NOT IN THIS NAV ANY MORE.
   -----------------------------------------------------------------------------
   It used to be a .v2-sidenav-item--home between the primary and secondary
   groups. Daniel moved it to the top row on 14-Aug — a glass circle on the left,
   in line with the status bar — which is where mobile already had it, so both
   breakpoints shared one control. On 10-Sep it split in two by MEANING rather
   than by breakpoint: the brand is the way HOME at every width, and the panel
   head's own .v2-panel-back is the way UP one level.

   That also closes the open question for Justin: the frames give the section view
   no way back at all, so wherever it went was invented either way, and a single
   affordance in a consistent place beats a nav item that only existed on desktop.
   The nav is now purely destinations, which is what the file actually draws. */


/* ── Panel ───────────────────────────────────────────────────────────────
   THE PAGE NEVER SCROLLS ON DESKTOP — THE PANEL DOES.
   -----------------------------------------------------------------------------
   Daniel called this on the Lead Times panel, which was just tall enough to push
   a scrollbar onto the window (2026-08-11). He's right, and the file says so
   explicitly: several panels carry their own Scrollbar_Vertical component INSIDE
   them, and every screen frame is a fixed 1440×1024.

       Resources           panel 930×1346   bar 1300:4408 at x=904 y=139 h=652
       Register a patient  panel 930×923    bar 1300:4870 at x=904 y=126 h=594

   Two things fall out of those numbers, both of which shape the CSS below.

   The bar starts BELOW the head — y=139 is where Resources' body begins, y=126
   is where Register's first form section begins, and both panels' titles sit
   above that. So the scroll container is the panel BODY and the head stays
   pinned; scrolling the whole panel would take its own title away with it.

   And it sits 14px from the PANEL edge (x=904 in a 930 panel), not at the content
   edge — i.e. inside the 40px padding, not outside it. Hence the negative margin
   on .v2-panel-body further down.

   The panel still HUGS its content (max-height, never height) — Lead Times is
   512px tall in a 1024 frame and is drawn that way. It only caps, and only then
   scrolls, when it would otherwise outgrow the shell.

   Desktop only. The mobile frames are genuinely tall scrolling pages
   (Resources/Mobile 1252:1214 is 402×1801), so there the window scrolls as
   normal — which is also why none of this goes near lockBodyScroll()'s territory
   or iOS's position:fixed problems. */
.v2-panel {
    width: 100%;
    --panel-pad-x: 40px;                /* 920:5923; also the fade band's reach */

    /* 40px above the title, not the frame's 60. Daniel's call (14-Aug) across all
       panels — the space above the title was the least useful 20px in the layout,
       and everything else has been competing for room. A deliberate deviation from
       920:5923, recorded as one.

       The BOTTOM stays 60: it isn't the same 20px. On a short panel it balances the
       card stack against the panel's own visible edge; on a tall one it sits below
       the fold, where it costs nothing. Asymmetric on purpose — say the word if it
       reads oddly on Lead Times, where both edges are visible. */
    --panel-pad-top: 40px;
    padding: var(--panel-pad-top) var(--panel-pad-x) 60px;
    border-radius: var(--radius-panel); /* 24px */
    background: var(--surface-page);
    color: var(--text-body);
    box-shadow:
        0 32px 64px rgb(23 30 32 / 22%),
        0 8px 20px rgb(23 30 32 / 14%);
}

.v2-panel[hidden] { display: none; }

/* ── No "bleed" variant. THE RADIUS AND THE BOTTOM PADDING ARE ALWAYS ON. ──
   Daniel, 18-Aug: "its worth always having the rounded edge but the rounding goes
   off page for those with scroll?"

   That removes a whole modifier. The old --bleed squared the bottom corners and
   zeroed the bottom padding so a long panel read as continuing below the fold —
   which meant a per-panel declaration ("does this content overflow?"), a thing CSS
   cannot derive and the view had to assert. Making it universal earlier today then
   gave SHORT panels square corners and no bottom padding, which is what Lead Times
   was showing: a rounded box with one flat, cropped-looking edge.

   Keeping the radius always is both simpler and correct, because the geometry
   already does the work. A panel is capped at its column, so:
     short content  the panel ends above the fold, rounded, with its 60px — right
     long content   the panel fills the column and its bottom edge sits at or past
                    the window edge, so the rounding is simply off-screen, and the
                    body scrolls inside it

   Nothing to declare per panel, no variant to keep in step, and one fewer thing
   for a new panel to get wrong. Both values live on .v2-panel itself. */


/* ── Crossfade between panels ────────────────────────────────────────────
   Daniel asked for content to move softly rather than snapping (14-Aug).

   `hidden` is still what takes a panel out of the layout — display: none can't
   animate, so the fade can't be expressed in CSS alone. overlays.js drives the
   sequence and this provides the two states it toggles between.

   140ms each way, overlapping: the outgoing panel starts fading and the incoming
   one begins before it finishes. A full 180+180 read as sluggish on a tool used
   repeatedly — the point is to soften the change, not to stage it.

   The 6px lift is deliberately small. Panels are large surfaces, and a big
   translate on something 930px wide reads as the page lurching. */
.v2-panel {
    transition: opacity 140ms ease, transform 140ms ease;
}

/* The outgoing state, and the incoming one before it settles — the same
   declaration, because a crossfade is symmetrical. JS adds it, then removes it on
   the next frame for the panel that's arriving. */
.v2-panel[data-v2-fade] {
    opacity: 0;
    transform: translateY(6px);
}

/* Nothing should animate on first paint: the page load itself is the transition.
   JS sets data-v2-ready on the shell once it has run. */
.v2-shell:not([data-v2-ready]) .v2-panel { transition: none; }

/* prefers-reduced-motion is not a suggestion. base.css already collapses
   transition DURATIONS globally, which is enough to make this instant — but the
   translate would still apply for a frame, so it's removed outright here. */
@media (prefers-reduced-motion: reduce) {
    .v2-panel { transition: none; }
    .v2-panel[data-v2-fade] { opacity: 1; transform: none; }
}

/* ── The panel header stays put; content scrolls UNDER it ────────────────
   Justin's annotation on RegisterANewPatient (1161:9345), titled "Headers":

     "We should always keep the section title (and in this case the download
      button) visible. All section content should scroll underneath using a
      progressive blur (example opposite)"

   ALWAYS — so this belongs on .v2-panel, not on one panel body. The example
   frame is 1161:9418, "Progressive blur example".

   Half of it was already true: .v2-panel-head sits OUTSIDE .v2-panel-body, which
   is the scroll container, so it never scrolled. That is also why the frames put
   their scrollbar below the title (y=139 in Resources, y=126 in Register) rather
   than beside it. What was missing is the blur.

   The head needs an opaque background of its own — without it, content passes
   visibly behind the title rather than disappearing under it. --surface-page is
   the panel's own colour, so it reads as the panel rather than a bar. */
.v2-panel-head {
    position: relative;
    z-index: 2;
    display: grid;
    gap: var(--space-3);                /* 12px, per the frame */
    /* 16px, down from the frame's 30. Daniel asked for this gap twice (18-Aug) — with
       the search now the last thing in the head, it IS the space between the search and
       the results, and 24 was still too much. Changed globally rather than overridden
       for one panel, so every panel keeps the same head-to-content rhythm. */
    margin-bottom: var(--space-4);      /* was the container's 30px */
    background-color: var(--surface-page);
}

/* THE FADE — A COLOUR SCRIM DOES THE WORK, THE BLUR ONLY SOFTENS IT.
   -----------------------------------------------------------------------------
   Third attempt, and the first two failed for the same underlying reason. A heavy
   masked blur (10px, then 24px+8px stacked) has to smear whatever is behind it —
   and behind it here are WHITE input fields on a dark card. Blurring a hard
   white-on-dark edge produces grey mush, and fading that mush out with an opacity
   mask makes the mask's own steps visible. Daniel, 14-Aug: "the fade is bleeding
   too badly when light goes through it it reveals the banding too much".

   So the mechanism is inverted. The thing that HIDES content is now a gradient in
   the panel's own colour — content fades toward --surface-page, which is the
   surface it is sliding under, so there is nothing to smear and nothing to band
   beyond a normal gradient. The blur is dropped to 6px and only takes the edge off
   the last few pixels, which is what "progressive blur" needs to read as depth
   rather than as a wipe.

   FIRST PASS AT THIS WAS TOO TIMID — "the fade is now barely noticeable". The fix
   isn't more blur, which is what caused the banding; it's holding the scrim OPAQUE
   for longer before it falls away, and keeping the blur confined to the region the
   scrim has already covered. Where the scrim is solid, the blur's smear is hidden,
   so the softening happens without ever showing mush.

       ::before   scrim, solid to 40% then out. 64px. No blur, no mask.
       ::after    blur(12px), masked out by 55% — i.e. only where the scrim is
                  still doing most of the work.

   Both reach into the panel's side padding via --panel-pad-x so they span the full
   width rather than stopping at the text column, and both are pointer-events: none
   so neither can eat a click on the content below.

   ONLY WHILE SCROLLED. Daniel, 14-Aug: "the fade bleeds into the first container? it
   should only really activate on scroll no?" — and he's right. These are static
   elements pinned under the head, so at scrollTop 0 they were dimming the top of the
   first card while there was nothing beneath them to hide. A fade with nothing to
   fade is just a smudge.

   So they start at opacity 0 and overlays.js sets data-v2-scrolled on the panel as
   soon as its body moves. Faded in over 140ms rather than switched, because a hard
   appearance on the first pixel of scroll is its own kind of jolt. */
.v2-panel-head::before,
.v2-panel-head::after {
    content: '';
    position: absolute;
    top: 100%;
    left: calc(-1 * var(--panel-pad-x));
    right: calc(-1 * var(--panel-pad-x));
    height: 64px;
    pointer-events: none;
    opacity: 0;
    transition: opacity 140ms ease;
}

.v2-panel[data-v2-scrolled] .v2-panel-head::before,
.v2-panel[data-v2-scrolled] .v2-panel-head::after { opacity: 1; }

/* The scrim. This is what makes content disappear, and it cannot band badly because
   it fades to the exact colour of the surface above it. */
.v2-panel-head::before {
    /* Two stops, deliberately. A mid-stop via color-mix() would ease it slightly
       more pleasingly, but color-mix would be this codebase's first use — and if it
       isn't supported the WHOLE background declaration is dropped, which means no
       scrim at all rather than a slightly less elegant one. Not a risk worth taking
       on the declaration that does the actual hiding. */
    background: linear-gradient(
        to bottom,
        var(--surface-page) 0%,
        var(--surface-page) 40%,
        transparent 100%
    );
}

/* The softener. Confined to the top 45%, which is where the scrim above is still
   near-solid — so the blur's smear happens UNDER cover and never shows as mush.
   That containment is what lets it be 12px rather than the 6px that was invisible. */
.v2-panel-head::after {
    -webkit-backdrop-filter: blur(12px);
            backdrop-filter: blur(12px);
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 55%);
            mask-image: linear-gradient(to bottom, #000 0%, transparent 55%);
}

/* A HEAD WITH A TRAILING CONTROL — My Patients' view toggle (MyPatientsTitle
   918:5449, the slider at x=778 of 850).
   -----------------------------------------------------------------------------
   It expects the copy PRE-WRAPPED in .v2-panel-head-text, and that is the whole
   trick: with column flow and `minmax(0,1fr) auto` the head must have exactly
   two children, so a title AND a sub AND a control would put the sub in the
   second column. Wrapping the copy makes it two again.

   (There used to be a sibling --action modifier for Register's "Download
   registration form". The button was never built and has been dropped as
   redundant — Daniel, 26-Aug — so this is the only head variant left.)

   align-items: end because the frame's row is items-end — the slider lines up
   with the subtitle's row, not the middle of the block. Column flow holds at
   every width: a 72px control always fits beside a title. */
.v2-panel-head--control {
    grid-auto-flow: column;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: end;
}

.v2-panel-head-text {
    display: grid;
    gap: var(--space-3);                /* the head's own 12px, kept when the
                                           title and sub become one child */
    min-width: 0;
}

.v2-panel-title {
    margin: 0;
    font-size: var(--type-h1-size);        /* 36px Bold */
    font-weight: var(--type-h1-weight);
    line-height: 1.15;
    /* §3's section accent, set by the panel wrapper. */
    color: var(--heading-colour, var(--text-strong));
}

/* NO FOCUS RING ON THE TITLE, and this is a considered exception rather than the
   outline-stripping base.css warns about.
   -----------------------------------------------------------------------------
   Opening a panel moves focus here (overlays.js), because otherwise a keyboard
   user activates a tile and focus is left on an element that has just been
   replaced by the nav. The heading takes tabindex="-1" purely to receive it.

   Daniel saw the result on a phone: tapping through drew a blue rectangle round
   the panel title. That blue is ours — --border-focus is Harbour 500 — and on
   touch the browser resolves :focus-visible for the scripted focus move.

   Suppressed because this is a NON-INTERACTIVE, script-only focus target. It is
   not reachable by tabbing (tabindex is -1) and it does nothing when activated,
   so a focus indicator points at something the user cannot act on. WAI-ARIA's
   practices make the same distinction: a visible indicator is required for
   user-navigable controls, not for a programmatic landing spot. Focus still
   moves, so screen readers still announce the heading and the keyboard's next
   Tab still starts from inside the panel — the accessible behaviour is
   unchanged, only the misleading ring is gone.

   Do NOT generalise this to anything the user can tab to. */
.v2-panel-title:focus,
.v2-panel-title:focus-visible { outline: none; }

.v2-panel-sub {
    margin: 0;
    font-size: var(--type-heading-size);   /* 16px Medium */
    font-weight: var(--type-heading-weight);
    /* [frame] Flint 200 (918:5457 and the other panel titles), in both themes.
       Daniel, 17-Aug: the stepped version read "too dark in light mode" and he is
       right — --text-muted is Pebble 600 there, which makes a subtitle compete with
       the title instead of sitting under it.
       MEASURED AT 2.4:1 on Seafoam and left at the frame's value, per the standing
       rule that the frame decides and I record the number rather than overriding
       it. Applies to every panel's subtitle, which is consistent: the frames give
       them all Flint 200. */
    color: var(--colour-flint-200);
}
/* TITLE TO SUBTITLE IS THE FRAME'S 12px, MEASURED THE FRAME'S WAY. Daniel,
   13-Oct, on Resources: "the sub header is too far away from the Title compared
   to spec". Every panel frame trims its text boxes to cap height and baseline
   (text-box-trim), so its 12px runs from the title's baseline to the top of the
   subtitle's capitals. Between CSS line boxes the head's 12px gap sits inside
   the leading instead: 8.4px under the title's baseline (36px Roboto at 1.15)
   + 12 + 6.1px over the subtitle's capitals (16px at the body's 1.5) = 26.5px.
   Taking 14px back leaves 12.5. By arithmetic rather than `text-box`, which
   squashed My Patients twice (components.css). Every panel, as every frame
   draws the same 12. */
.v2-panel-title + .v2-panel-sub { margin-top: -14px; }

/* [frame] 1795:2546 — the subtitle's pipe is #cad963 while the reference and the
   clinic either side of it stay flint/200. That hex is --colour-marram, so the
   token is used rather than the literal. Decorative: it separates two values and
   carries none of its own, which is why a colour that would be too low-contrast
   for text is right for it. */
.v2-panel-sub-sep {
    color: var(--colour-marram);
    /* The spacing was two literal text nodes either side of the pipe until
       10-Sep. It is a margin now because the phone HIDES this element to stack
       the ref onto two lines, and a hidden element takes its own margin with it
       where stray text nodes would have stayed behind as double spaces. */
    margin: 0 6px;
}

/* ── THE SPLIT HEAD ──────────────────────────────────────────────────────
   Daniel, 10-Sep: "moving the order number and clinic to the right side and
   having the < my orders underneath the title", then "lets move the order
   number and clinic to the right too … leaving the back button as the sub
   again under the title".

   LEFT COLUMN IS IDENTITY AND MOVEMENT — what this panel is, how you leave it.
   RIGHT COLUMN IS CONTEXT AND ACTION — which order, what you can do to it.

   WHY IT IS THE ONE THAT WON: the back control lands in the line the subtitle
   used to occupy, at the subtitle's own step, so it adds no row of its own.
   Measured on a 930px panel, head height goes 70px → 89px, and the whole 19px
   is the aside dropping to a second row — against 116px for a chip above the
   title, which is more than the 20px of top padding taken out on 14-Aug.

   display: contents on .v2-panel-head-text so the title, the sub and the back
   control become grid items of the head itself. That keeps ONE head partial
   serving every panel instead of a second one for this shape.

   NOT COMBINED WITH --control: that modifier's flex row is what this replaces,
   so dashboard.php emits one or the other, never both. */
/* ── ONE VERTICAL RHYTHM, DECLARED ONCE ──────────────────────────────────
   Daniel, 10-Sep: "keep format of all the way through, some have the back
   button higher or lower and the title from that higher or lower, again should
   be uniform."

   The gap between the nav row and the subject row, and the height the nav row
   occupies, are now two tokens rather than a value repeated per rule. Both the
   panel head's grid and the query thread's own nav row (which is that screen's
   header, because its head is hidden) read them — so back-to-title cannot come
   out different on one screen from another by someone tuning one site and not
   the other. That is the mistake this whole sequence has been made of.

   Declared on .v2-panel so they cascade into the body, where the thread card
   lives. */
.v2-panel {
    --v2-head-gap: var(--space-3);   /* 12px — nav row to subject row */
    --v2-head-nav-h: 26px;           /* the nav row's height, content or not */
}

.v2-panel-head--split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--space-6);      /* 24px */
    row-gap: var(--v2-head-gap);
    align-items: start;
}

.v2-panel-head--split .v2-panel-head-text { display: contents; }

/* ── THE GRAMMAR, AND IT IS THE SAME AT EVERY WIDTH ──────────────────────
   Daniel, 10-Sep, after several rounds of me changing one screen at a time:

       "< back button above title.

        Title                         anything else
        Anything                 anything"

   And: "this layout should be respected on desktop too by the way."

   So there is ONE arrangement now, not one per breakpoint — and each COLUMN
   packs from the top, so there are no holes in it. Daniel again, seeing the
   queries list with an empty cell beside the back control: "so obviously bring
   the right side up".

     row 1   ‹ Back                    which thing this is
     row 2   Title                     actions

   LEFT IS THE ROUTE THEN THE SUBJECT; RIGHT IS WHICH SUBJECT THEN WHAT YOU CAN
   DO. Two columns, filled top-down, nothing reserved. A panel with only some of
   the four still has no gaps: Order details on touch keeps its actions in the
   body, so its right column is just the ref, on row 1.

   THE WAY OUT GETS A ROW TO ITSELF, above the title, everywhere. It stopped
   being a mobile question the moment it had to be uniform: a control that sits
   above the heading on a phone and below it on a desktop is two designs, and
   every complaint in this sequence was some version of that.

   ROW 2 IS THE SUBJECT AND WHICH SUBJECT. The ref belongs to the title the way
   a subtitle always did, because that is what it is. It rode with the back
   control for one revision — "flip the new query button with the Order number
   and clinic" — and seeing it settled it the other way: "move the SO and clinic
   down one row to be inline with the title not the < query."

   ROW 3 IS WHATEVER ELSE THE PANEL HAS. A head puts its actions in the right
   half; the query thread screen, which builds this same grid out of its card,
   puts the other-queries link left and the status pill right. Two columns and
   three rows is the shape; what fills them is the panel's business.

   NO PER-WIDTH PLACEMENTS BELOW. The touch block adjusts type size, gaps and
   how the action slot is justified — never which cell anything is in. That is
   what stops this drifting again, and drift is what the last four passes were. */
.v2-panel-head--split .v2-panel-back {
    grid-column: 1;
    grid-row: 1;
    min-height: var(--v2-head-nav-h);
}

.v2-panel-head--split .v2-panel-title { grid-column: 1; grid-row: 2; }

.v2-panel-head--split .v2-panel-sub {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    /* Centred against the title rather than top-aligned, or a short line
       floats above its cap height. */
    align-self: center;
    /* Flex because the three parts (ref, separator, clinic) are already
       separate elements — the separator carries the frame's marram.

       WRAP, and justified right when it does: without it the row takes its
       max-content width, and since column 1 is the flexible one, a long clinic
       name would squeeze the TITLE instead of folding itself. Wrapping costs a
       second short line in the column that has room for it. */
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    margin: 0;
    text-align: right;
}

/* Under the ref, in its column — "span the width of the Order number through
   the clinic? for balance". Column 2 is content-sized and the ref is usually
   the widest thing in it, so the two share a right edge without either
   measurement being written down.

   Row 2, so the right column has no hole above it: "bring the right side up".
   It lands opposite the title, which is also where Justin draws the pair on
   desktop (OrderDetails 1788:17704).

   justify-self is set per width: stretched on touch, where one button should
   fill that width, and end on desktop, where two or three controls keep their
   natural sizes and hang off the same edge. */
.v2-panel-head--split .v2-panel-head-aside {
    grid-column: 2;
    grid-row: 2;
    justify-self: end;
}

/* ── WITH NOTHING OPPOSITE IT, THE BACK CONTROL SITS ALONE ───────────────
   Daniel, 10-Sep, on View order: "thats now pushed the order number up inline
   with the back button on view order. should be a row down as it has a gap
   there?"

   Right, and it is the same rule as the thread screen's fifth entry, arrived at
   from the other direction. The back control shares its row only when there is
   something to PUT opposite it. Order details on touch has no actions in its
   head at all — record.js moves the pair into the body at that width — so
   pairing the ref with the back leaves a hole beneath the ref and the head
   reads as a 2x2 with a corner missing.

   THE COUNT DECIDES, every time:

     4 things   back | ref  over  title | actions          a grid
     3 things   back        over  title | ref              back alone, then a pair
     5 things   back        over  the 2x2                  see the thread screen

   :has() rather than a class the view has to remember to set — the condition IS
   "is there anything in the actions slot", and asking the DOM that directly
   cannot fall out of step with record.js moving the buttons about. The second
   selector covers a --split head with no actions slot in its markup at all,
   which the next nested panel may well be. */
.v2-panel-head--split:has(.v2-panel-head-aside:empty) .v2-panel-sub,
.v2-panel-head--split:not(:has(.v2-panel-head-aside)) .v2-panel-sub {
    grid-row: 2;
}

/* AN EMPTY SLOT TAKES NO ROW. On a phone record.js moves Order details' pair
   into the body (see placeAside), which leaves this slot empty — and a placed
   grid item with no content still opens its row and so still claims a row-gap
   above it. That is 8px of head with nothing in it, on the screen with the
   least room to spare. */
.v2-panel-head-aside:empty { display: none; }

/* THE BELOW SLOT SITS UNDER BOTH ROWS. Its base rule names row 2 explicitly —
   see the note there, it had to, because auto-placement put it beside the
   title — and rows 1 and 2 are the header's own. Order queries is the only
   panel carrying both a filter row and this head shape. */
/* ── A FIFTH THING GETS ITS OWN ROW ──────────────────────────────────────
   Daniel, 10-Sep: "if queries is the 5th entry, it should have its own row
   above the grid below. if only 4, then it should just be a grid."

   Which is the head exactly: the four — back, title, ref, actions — are the
   2x2, and anything else is a full-width row underneath, above the card grid in
   the body. Order queries' search-and-status row is the only one so far.

   The span and the margin are declared here rather than inherited, because the
   general rule for this slot now excludes split heads — see the note by it for
   why that had to happen. */
.v2-panel-head--split [data-v2-head-below] {
    grid-column: 1 / -1;
    grid-row: 3;
    margin-top: var(--space-3);
}

/* ── The back control ────────────────────────────────────────────────────
   16px Medium in Flint 200 — the subtitle's own step and colour, because it is
   standing in the subtitle's place. A chevron rather than a word "Back": the
   destination is what it names, which is also what syncUp() has always written
   to aria-label. See the note on syncUp() for why it hides itself. */
.v2-panel-back {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    align-self: start;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    font-family: inherit;
    font-size: var(--type-heading-size);       /* 16px */
    font-weight: var(--type-heading-weight);   /* Medium */
    line-height: 1.5;
    color: var(--colour-flint-200);
    transition: color 120ms ease;
}

.v2-panel-back:hover { color: var(--text-body); }

/* BACK IS ALWAYS TOP LEFT, ABOVE THE TITLE, IN EVERY PANEL (Daniel, 13-Oct: "if
   a back button exists its always in that very top left corner. never under
   the title. i thought that was the rule we had set"). It was — 10-Sep,
   "< back button above title" — but only the split head (Order details)
   carried it; the plain and control heads left it last, under the subtitle
   (Lead times, Resources' levels). So it is ordered first in every head, on
   the split head's own nav row (--v2-head-nav-h) and gap (--v2-head-gap,
   12px, 8 on touch: the head's 12 adjusted to it), so back-to-title reads the
   same everywhere. The split head places it by grid row already. */
.v2-panel-head .v2-panel-back {
    order: -1;
    min-height: var(--v2-head-nav-h);
    margin-bottom: calc(var(--v2-head-gap) - var(--space-3));
}
.v2-panel-head--split .v2-panel-back { margin-bottom: 0; }

.v2-panel-back svg {
    width: 18px;
    height: 18px;
    flex: none;
}

@media (max-width: 1024px) {
    /* ── TOUCH KEEPS THE TWO COLUMNS, AND THE REF STAYS ONE LINE ───────
       Daniel, 10-Sep, spelling out the shape he wants on a phone:

           Title                          No | Clinic
           Back button              new query

       So the same grid as the desktop, not a stack. This took two wrong turns
       to get to and both are worth recording, because they are the two obvious
       answers:

         - STACKING THE REF into two right-aligned lines fitted the columns but
           broke the ref itself. It is ONE fact — which order is this — and split
           over two lines above a pair of buttons it read as two unrelated
           labels: "the name and clinic on mobile above the new query button
           doesnt work."
         - COLLAPSING TO ONE COLUMN kept the ref intact and spent a whole row on
           it, pushing everything down: "lets but the name and clinic on the same
           top line with the coloured seperator as normal."

       Both columns, one line, separator intact. What gives instead is the ref's
       type size — 12px, --type-caption-size, a real step rather than an invented
       one. "Order queries" at 24px is ~150px and the ref at 12px is ~170px,
       which is 330 of the 346px a 402px panel has: "it should always fit."
       A clinic long enough to beat that wraps inside its own column rather than
       squeezing the title, since column 1 is the flexible one. */
    .v2-panel {
        --v2-head-gap: var(--space-2);   /* 8px on touch */
    }

    .v2-panel-head--split {
        grid-template-columns: minmax(0, 1fr) auto;
        column-gap: var(--space-3);   /* 12px */
    }

    /* ── ON TOUCH THE WAY BACK GOES TOP-LEFT ─────────────────────────
       Daniel, 10-Sep: "on mobile thats add all the back buttons in the upper
       top left corner of the panels not below. i thnk thats the best way."

       He is right, and it is worth saying why rather than just moving it. On a
       phone this is the ONLY way out of a panel — the left rail is hidden and
       the brand only goes home — so it is navigation, and navigation belongs
       where every phone puts it: first thing, top-left. Under the title it was
       a footnote to the heading. Above it, it is the screen's chrome.

       Desktop keeps it under the title, where the nav rail is also on screen
       and this is one route among several rather than the only one. Uniform
       PER WIDTH, which is what "no uniformaty from page to page" was asking
       for — every panel agrees at any given size.

       NO PLACEMENTS HERE. The rows and columns are declared once, above, and
       hold at every width — that is the whole of what "should be respected on
       desktop too" asked for, and what four passes of per-screen tweaking kept
       breaking. This block only changes SIZES.

       The ref drops to 12px because a 24px title and a 14px ref will not share
       346px: "Order queries" is ~150px and the ref at 12px ~170px, which is 330
       of the 346 a 402px panel has. "it should always fit." */
    .v2-panel-head--split .v2-panel-sub {
        font-size: var(--type-caption-size);   /* 12px */
    }

    /* ── THE ACTION FILLS THE REFERENCE'S WIDTH ──────────────────────
       "have the new query button span the width of the Order number through the
       clinic? for balance."

       Stretched rather than end-justified, which is the one thing that differs
       from desktop: on a phone there is a single control in this slot and it
       should fill the column the ref sizes, where a desktop head has two or
       three that keep their natural widths and hang off the same edge.

       Order details' slot is empty at this width, its pair having moved into
       the body, so its row 3 collapses and the head is two rows — the :empty
       rule above does that, so neither panel needs its own placements. */
    .v2-panel-head--split .v2-panel-head-aside {
        justify-self: stretch;
    }

    /* 44px of TARGET without moving the text: negative margin cancels the
       padding, so the control sits exactly where it looks like it sits while
       answering §6's touch height. */
    .v2-panel-back {
        font-size: var(--type-body-size);   /* 15px */
        padding: 10px 0;
        margin: -10px 0;
    }
}

/* Placeholder. Names what will live here rather than saying "coming soon", so on
   staging an unfinished feature is distinguishable from a broken one. */
.v2-panel-todo {
    display: grid;
    gap: var(--space-3);
    padding: var(--space-8);
    border: 1px dashed var(--border-medium);
    border-radius: var(--radius-card);
    background: var(--surface-card);
    color: var(--text-muted);
    font-size: var(--type-body-size);
}

.v2-panel-todo strong { color: var(--text-strong); }

/* A one-line caveat inside otherwise-real content — lighter than the dashed
   placeholder above, which is for a panel body that doesn't exist yet. This is
   for a control that is drawn and correct but not yet connected. */
.v2-panel-note {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--type-caption-size);
}


/* ── Panel bodies ────────────────────────────────────────────────────────
   Everything below the head. The frames call this container "Frame 464": 850px
   wide (930 panel − 2 × 40 padding) with a 20px gap between blocks.

   EVERY PANEL BODY MUST BE WRAPPED IN THIS. On desktop it is the element that
   scrolls, so a panel that puts its content directly under .v2-panel-head has
   nowhere to send the overflow and gets clipped by the shell instead. The six
   unbuilt panels use .v2-panel-todo without it, which is safe only because a
   two-line placeholder cannot overflow — not a pattern to copy.

   Split this into its own file once a third panel body lands — at that point
   ui_next_stylesheets() needs the new name too. */
.v2-panel-body {
    display: grid;
    gap: var(--space-5);                    /* 20px · Frame 464 */
}

/* Two 415px cards, 20px apart, in an 850px row. Expressed as fractions rather
   than the literal 415px so the panel can narrow without the row breaking. */
.v2-choice-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-5);
}


/* ── Lead Times ──────────────────────────────────────────────────────────
   LeadTimes/Desktop/Light 1300:3351 and /Dark 1300:3299.

   The panel is a chooser, not a table: two product-type cards leading to the
   per-brand view (LeadTimes/Category, 1300:2368 — not built), plus a direct SKU
   lookup for when the nurse already knows the code. */
/* No .v2-lookup gap override any more: .v2-card is 30px by default, because every
   Section/FormField in the file uses that — here and across Register's three.

   The ramp question stands though: 30px is off §6's stated 4/8/12/16/20/24, as is
   the 10px inside a choice card. The frames use both, so §6's "even numbers" reads
   as looser than the ramp it lists. Still on the list for Justin. */

/* The two product-type glyphs. Keyed off data-v2-leadtimes-type, which the
   markup already carries as the card's target, and declared HERE so the url() is
   resolved against this stylesheet — see the note in the nav-icons block above
   for why an inline custom property is the wrong place for it.

   Boxes are the exports' own, carried per card because the two are genuinely
   different shapes: one shared size would squash one of them. The plaster's -40°
   rotation is baked into its SVG rather than applied here, because a mask cannot
   be rotated apart from its box. */
.v2-choice-card[data-v2-leadtimes-type="lymphoedema"] .v2-choice-glyph {
    width: 36.003px;
    height: 45px;
    --v2-mask: url("data:image/svg+xml,%3Csvg preserveAspectRatio='none' overflow='visible' style='display: block;' width='36.0029' height='45' viewBox='0 0 36.0029 45' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath id='Union' d='M16.9395 30.4395C17.5252 29.8537 18.4748 29.8537 19.0605 30.4395L24.0605 35.4395C24.6463 36.0252 24.6463 36.9748 24.0605 37.5605C23.4748 38.1463 22.5252 38.1463 21.9395 37.5605L19.5 35.1211V43.5C19.5 44.3284 18.8284 45 18 45C17.1716 45 16.5 44.3284 16.5 43.5V35.1211L14.0605 37.5605C13.4748 38.1463 12.5252 38.1463 11.9395 37.5605C11.3537 36.9748 11.3537 36.0252 11.9395 35.4395L16.9395 30.4395ZM31.0977 17.5947C33.8062 17.5947 36.0025 19.7906 36.0029 22.499C36.0029 25.2078 33.8065 27.4043 31.0977 27.4043H4.9043C2.1958 27.4039 0 25.2076 0 22.499C0.000441461 19.7908 2.19607 17.5951 4.9043 17.5947H31.0977ZM4.9043 20.5947C3.85293 20.5951 3.00044 21.4477 3 22.499C3 23.4851 3.74963 24.2969 4.70996 24.3945L4.9043 24.4043H31.0977L31.293 24.3945C32.1892 24.3033 32.9021 23.5906 32.9932 22.6943L33.0029 22.499C33.0025 21.4474 32.1493 20.5947 31.0977 20.5947H4.9043ZM18 0C18.8284 0 19.5 0.671573 19.5 1.5V9.87891L21.9395 7.43945C22.5252 6.85367 23.4748 6.85367 24.0605 7.43945C24.6463 8.02524 24.6463 8.97476 24.0605 9.56055L19.0605 14.5605C18.4748 15.1463 17.5252 15.1463 16.9395 14.5605L11.9395 9.56055C11.3537 8.97476 11.3537 8.02524 11.9395 7.43945C12.5252 6.85367 13.4748 6.85367 14.0605 7.43945L16.5 9.87891V1.5C16.5 0.671573 17.1716 0 18 0Z' fill='%233A9FD4'/%3E%3C/svg%3E");
            -webkit-mask-image: var(--v2-mask);
            mask-image: var(--v2-mask);
}

.v2-choice-card[data-v2-leadtimes-type="dressings"] .v2-choice-glyph {
    width: 44.549px;
    height: 41.599px;
    --v2-mask: url("data:image/svg+xml,%3Csvg width='44.5486' height='41.5993' viewBox='0 0 44.5486 41.5993' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate(22.2743 20.7997) rotate(-40) translate(-21.26905 -9.3052)'%3E%3Cpath id='Union' d='M33.2334 0C38.3725 0.000122939 42.5381 4.16657 42.5381 9.30566C42.5378 14.4446 38.3723 18.6102 33.2334 18.6104H9.30566C4.16664 18.6104 0.000237932 14.4446 0 9.30566C0 4.1665 4.1665 0 9.30566 0H33.2334ZM30.1826 2.6582C30.3719 3.09121 30.4785 3.56944 30.4785 4.07227V14.5391C30.4784 15.0417 30.3719 15.5193 30.1826 15.9521H33.2334L33.5752 15.9434C37.0869 15.7654 39.8797 12.8616 39.8799 9.30566C39.8799 5.6349 36.9041 2.65833 33.2334 2.6582H30.1826ZM16.4785 2.65918C15.6982 2.65938 15.0656 3.29193 15.0654 4.07227V14.5391L15.0723 14.6836C15.1449 15.396 15.747 15.952 16.4785 15.9521H26.9453L27.0898 15.9453C27.7548 15.8776 28.2838 15.3485 28.3516 14.6836L28.3584 14.5391V4.07227C28.3582 3.34071 27.8023 2.73855 27.0898 2.66602L26.9453 2.65918H16.4785ZM9.30566 2.6582C5.63483 2.6582 2.6582 5.63483 2.6582 9.30566C2.65843 12.8616 5.45204 15.7654 8.96387 15.9434L9.30566 15.9521H13.2402C13.051 15.5193 12.9454 15.0417 12.9453 14.5391V4.07227C12.9454 3.5695 13.0509 3.09116 13.2402 2.6582H9.30566ZM19.0537 10.6348C19.7879 10.6348 20.3828 11.2307 20.3828 11.9648C20.3826 12.6988 19.7877 13.2939 19.0537 13.2939C18.3198 13.2938 17.7249 12.6987 17.7246 11.9648C17.7246 11.2307 18.3196 10.6349 19.0537 10.6348ZM24.3711 10.6348C25.1051 10.635 25.7002 11.2308 25.7002 11.9648C25.6999 12.6987 25.1049 13.2938 24.3711 13.2939C23.6371 13.2939 23.0413 12.6988 23.041 11.9648C23.041 11.2307 23.6369 10.6348 24.3711 10.6348ZM19.0537 5.31738C19.7878 5.31738 20.3827 5.91245 20.3828 6.64648C20.3828 7.38065 19.7879 7.97656 19.0537 7.97656C18.3196 7.97652 17.7246 7.38062 17.7246 6.64648C17.7248 5.91247 18.3197 5.31743 19.0537 5.31738ZM24.3711 5.31738C25.105 5.31746 25.7 5.91257 25.7002 6.64648C25.7002 7.3806 25.1052 7.97648 24.3711 7.97656C23.6369 7.97656 23.042 7.38065 23.042 6.64648C23.0422 5.91252 23.6371 5.31738 24.3711 5.31738Z' fill='%233A9FD4'/%3E%3C/g%3E%3C/svg%3E");
            -webkit-mask-image: var(--v2-mask);
            mask-image: var(--v2-mask);
}

.v2-lookup-head { display: grid; gap: var(--space-3); }   /* 12px */

.v2-lookup-title {
    margin: 0;
    font-size: var(--type-h2-size);         /* 24px Bold */
    font-weight: 700;
    line-height: 1.2;
    /* The frame uses colour/flint light / colour/seafoam dark — that pair is
       --text-body, not --text-strong. Faithful to the file even though it is a
       heading; --text-strong would be a step darker than drawn. */
    color: var(--text-body);
}

.v2-lookup-sub {
    margin: 0;
    max-width: 350px;                       /* the frame's own measure */
    font-size: var(--type-body-size);
    font-weight: 500;
    color: var(--text-muted);
}

/* Field 662 + gap 8 + button 140 = 810, i.e. the card's 850 less its 2 × 20
   padding. Held as fr + fixed so the field takes the slack on a narrower panel
   and the button keeps its designed width. */
.v2-lookup-form {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 140px;
    gap: var(--space-2);                    /* 8px */
    align-items: center;
}

.v2-lookup-form .v2-panel-note { grid-column: 1 / -1; }



/* ── Desktop: the panel scrolls, the window doesn't ──────────────────────
   See the long note on .v2-panel for why, and for where the numbers come from.

   MIN-HEIGHT: 0 AT EVERY LEVEL IS THE WHOLE TRICK. A grid or flex item's
   automatic minimum size is its CONTENT, so a `1fr` track silently grows past
   its share rather than capping — and one uncapped ancestor anywhere in the
   chain hands the overflow back to the window. The chain here is:

       .v2-shell        height: 100dvh  (definite, so 1fr means something)
        └ .v2-dash-mid  grid-row 2, the 1fr track        → min-height: 0
           └ .v2-section  row 1 of dash-mid's own 1fr    → min-height: 0
              └ .v2-panel-col  stretched to that row     → min-height: 0
                 └ .v2-panel   max-height: 100%          → min-height: 0
                    └ .v2-panel-body  overflow-y: auto

   .v2-shell already sets overflow: hidden for the wallpaper crop, so once its
   height is definite rather than a minimum there is nothing left to scroll. */
@media (min-width: 1025px) {
    /* min-height → height, so 1fr becomes a real cap rather than a floor. As a
       minimum it let the shell grow past the viewport, which is exactly the
       scrollbar Daniel saw.

       SECTION VIEW ONLY, deliberately. The home view keeps its min-height and is
       still allowed to grow: it has no internal scroll container to hand the
       overflow to — a greeting and a dock either fit or they don't — so pinning
       its height would trade a scrollbar for a dock clipped off the bottom of a
       short laptop window, which is the worse failure. The dock's own rules
       already scale it down before that point. */
    .v2-shell[data-v2-view="section"] { height: 100dvh; }

    .v2-shell[data-v2-view="section"] .v2-dash-mid,
    .v2-shell[data-v2-view="section"] .v2-section { min-height: 0; }

    /* AND THEY HAVE TO STRETCH, not just be allowed to shrink.
       -------------------------------------------------------------------------
       min-height: 0 lets a grid item shrink below its content; it does NOT make the
       item fill its track. .v2-section inherits align-items: start from its own
       base rule and .v2-dash-mid's items align to the start of the row, so
       .v2-section was CONTENT-sized — and a content-sized ancestor makes every
       percentage below it indefinite. .v2-panel-col stretched to a box that was
       already the height of its content, .v2-panel's max-height: 100% resolved
       against that, and the cap was a no-op for the second time.

       Proof it was still broken after the last attempt: the served overlays.css
       hashed to cdaa15e743, which is commit 7ad528db — the grid-template-rows fix
       WAS live and the panel still didn't scroll. The track was right; the thing
       the track was measured against wasn't.

       Stretch is the default for both, so this is restating it against the base
       rule's align-items: start rather than introducing anything new. */
    .v2-shell[data-v2-view="section"] .v2-dash-mid { align-items: stretch; }
    .v2-shell[data-v2-view="section"] .v2-section { align-self: stretch; }

    /* align-self, not the container's align-items: the nav column must stay
       start-aligned (its items are 44px rows, not a full-height rail) while this
       column stretches to give the panel something definite to cap against. */
    /* THE TRACK FILLS THE COLUMN; THE PANEL HUGS INSIDE IT.
       -------------------------------------------------------------------------
       This was `align-content: start` with no explicit rows, and that quietly
       defeated the panel's cap below. align-content: start sizes the row track to
       its CONTENT, so .v2-panel's `max-height: 100%` resolved against a track only
       as tall as the panel already was — a no-op. The panel grew past the viewport,
       .v2-panel-body never overflowed, and there was no scrollbar to draw. Daniel,
       18-Aug: "the scroll bar is still not there".

       The two requirements are separable and both are kept:
         the TRACK stretches   grid-template-rows: minmax(0, 1fr), so 100% means the
                               column's height and the cap has something to bite on
         the PANEL hugs        align-self: start on .v2-panel, so a short panel is
                               still its own height rather than stretched — which is
                               what the old comment was protecting (Lead Times is
                               drawn 512px tall in a 1024 frame). */
    .v2-panel-col {
        display: grid;
        grid-template-rows: minmax(0, 1fr);
        align-self: stretch;
        min-height: 0;
    }

    .v2-panel {
        display: flex;
        flex-direction: column;
        align-self: start;         /* hug when short — this is what carries the
                                      old align-content: start's intent */

        /* WHY THE FAILURE IS SILENT. .v2-shell is overflow: hidden, so when the cap
           above fails the surplus is CLIPPED by the shell rather than overflowing
           anything that could show a bar — the panel looks cut off and no scrollbar
           appears anywhere, which is exactly the symptom and is indistinguishable
           from "the scrollbar CSS is broken". It isn't; there was never an overflow
           to paint one for.
           Clipping here instead means the panel itself is the boundary: if the cap
           ever fails again the content stops at the panel's own edge, which is
           visibly wrong at the right place instead of silently wrong 200px lower.

           ONLY WHILE THE PANEL ACTUALLY OVERFLOWS, though — see below. */
        max-height: 100%;          /* cap, never stretch — Lead Times is drawn
                                      512px tall in a 1024 frame. Only bites now
                                      that the track above fills the column. */
        min-height: 0;
    }

    /* ── The clip is CONDITIONAL, or it eats the dropdowns ────────────────
       Daniel, 2-Sep: "this bug where the filter drop down clips the panel end
       when low/no results."

       Exactly that. A .v2-combo-list is absolutely positioned and escapes its
       field, but it cannot escape an ancestor that clips — so on a SHORT panel
       (one result, or none) the Status/Date/Clinic list was cut off at the
       panel's bottom edge with most of its options unreachable.

       Both requirements are satisfiable at once because they never apply at the
       same time. The clip exists to make a FAILED HEIGHT CAP visible at the
       panel's own edge, which is only a risk when there is surplus content to
       clip — i.e. when the panel overflows. A short panel has nothing to hide,
       so the diagnostic loses nothing by standing down, and the dropdown gets to
       leave the box.

       And when the panel IS tall the dropdown does not need to escape: the head
       is at the top, so there are hundreds of pixels of panel beneath the field
       for a 240px list to open into.

       data-v2-overflowing is already maintained by overlays.js — measured at
       four moments, see the note there — so this needed no new JavaScript and no
       new state. */
    .v2-panel[data-v2-overflowing] { overflow: hidden; }

    /* ── A PANEL WHOSE CONTENT SCROLLS, NOT ITS BODY ─────────────────────
       Declared in markup (data-v2-inner-scroll, from the overlay's
       'inner_scroll' key). The body stops being the scroll region so the
       content can have two of its own — Order queries has a query list and a
       conversation that must move independently, with a composer that stays
       put at the bottom.

       EVERYTHING ELSE IS UNCHANGED, which is the point: the panel still caps
       at 100% and still hugs when short, so a short one is rounded and a tall
       one squares off exactly as My Patients does. overlays.js measures the
       PANEL rather than the body for these — see the note there — because a
       capped panel whose body cannot scroll overflows itself, which is the same
       fact read off the right box.

       min-height: 0 is what lets the body be shorter than its content so the
       children inside it can scroll at all. */
    .v2-panel[data-v2-inner-scroll] .v2-panel-body {
        overflow-y: hidden;
        min-height: 0;
    }

    /* ── EXCEPT WHEN THAT PANEL IS SHOWING AN ORDINARY LIST ──────────────
       Order queries has two faces: a bounded pair of columns for a
       conversation, and a plain card list for the nav landing. The second is a
       normal collection — collection.js's cards and its Load more button want
       the panel to GROW and the shell to square it off, exactly as My Orders
       does — so the body has to be the scroller again there.

       data-view is on the panel because applyScope() puts it there for the
       head's own rules, which is what makes this expressible at all. */
    .v2-panel[data-v2-inner-scroll][data-view="list"][data-scope="global"] .v2-panel-body {
        overflow-y: auto;
    }

    /* ── AN INNER-SCROLL PANEL SPANS THE PAGE BY DEFAULT ────────────────────
       Daniel, 8-Sep: "can we make it so the chat always spans page? allowing room
       for chat growth by default?"

       Which is the right default for this one: every other panel is as tall as
       what it holds, and a conversation is not a fixed amount of content — a thread
       with two messages in it should still have somewhere to put the next twenty.
       Hugging meant a two-message thread got a short box that then had to start
       scrolling on the third.

       align-self: stretch overrides the "hug when short" above. Spanning the page
       means the bottom edge squares off, which is the shell's existing rule and
       needs nothing here: syncOverflow() measures the panel against the space it
       was given, and a stretched panel fills it by definition, so
       data-v2-overflowing gets set and takes the radius and the padding with it.

       EXCEPT ON THE LANDING LIST, which is a plain collection of cards and should
       grow and hug exactly as My Orders does. Same condition the JS uses for the
       same reason — the two must agree about which shape this panel is in. */
    .v2-panel[data-v2-inner-scroll]:not([data-scope="global"][data-view="list"]) {
        align-self: stretch;
    }

    /* ── A WORKSPACE PANEL FILLS THE COLUMN ──────────────────────────────
       Declared in markup (data-v2-fill, from the overlay's 'fill' key).

       .v2-panel's align-self: start is right for a panel that PRESENTS something
       — Lead Times is drawn 512px tall in a 1024 frame and hugging is what keeps
       it looking deliberate. It is wrong for a panel you WORK IN. Daniel,
       11-Sep: "every other panel goes page length. its just not overflowing the
       panel thats the issue."

       Request Patient Order is the first of those: a nurse builds an order in
       it, so its height should be the page's, not its current content's.
       Otherwise the panel grows and shrinks under them as a basket fills, and
       while it is short there is nowhere for a select to open into — which is
       what made the clinic dropdown look broken rather than merely early.

       Separate from data-v2-inner-scroll deliberately. That one ALSO stops the
       body scrolling and moves the foot inside, because order-queries has two
       scroll columns and a pinned composer. A workspace wants the ordinary body
       scroll and only the height. */
    .v2-panel[data-v2-fill] { align-self: stretch; }

    /* NOTHING HERE ABOUT flex OR min-height: the shell already gives every
       .v2-panel-body flex: 1 1 auto, min-height: 0 and overflow-y: auto at this
       breakpoint, further down the file. The rule that used to sit here repeated
       it and, being earlier, lost to it anyway.

       What a FILLED panel does need is to be exempt from the overflow padding —
       see below. */

    /* THE 60px FOOT IS FOR A PANEL THAT SCROLLS, AND IT CREATED A LOOP HERE.
       A filled panel's body exactly fills its space. The moment overlays.js
       measured it as spanning, .v2-panel[data-v2-overflowing] .v2-panel-body
       added 60px — which GUARANTEED scrollHeight > clientHeight, so the flag
       could never clear and the scrollbar became permanent. Daniel, 14-Sep:
       "why does the panel have a scroll at this stage?" — there was nothing to
       scroll; the padding was the content.

       data-v2-inner-scroll already sidesteps this by overriding to 36px. A
       filled panel needs none at all: .v2-panel's own 60px bottom padding sits
       outside the body and already holds the content off the edge. */
    .v2-panel[data-v2-fill][data-v2-overflowing] .v2-panel-body {
        padding-bottom: 0;
    }

    /* THE FOOT MOVES INSIDE, which is the shell's own trick for a spanning panel —
       see the note above .v2-panel[data-v2-overflowing] .v2-panel-body. The panel
       edge wants to be flush with the fold and the CONTENT wants room above it, and
       those are two different boxes. 36px rather than the 60px a scrolled list
       gets: Daniel, 8-Sep, on the 20px this was briefly cut to — "give a little
       more padding to the footer infact (undoing the recent change reducing it just
       not as much)".

       BUT NOT A THREAD, which is where 36px of it became the gap Daniel ringed in
       red on 9-Sep: "the padding on the panel just needs reducing on mobile so it
       lands closer to the bottom." The rule above is for a panel whose CONTENT
       ends and leaves an edge showing below it — room above the edge is exactly
       right for that. A conversation is the other shape: its last row is the
       composer, so the content and the edge want the same place and 36px between
       them is dead space with a text field sitting on top of it.

       AT EVERY WIDTH, not just on a phone, because the argument does not depend on
       the width — and Daniel asked for the same thing on desktop on 8-Sep: "the
       chat pane just drop the bottom a little closer to the bottom of the panel".
       The panel keeps its own 8px, which is what holds the field off the corner. */
    .v2-panel[data-v2-inner-scroll]:not([data-scope="global"][data-view="list"]) .v2-panel-body {
        padding-bottom: 36px;
    }

    /* ── Order queries: the head's columns mirror the body's ─────────────
       Which is what puts the controls where the mockup has them: the title and
       subtitle over the query list, New query at the left edge of the
       conversation, Assigned to at the far right.

       .v2-panel-head--control is a two-column grid whose control column is its
       own content width, and that could not hold both at this panel's size, so
       they stacked. Giving that column the conversation's width fixes the
       stacking and the alignment together: the two grids agree, so the button
       lines up with the card beneath it at every desktop width.

       HERE RATHER THAN IN components.css, for the reason the mobile block below
       spells out: these selectors are declared in THIS file, the layout loads
       this file after components.css, and an override over there with the
       specificity to beat the mobile rule beats it at every width. That is
       precisely what happened — a two-column head on a phone, reported as the
       panel "not rendering correctly" on mobile. Inside min-width: 1025px it
       cannot reach a phone at all.

       Below this width the shell stacks the head, the aside stretches, and the
       two controls become full-width rows — which is right for a phone and is
       what components.css's max-width: 900px block finishes off. */
    /* --split rather than --control since 10-Sep, when this head grew a back
       control and took the split shape. The reason is unchanged and still
       Daniel's from 9-Sep: the head's columns have to agree with the BODY's, or
       the New query button does not line up with the conversation card under
       it. */
    .v2-panel[data-v2-panel="order-queries"] .v2-panel-head--split {
        grid-template-columns: minmax(0, min(32%, 300px)) minmax(0, 1fr);
        gap: var(--space-5);   /* the body grid's, so the two line up */
    }

    /* .v2-panel-head-aside is flex-direction: column, because every other head
       has one control and stacks its tiles. THAT is what kept New query above
       Assigned to however the grid around them was arranged. */
    .v2-panel[data-v2-panel="order-queries"] .v2-q-head {
        flex-direction: row;
        flex-wrap: nowrap;
        /* New query hard left of the column, Assigned to hard right. */
        justify-content: space-between;
    }

    /* One control on the landing (the ViewSlider) and nothing to space it
       against, so it goes right like every other panel's. */
    .v2-panel[data-v2-panel="order-queries"][data-view="list"] .v2-q-head {
        justify-content: flex-end;
    }

    .v2-panel-body {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;

        /* THE SCROLLBAR LIVES IN THE PADDING AND MUST NOT NARROW THE CONTENT.
           ---------------------------------------------------------------------
           Daniel caught the consequence of getting this wrong (14-Aug): the
           Download button appeared to overhang the cards by ~12px. It wasn't the
           button — a classic scrollbar is taken out of the element's PADDING BOX,
           so when it appeared the cards dropped from 850 to 838 while the head,
           which doesn't scroll, stayed at 850.

           The frames put the bar 14px in from the panel's edge — x=904 in a 930
           panel — with content ending at 890. Working backwards from that:

               panel content box        40 → 890   (850 wide)
               body box, -26 margin     40 → 916
               gutter reserved             904 → 916   (12px, the bar)
               padding-right 14         content ends at 890  ✓ aligned

           scrollbar-gutter: stable is what makes it hold: it reserves the 12px
           whether or not the bar is showing, so the cards don't shift by 12px the
           moment content gets long enough to scroll. Without it the alignment is
           only correct in one of the two states.

           Safari gained scrollbar-gutter in 18.2; older versions will sit 12px
           wide when no bar is present. Worth knowing, not worth a fallback. */
        margin-right: -26px;
        padding-right: 14px;
        scrollbar-gutter: stable;
    }

    /* The bar's own appearance is NOT here. It's a library component — the
       "Scroll bars" section (841:4852) has masters for it — so it lives in
       components.css with the rest of the library, unscoped, and applies to any
       v2 scroll region rather than only to this one. */
}


/* ── Tablet and phone ────────────────────────────────────────────────────
   The 230 + 930 columns don't fit, and on a touch device the dock is already the
   navigation — it stays at the bottom of the screen in section view rather than
   being replaced by a side rail. So the nav column is dropped and the panel goes
   full width.

   AND THERE IS NO MOBILE NAV TO FALL BACK ON. This comment used to say the
   Mobile/Menu frames (1102:8660) "look like the intended mobile navigation for
   this state", and the redesign log itself contradicts that: those frames are
   what the ⋯ button should open, and "the section view no longer needs them for
   navigation... so this is now only about the menu panel's contents". Daniel,
   7-Sep, when I repeated the claim to him: "i dont think Justin made a mobile
   menu?" — he was right.

   So a touch section view has no navigation at all, and the design has none to
   copy: the log's own open question records that "it is not in the design at
   all — the frames give the section view no way back". The top row's arrow is
   ours, and it now goes UP ONE LEVEL rather than always to the dashboard, which
   is what makes a nested panel returnable-from on a phone. */
@media (max-width: 1024px) {
    .v2-section {
        grid-template-columns: minmax(0, 1fr);
        gap: var(--space-5);
    }

    .v2-sidenav { display: none; }

    /* THE DOCK IS NOT ON SCREEN IN SECTION VIEW, on touch either.
       -------------------------------------------------------------------------
       The first pass kept it live here, reasoning that on a touch device the
       dock IS the navigation and the 230px rail can't fit. Daniel corrected it
       (2026-08-11): per the spec the dock belongs to the dashboard only, and the
       mobile panel frames bear that out — RegisterPatient/Mobile 1252:1749 and
       LeadTimes/Mobile 1300:3402 both carry a 48px button in a 48px top row and
       no dock at all.

       Two things follow. The way back is the panel head's own .v2-panel-back
       — without something in the panel there would be none, since the rail is
       hidden here too and the brand only goes home. (It was the glass circle in
       the top row until 10-Sep; the brand has that corner now.) And with the
       dock gone the panel can start much higher, which is the padding change
       below. */
    .v2-shell[data-v2-view="section"] .v2-dash-nav,
    .v2-shell[data-v2-view="section"] .v2-greeting { display: none; }

    /* The band of wallpaper between the top row and the panel is 58px.
       -------------------------------------------------------------------------
       RegisterPatient/Mobile/Dark 1252:1749 puts the ⋯ row at y=70–118 and the
       panel at y=176; LeadTimes/Mobile 1300:3402 agrees. So 176 − 118 = 58.

       The frame's first 70px are the device status bar, which a browser viewport
       already sits below — so the thing to reproduce is the GAP, not the panel's
       absolute 176. Reproducing 176 would push the panel a further ~50px down on
       a real phone.

       6vh only reached the 58px ceiling on a ~970px-tall viewport, so every real
       phone got 42–48px and Daniel read it as starting too high. 8vh hits 58 by
       ~725px, which covers portrait on anything current; the 32px floor is for
       landscape, where there isn't the height to spend. */
    .v2-shell[data-v2-view="section"] .v2-dash-mid {
        padding-top: clamp(32px, 8vh, 58px);
    }

    /* 60px of top padding, NOT 24. This is the gap Daniel was actually pointing
       at — the one INSIDE the panel, between its top edge and the title. I'd
       been adjusting the gap above the panel, which was already right, while the
       visible shortfall was three times bigger and one level in.

       The mobile frames are unanimous: the panel's first child sits at y=60,
       x=18, in all five of them —

           RegisterANewPatient/Mobile  1300:4952 → title 1300:4953  x=18 y=60
           LeadTimes/Mobile            1300:3456 → title 1252:1215  x=18 y=60
           Resources/Mobile            1252:1214 → title 1252:1328  x=18 y=60
           …and 1300:4569 / 1300:4690 likewise

       So the vertical padding is the SAME 60px as desktop; only the sides come
       in. The frames are all 402 wide (phone), so 18px is a phone value — a
       tablet at 1024 with 18px sides would be uncomfortably tight, and the
       design has no tablet frame to read. 24px here, 18px at the phone
       breakpoint below; the tablet number is an interpolation, flagged as one. */
    .v2-panel { --panel-pad-x: var(--space-6); }
}

@media (max-width: 640px) {
    /* 60px / 18px — the frames' own numbers, measured on five mobile panels.
       Only the sides differ from the tablet value above. */
    .v2-panel {
        border-radius: var(--radius-card);
        --panel-pad-x: 18px;
    }

    .v2-panel-title { font-size: var(--type-h2-size); }   /* 36px is too big here */
    /* At 24px the title leaves 5.6px under its baseline, not 8.4: the same
       12px to the subtitle's capitals takes 12px back (see the desktop rule). */
    .v2-panel-title + .v2-panel-sub { margin-top: -12px; }
    .v2-panel-head { margin-bottom: var(--space-5); }

    /* The mobile frames stack the product-type cards full width
       (LeadTimes/Mobile/Dark 1300:3456 — 366px cards in a 402px viewport). */
    .v2-choice-row { grid-template-columns: minmax(0, 1fr); }

    /* 140px of button next to a 15px placeholder leaves the field unusably
       narrow on a phone, so the search moves under the input. */
    .v2-lookup-form { grid-template-columns: minmax(0, 1fr); }

    /* Touch target: §6's 36px control height is a desktop value, and the mobile
       frames use 44px throughout (1300:4964, 1300:4992). */
    .v2-field,
    .v2-lookup-form .v2-button { min-height: 44px; }

}


/* ── A panel that RUNS OFF THE BOTTOM loses its bottom edge ───────────────
   Daniel, 18-Aug: "rounded edges should only show for the panels where the bottom is
   visible", and for the ones that scroll, "no bottom padding or rounded edges showing
   (as they would be off page)".

   DERIVED, NOT DECLARED — which is why the old --bleed modifier had to go and this
   isn't just it renamed. --bleed made the VIEW assert "this content overflows", a
   fact about rendered height that PHP cannot know and CSS cannot query. So it was
   wrong for any panel whose content changed, and My Patients' cards arrive after
   paint, which no static flag can anticipate.

   overlays.js measures the body instead — scrollHeight against clientHeight — and
   sets data-v2-overflowing, re-checking whenever the content resizes. One rule, no
   per-panel key, and it stays correct as content loads.

   Both halves are the same idea: if the bottom edge is off-screen then a radius is
   invisible, and 60px of bottom padding is a band of nothing sitting above the fold.
   Not inside the desktop media query — on mobile the panel is never capped, so it
   never overflows and the attribute is never set. */
/* UNCONDITIONAL. Daniel, 18-Aug: "just have them match. it should be the global rule
   and simple? the fact 2 things are happening to one element makes no sense."

   He is right and I should have done this three attempts ago. The measured version
   worked on My Patients and not on Register, and rather than accept that a
   conditional I could not make behave was the wrong shape, I kept refining it. A
   rule that needs JavaScript to decide which of two ways it applies is not a global
   rule — it is two rules with a coin toss.

   So: every panel loses its bottom radius and its bottom padding, full stop. The
   panel runs off the bottom, which is what both of these panels are doing in
   practice, and the JS that measured for it is deleted along with the attribute.

   KNOWN TRADE, flagged rather than hidden: Lead Times is short enough for its bottom
   edge to be on screen, and it will now show two square corners there. That is a
   visible cost and Daniel's to weigh — but it is one honest, predictable rule
   instead of an unpredictable one, and if the rounded edge matters more than the
   consistency the fix is a single declaration back on a short-panel selector we
   agree on, not a measurement. */
/* ── A panel that SPANS OFF THE PAGE has no bottom edge ───────────────────
   Daniel's rule, stated plainly (18-Aug):
     spans off page      scrollbar, no bottom edge
     does not span       no scrollbar, no cut off

   That is ONE rule with a condition on a physical fact — does this content overflow —
   not two rules. I argued the opposite earlier ("a rule that needs JS to decide isn't
   global") and that was rationalising a broken implementation, not a principle. The
   condition is legitimate; my measuring of it was what kept failing.

   overlays.js sets data-v2-overflowing from the body's scrollHeight against its
   clientHeight, and measures at several moments rather than one — see the note there
   for why belt-and-braces is warranted here. */
.v2-panel[data-v2-overflowing] {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    padding-bottom: 0;
}

/* THE 60px IS NOT LOST — IT MOVES INSIDE THE SCROLLER.
   -----------------------------------------------------------------------------
   Dropping the panel's bottom padding is right for the EDGE (a spanning panel
   should have no visible bottom), and it was wrong for the CONTENT: the last
   thing in the list ended flush against the bottom of the scroll box, so
   scrolling to the end of My Patients put "Load more patients" hard on the
   window edge with nothing under it (Daniel, 2-Sep: "give it a little more foot
   room to the edge of page").

   The two are different spaces and were being served by one declaration. The
   panel's padding sits OUTSIDE .v2-panel-body, so it can only ever hold the
   panel's edge off the fold — it can't give scrolled content room, because
   content scrolls past it. Room at the end of a scroll region has to be inside
   the scroll region.

   So the same 60px is re-applied to the scroller, and only in the overflowing
   case: a panel that fits still gets its room from the panel's own padding, and
   adding this there as well could tip a nearly-fitting panel into scrolling to
   show space that is already on screen. */
/* NOT A THREAD, for the reason given above the 36px: a conversation ends in its
   composer, so content and edge want the same place. Without this the gap comes
   straight back the moment overlays.js decides the panel spans the fold. */
.v2-panel[data-v2-overflowing] .v2-panel-body { padding-bottom: 60px; }

/* ══ EXCEPT ON A PHONE, WHERE THE COMPOSER IS THE EDGE ════════════════════════
   Daniel: "how the chat container has some space before page end. but on v2 its
   touching page end... thats wanted on desktop, not on mobile."

   Both rules above give a spanning panel room under its content, and I excluded
   threads from both while fixing the phone — where the composer IS the panel's
   last row and 36px under it read as dead space. That was right for the phone
   and wrong everywhere else: on desktop the conversation is a card inside the
   panel, so the panel's own room below it is exactly what the frame shows
   (1913:10944), and taking it away pushed the card onto the page edge.

   So the exclusion is a phone rule now, not a thread rule. The specificity is
   deliberate: [data-v2-inner-scroll] and :not([hidden]) bring this level with
   the 36px rule above, and being later in the same file settles the tie —
   which is the only reason it can win without being written as a longer
   selector than it deserves. */
@media (max-width: 900px) {
    .v2-panel[data-v2-inner-scroll][data-view="thread"]:not([hidden]) .v2-panel-body {
        padding-bottom: 0;
    }
}

/* The radius and the 60px stay on .v2-panel itself for every other panel, which is
   the not-spanning case: bottom edge visible, rounded, content not cut off.

   Squaring them unconditionally fixed the tall panels and cut the short ones off
   (Daniel, 18-Aug: "the panles that do NOT go off page are cut off"), which is the
   trade this file predicted. Making it conditional needed JS, and that produced two
   behaviours from one rule — the thing he ruled out.

   Both of those are treating the symptom. THE REAL PROBLEM IS THAT A TALL PANEL
   STOPS SHORT OF THE WINDOW: it is capped at .v2-panel-col, and that column ends
   above the viewport bottom, so its rounded edge and its padding are on screen where
   the design expects them to be off it. Get the column to the bottom of the window
   and this whole question disappears — a tall panel's edge is then past the fold and
   invisible, a short one's is where it always was, and neither needs a rule or a
   measurement.

   That is one geometry change, not a variant, and it is the only version of this that
   is genuinely global. It needs finding where the gap below the column comes from
   first, which is the next thing to do rather than another rule here. */

/* ── The search row pins under the title as you scroll ───────────────────
   Daniel, 18-Aug — either a back-to-top button, or the search "reappears dynamically
   into the header". The search, because back-to-top makes you scroll AND THEN act on
   what is really "I want to search again".

   ONE ELEMENT, NOT TWO. It stays where the frame puts it — the first thing in the
   body — and sticks to the top of the scroller when it gets there. No pinned copy in
   the head to keep in sync, no duplicate input id, no reflow when it appears.

   z-index: 3 IS THE WHOLE FIX, and it is why my first attempt failed. I used 1, which
   is below .v2-panel-head's 2, so the head's scrim-and-blur fade painted over the
   search — it came out greyed with cards smeared behind it. .v2-panel-body does not
   create a stacking context (overflow alone doesn't), so a sticky child competes
   directly with the head and 3 beats 2. The cards are static and stay below the head,
   so they still slide under the fade exactly as before: the search rises above it,
   nothing else does.

   Its own --surface-card fill makes it opaque, so results pass behind rather than
   through it. */
/* NOT STICKY. Twice I built sticky and twice it was wrong, the second time for a
   reason I should have predicted: to be visible a sticky search has to outrank
   .v2-panel-head, and once it does it is sitting ON the head's fade band — so the
   panel "loses the fade on scroll" (Daniel, 18-Aug). The fade and a sticky child of
   the body are competing for the same 64px, and one of them has to lose.

   IT MOVES INSTEAD. patients.js relocates the actual element into the head when it
   scrolls out of view, and back when it returns. The head is already above the fade
   by design, so there is nothing to out-rank and nothing to cover: the fade keeps
   working on the cards, which is all it was ever for.

   One element, so one input, one value, one id — no copy to keep in sync. */
/* KEYED ON THE ROLE, NOT ON THE COMPONENT — and that distinction is a bug I
   shipped rather than a refinement.
   -----------------------------------------------------------------------------
   This was `.v2-panel-head .v2-search`, which is the class of the ONE partial
   that happened to fill this slot at the time. My Orders' filter card is a
   different component (.v2-order-filters), so it matched nothing, fell into the
   head's ordinary auto-placement, and landed in row 1 column 2 — squeezing "My
   Orders" into a narrow left column with the filter card beside it and shoving
   the view toggle down to row 2. Daniel, 2-Sep: "the search bar and title
   cramping instead of stacking".

   The slot is what needs the placement: whatever the panel loop renders from its
   'head_below' key goes full width beneath the title row. So the partials carry
   data-v2-head-below and the rule addresses that, which means the next panel to
   use the slot cannot repeat this.

   :not(--split) IS LOAD-BEARING, and leaving it off broke Order queries outright
   — Daniel, 10-Sep: "you have completely brokent he headers on the v2 ... just
   on Queries in fact", with a screenshot of the search row sitting on top of the
   title.

   A --split head is a two-row grid whose rows are named explicitly, and its own
   rule puts this slot on row 3. That rule and this one have the SAME
   specificity — one class plus one attribute each — so source order decides,
   and this one is nine hundred lines further down the file. It won, put the
   filter row back on row 2, and row 2 is the title's.

   Order queries is the only panel carrying both a split head and this slot,
   which is exactly why it was the only page that broke. Excluding split here is
   better than out-specifying it there: the two rules now describe two different
   head shapes rather than racing for the same one. */
.v2-panel-head:not(.v2-panel-head--split) [data-v2-head-below] {
    /* AN EXPLICIT ROW, not just a span. The head is grid-auto-flow: column with
       `1fr auto` when it carries a trailing control, so grid-column: 1 / -1 alone
       claimed row 1 and pushed the title block sideways — Daniel, 18-Aug: "its in the
       wrong place?" Naming row 2 puts it beneath the title and the view toggle, full
       width, which is where he asked for it ("above the selector" being the other
       option, and below reads better because a full-width field next to a 72px toggle
       is what crowds). */
    grid-row: 2;
    grid-column: 1 / -1;
    margin-top: var(--space-3);
}

/* ── The head goes to ONE COLUMN on a phone ──────────────────────────────
   My Orders' trailing slot holds three stat tiles, and there is no seating them
   beside a title in 370px — Daniel's reference has them three across the full
   width above the search (2-Sep: "mobile/tablet isnt quite right either").

   Three things have to move together, which is why they are one block:

     - auto-flow back to ROW, or the two-column template keeps the aside beside
       the title and squeezes both
     - the explicit grid-row: 2 above has to be released, because the aside now
       occupies that row — leave it and the filter card lands on top of the tiles
     - margin-top goes, since the head's own 12px gap is now doing that spacing

   HERE RATHER THAN IN components.css, deliberately: these two selectors are
   declared in this file, the layout loads this file AFTER components.css, and a
   media query adds no specificity — so the same override written over there
   would lose regardless of where in the file it sat. That is the trap that put a
   56px nav column on a phone once already.

   The view toggle needs nothing: components.css already hides it below 1024px,
   which is the frames' own call. */
@media (max-width: 900px) {
    .v2-panel-head--control {
        grid-auto-flow: row;
        grid-template-columns: minmax(0, 1fr);
        align-items: start;
    }

    /* Split heads excluded for the same reason as the rule above: their rows are
       named, and grid-row: auto here would hand this slot back to
       auto-placement inside a grid whose first two rows are already claimed. */
    .v2-panel-head:not(.v2-panel-head--split) [data-v2-head-below] {
        grid-row: auto;
        margin-top: 0;
    }
}

/* SUPERSEDED NOTE, kept because the reasoning was wrong in an instructive way:
   -----------------------------------------------------------------------------
   Daniel asked for it to stay put on scroll (18-Aug) and I made it sticky inside
   .v2-panel-body. It came out faded with the cards visible through it, which is the
   global rule pushing back rather than a bug in the sticky: .v2-panel-head owns the
   pinned scrim-and-blur fade at z-index 2, and a sticky child of the body sits below
   that, so the fade paints over the search and blurs whatever passes behind it.

   Raising the search above the fade would mean giving the body a stacking context
   too, and then the CARDS would paint over the head as well — trading one broken
   thing for a worse one.

   THE PANEL HEAD IS ALREADY THE PINNED REGION. Anything that must stay while content
   scrolls belongs in it, which is what the title and the view toggle do. So the fix
   is to render the search in the head via a slot, exactly like head_control, not to
   fight the fade from underneath. Left un-sticky rather than half-working until that
   is agreed — see the note to Daniel. */


/* ══════════════════════════════════════════════════════════════════════════
   THE MODAL

   Daniel, 26-Aug, pointing at the Patient details frames: "if we look at things
   like the attched, it gives inspiration for what a modal should like like?"

   So the language is the panel's, one step in: a card that holds cards. What it
   does NOT take from those frames is their type scale — .v2-record-cell runs
   8.7px labels over 10.9px values because it is a TILE in a grid of twelve. A
   modal is a detail view with one thing in it and the whole screen dimmed behind
   it; the same sizes there would be a deliberate squint. Detail sizes, tile
   language.

   THE DIALOG IS THE BACKDROP HOLDER, .v2-modal-panel IS THE CARD. Two elements
   rather than one, and not for styling: a click on the dialog itself is the only
   way to detect a click OUTSIDE the content, because there is no backdrop-click
   event. One element and every click would dismiss. See modals.js.
   ══════════════════════════════════════════════════════════════════════════ */

/* Must match FADE_MS in modals.js.
   160ms was too fast to read as a movement — Daniel, 26-Aug: "the modal appears
   to slide in but does so extremeley quickly - feels odd". The oddness is that
   at that speed the eye catches the displacement but not the travel, so it reads
   as a jump rather than an arrival. 240ms with a decelerating curve lands it. */
:root {
    --modal-fade: 240ms;
    /* Fast out of the gate, slow into place. The default `ease` is symmetrical,
       which is what made a short transition feel mechanical. */
    --modal-ease: cubic-bezier(0.16, 1, 0.3, 1);
}

.v2-modal {
    /* The dialog fills the viewport and centres its panel, rather than using the
       UA's own centring — which is margin:auto on a block whose size we do not
       control, and fights a max-height in a way flex does not. */
    display: flex;
    align-items: center;
    justify-content: center;
    /* ── 100%, NOT 100vw ───────────────────────────────────────────────────
       Daniel, 20-Sep: "modal was off centre when i went to product variant".

       100vw is the viewport INCLUDING the page's scrollbar, so on any page
       long enough to scroll the dialog was a scrollbar wider than the space it
       could actually use — and a flex-centred panel inside it sat half a
       scrollbar right of true centre. Six pixels, which is invisible until a
       narrow dialog puts it next to a centred title and it is not.

       A dialog in the top layer takes the viewport as its containing block, so
       100% is the same measurement without the scrollbar in it. */
    width: 100%;
    max-width: 100%;
    height: 100dvh;
    max-height: 100dvh;
    padding: var(--space-5);
    border: 0;
    background: none;                  /* the scrim is ::backdrop, not this */
    overflow: hidden;                  /* the PANEL scrolls, not the dialog */
    /* A SWIPE IN A MODAL STAYS IN THE MODAL (Jake, 15-Oct). overflow: hidden
       makes the dialog a scroll container, so this holds: a swipe starting
       on the backdrop, or running off the end of the body below, is not
       handed on to the page behind it. */
    overscroll-behavior: contain;
}

/* display:flex above would show the dialog even when closed — [open] is what the
   UA's own display:none rule keys off, and overriding display defeats it. */
.v2-modal:not([open]) { display: none; }

.v2-modal::backdrop {
    /* Deliberately not a token: ::backdrop is outside the document tree, so it
       inherits NOTHING — custom properties defined on :root do not reach it in
       every engine. A literal is the honest thing here. */
    /* 0.55 left the panel behind it legible enough to keep reading, which works
       against the whole point of going modal (Daniel, 26-Aug). */
    background-color: rgba(19, 26, 29, 0.72);

    /* AND IT BLURS. Justin, via Daniel 26-Aug: "Regarding the background can it
       blur a little so the focus is on the modal? Like we have elsewhere."

       8px, not the glass dock's --glass-blur: 20px. "Elsewhere" is a panel
       blurring a photograph behind it, where the subject is the panel and the
       wallpaper is texture. Here it is the whole working UI behind a question,
       and at 20px a form the clinician is mid-way through stops being
       recognisable as their own work. "A little" is the instruction; this is it.

       NOT A TOKEN, for the same reason the colour above is not: ::backdrop sits
       outside the document tree and inherits nothing, so a custom property
       defined on :root does not reliably reach it. -webkit- first for Safari.

       WORTH KNOWING: blur and darkness do the same job. The 0.72 above was set
       when there was no blur to help — with both, it may read heavier than
       needed, and the scrim is the dial to reach for rather than this. */
    -webkit-backdrop-filter: blur(8px);
            backdrop-filter: blur(8px);

    opacity: 0;
    transition: opacity var(--modal-fade) var(--modal-ease);
}

.v2-modal[data-v2-modal-open]::backdrop { opacity: 1; }

/* ── The card ────────────────────────────────────────────────────────────── */

.v2-modal-panel {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    /* SIZED TO ITS CONTENT, capped by the viewport. Two goes at this.

       First it was a flat 520px — a phone-sized card in the middle of a 1440px
       screen. Then min(92vw, 760px), which was worse in a different way,
       because `width: 100%` alongside it meant every modal FILLED the cap
       whether it had the content for it or not (Daniel, 26-Aug: "its far too
       wide for its content... the modal should in theory flex depending on the
       content its for").

       So the cap is a CEILING, not a width. fit-content lets a short
       confirmation stay small; a paragraph grows until --modal-max stops it
       and then wraps. min-width keeps a two-word modal from becoming a tile.

       --modal-max is the per-modal dial. 560px is a comfortable measure for
       body copy; a modal that genuinely needs more (a table, a long list) sets
       it rather than every modal paying for the widest case. */
    width: fit-content;
    min-width: min(92vw, 320px);
    max-width: min(92vw, var(--modal-max, 560px));
    /* Bounded, and the BODY takes the scroll — a modal taller than the viewport
       with the actions off the bottom is a modal you cannot answer. */
    max-height: 100%;
    padding: var(--space-5);
    border-radius: var(--radius-card);          /* 20px, as every card; the
                                     rim below inherits it */
    /* ── VOLUME, NOT A FLAT RECTANGLE ──────────────────────────────────────
       Daniel, 20-Sep: "just be mindful with these modals also is they dont
       have any sort of valume, glass effect. they are very flat".

       Two things carry it, and both are the shell's own material rather than
       anything new: the five-stop shadow read verbatim off the Dock node, and
       the GRADIENT RIM — a highlight catching the top edge, fading across the
       middle, picking up again at the bottom. A flat hairline reads as an
       outline drawn around a shape; the rim reads as light landing on one.
       The rim itself is in components.css beside the dock's, so there is one
       implementation of the material and not two that can drift.

       The background stays OPAQUE. A modal sits over a dimmed page, not over
       the wallpaper, so a translucent fill would buy a suggestion of depth at
       the cost of every word on it. Glass here means the edge and the
       elevation, and the calendar inside it is what actually lifts. */
    background-color: var(--surface-page);
    box-shadow: var(--glass-shadow), 0 18px 48px rgba(0, 0, 0, 0.32);

    outline: none;

    opacity: 0;
    /* Less travel than before, and more of the movement carried by the scale.
       A bigger panel moving the same 8px reads as a slide; the same panel
       growing into place reads as arriving. */
    transform: translateY(4px) scale(0.97);
    transition: opacity var(--modal-fade) var(--modal-ease),
                transform var(--modal-fade) var(--modal-ease);
}

.v2-modal[data-v2-modal-open] .v2-modal-panel {
    opacity: 1;
    transform: none;
}

@media (prefers-reduced-motion: reduce) {
    .v2-modal::backdrop,
    .v2-modal-panel { transition: none; }
    .v2-modal-panel { transform: none; }
}

/* A quieter line under the actions — the clinician's PDF, which is useful at
   this moment but is not the decision being asked for. Separated by a rule
   rather than just space, so it reads as an aside and not a third button. */
.v2-modal-aside {
    margin: 0;
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-subtle);
    text-align: center;
    font-size: var(--type-caption-size);
}

/* The .v2-button--text default is left-aligned for a form row; centred here. */
.v2-modal-aside .v2-button--text { text-align: center; }

/* The dial, for the modals that need it. Deliberately two named steps rather
   than an inline style per instance — a modal wide enough to need saying so is
   a decision worth being able to find. */
.v2-modal--wide  .v2-modal-panel { --modal-max: 760px; }
.v2-modal--narrow .v2-modal-panel { --modal-max: 420px; }

/* On a phone the dialog's own padding is most of the margin, so it comes down
   and the panel gets the room instead. */
@media (max-width: 600px) {
    .v2-modal { padding: var(--space-3); }
    .v2-modal-panel {
        width: 100%;               /* on a phone, filling the width IS the fit */
        min-width: 0;
        max-width: 100%;
        padding: var(--space-4);
    }
}

/* ── Head ────────────────────────────────────────────────────────────────── */

.v2-modal-head {
    display: flex;
    align-items: flex-start;
    gap: var(--space-4);
}

/* ── THE SCROLL FADE, THE SAME ONE THE PANELS HAVE ───────────────────────
   Daniel, 18-Sep: "could we add the fade on scroll thats persistent across v2?"

   A modal body is a scroll container exactly as a panel body is, and until now it
   was the only one in v2 where content met the head with a hard edge. The
   mechanism is .v2-panel-head::before/::after's, not an approximation of it —
   same two pseudo-elements, same 64px band, same 40%/100% scrim and 12px blur
   masked out at 55%, same 140ms fade-in. Copying the VALUES rather than the idea
   is the point: two fades that are nearly the same read as a mistake in whichever
   one you see second.

   THE SCRIM COLOUR IS RIGHT WITHOUT CHANGING: .v2-modal-panel is --surface-page,
   the same surface .v2-panel is, so content fades toward exactly what it slides
   under — which is what stops it banding.

   It reaches into the panel's 20px padding rather than the panel's 40px, because
   that is this container's own gutter.

   ON THE DIALOG, NOT THE PANEL ELEMENT: modals.js sets data-v2-scrolled on the
   <dialog> when the body moves, the same attribute overlays.js sets on a panel.

   ONLY WHILE SCROLLED, for the reason the panel note gives at length: a fade with
   nothing beneath it to hide is just a smudge across the top of the first field. */
.v2-modal-head {
    position: relative;
}

.v2-modal-head::before,
.v2-modal-head::after {
    content: '';
    position: absolute;
    top: calc(100% + var(--space-5));   /* clears the panel's own 20px row gap */
    left: calc(-1 * var(--space-5));
    right: calc(-1 * var(--space-5));
    height: 64px;
    pointer-events: none;
    opacity: 0;
    transition: opacity 140ms ease;
    z-index: 1;
}

.v2-modal[data-v2-scrolled] .v2-modal-head::before,
.v2-modal[data-v2-scrolled] .v2-modal-head::after { opacity: 1; }

.v2-modal-head::before {
    background: linear-gradient(
        to bottom,
        var(--surface-page) 0%,
        var(--surface-page) 40%,
        transparent 100%
    );
}

.v2-modal-head::after {
    -webkit-backdrop-filter: blur(12px);
            backdrop-filter: blur(12px);
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 55%);
            mask-image: linear-gradient(to bottom, #000 0%, transparent 55%);
}

@media (prefers-reduced-motion: reduce) {
    .v2-modal-head::before,
    .v2-modal-head::after { transition: none; }
}

/* No longer takes focus on open — see focusFirst() in modals.js. */
.v2-modal-title {
    flex: 1 1 auto;
    margin: 0;
    font-size: var(--type-h2-size);            /* 24px, as .v2-section-title */
    font-weight: 700;
    line-height: 1.2;
    /* THE PANEL'S OWN ACCENT. Justin, via Daniel 26-Aug: the modal title should
       match the title colour of the panel it belongs to.

       It costs nothing to do properly, because the mechanism already exists:
       the panel wrapper carries --heading-colour inline (§3's section accent),
       and a dialog inside that panel INHERITS it — the top layer changes where
       the dialog paints, not where it sits in the tree. So this was already
       reaching the modal and being overridden by the flat --brand-primary that
       used to be here.

       Declared rather than left to base.css's `:where(h1…h6)` default, which
       has zero specificity and would lose to anything. --brand-primary stays as
       the fallback for a modal outside any panel. */
    color: var(--heading-colour, var(--brand-primary));
}

/* WHERE FOCUS LANDS INSTEAD. The panel takes it, out of the tab order and with
   no ring: the dialog still needs focus inside it for the trap and for a screen
   reader to be in the right place, but a visible outline on the heading was
   reading as an error (Daniel, 26-Aug: "auto focus when modal opens on the title
   - get rid of"). Focus on the container keeps the behaviour and drops the
   decoration. */
.v2-modal-panel:focus { outline: none; }

.v2-modal-close {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-button);
    background: none;
    color: var(--text-body);
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
}

.v2-modal-close:hover { background-color: var(--surface-raised); }

/* ── Body ────────────────────────────────────────────────────────────────── */

.v2-modal-body {
    flex: 1 1 auto;
    min-height: 0;                 /* or the flex child refuses to shrink and the
                                      overflow never engages */
    overflow-y: auto;
    overscroll-behavior: contain;  /* reaching its end does not scroll the page */
    /* ── AND THE CONTENT DOES NOT MOVE WHEN THE BAR COMES AND GOES ─────────
       Daniel, 20-Sep: "on rest, the drop downs are slightly to the left, only
       when i open a drop down they shift slightly to the right to be truly
       central? why and i want them central throughout."

       Because opening a combo lifts this box's clip — it has to, or the list is
       cut off at the edge — and lifting it stops the box being a scroll
       container, which takes the scrollbar away and hands its width back to the
       content. Everything inside slid right by exactly one scrollbar.

       both-edges reserves the gutter on BOTH sides, so the content is centred
       whether the bar is there or not. The box is a few pixels narrower while
       it scrolls; it is never off-centre, which is the part that was visible.

       Where it is unsupported the behaviour is simply what it was before. */
    scrollbar-gutter: stable both-edges;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.v2-modal-lede {
    margin: 0;
    font-size: var(--type-body-size);
    line-height: 1.45;
    color: var(--text-body);
}

/* A card inside the modal. The inner step, as the frames draw it. */
.v2-modal-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4);
    border-radius: var(--radius-button);
    background-color: var(--surface-card);
}

.v2-modal-card-title {
    margin: 0;
    font-size: var(--type-body-size);
    font-weight: 700;
    line-height: 1.2;
    color: var(--text-strong);
}

/* Label over value, as .v2-record-cell does — at detail sizes. <dl> markup, so
   the pairing is in the document and not only in the layout. */
.v2-modal-facts {
    display: grid;
    gap: var(--space-3);
    margin: 0;
}

.v2-modal-facts > div {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.v2-modal-facts dt {
    font-size: var(--type-caption-size);
    font-weight: 500;
    line-height: 1.2;
    color: var(--text-card-label);
}

.v2-modal-facts dd {
    margin: 0;
    font-size: var(--type-body-size);
    font-weight: 500;
    line-height: 1.3;
    color: var(--text-body);
    overflow-wrap: break-word;      /* an email or a long address wraps rather
                                       than widening the modal */
}

/* ── Actions ─────────────────────────────────────────────────────────────── */

.v2-modal-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

/* Both actions share the row equally when there are two — "go to the dashboard"
   and "place an order" are peers, and sizing one to its label makes it look like
   the lesser choice. A single action still fills the row, which reads as the
   obvious next step rather than a button hiding in a corner. */
.v2-modal-actions > .v2-button {
    /* BASIS ZERO, which is what "equal share" actually needs. Daniel, 22-Sep:
       "make the buttons 50 50".

       The comment above this rule has always said the actions share the row
       equally, and the rule has never done it: flex-basis: auto sizes each
       button from its own label first and only then shares what is left, so
       "Save changes" came out wider than "Cancel" by the width of the extra
       word. A pair where one is visibly bigger reads as a recommendation, which
       is not what a Cancel and a Save are to each other.

       Basis 0 gives them the row to divide before either has an opinion about
       its text. min-width goes with it: a floor measured against the longest
       label was the old rule propping up the wrong mechanism, and it would now
       be the thing that breaks the split on a narrow dialog. */
    flex: 1 1 0;
    min-width: 0;
    /* Equal share of the row, but not unbounded — in a --wide modal a pair of
       two-word buttons stretched to 350px each and read as banners. */
    max-width: 260px;
}

/* A LONE ACTION FILLS THE ROW, which is what the comment above already claims
   it does — the 260px cap was added for a PAIR and quietly broke the single
   case: one button grew to 260px and sat left, stopping well short of the right
   edge. Daniel, 4-Sep, on the EPS modal: "the modal the close button falls
   short?" The cap is for sharing a row, so it does not apply when there is
   nothing to share with. */
.v2-modal-actions > .v2-button:only-child {
    max-width: none;
}

/* ── Spinner ─────────────────────────────────────────────────────────────────
   For a wait the user cannot shorten — currently the patient create, which
   inserts, calls Staff Tools, renders a PDF, uploads it and sends an email.

   currentColor, so it inherits whatever text it sits in and needs no palette of
   its own in either theme. The track is the same colour at low alpha rather
   than a second token, which keeps it correct on any surface it lands on.

   REDUCED MOTION GETS A PULSE, NOT A FREEZE. A spinner that stops spinning
   reads as "hung", which is the opposite of what it is for — so the animation
   changes rather than being removed. */
.v2-spinner {
    display: inline-block;
    width: 1em;
    height: 1em;
    margin-right: 0.5em;
    vertical-align: -0.125em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    opacity: 0.75;
    animation: v2-spin 0.7s linear infinite;
}

@keyframes v2-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
    .v2-spinner {
        animation: v2-spin-pulse 1.4s ease-in-out infinite;
        border-right-color: currentColor;
    }
    @keyframes v2-spin-pulse {
        0%, 100% { opacity: 0.25; }
        50%      { opacity: 0.85; }
    }
}

/* ── Progress steps ──────────────────────────────────────────────────────────
   The waiting state of the patient-create modal. Three lines that light up in
   turn and then resolve to ticks.

   THE PACING IS INDICATIVE, THE TICKS ARE FACTS. The create is one request, so
   the browser cannot know which step is running; validate.js advances the
   ACTIVE step on a timer and only ticks a step when the response says it
   happened. That distinction is why active and done look nothing alike. */
.v2-steps {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 12px);
}
.v2-step {
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: var(--type-body-size, 0.95rem);
    color: var(--text-muted, #94a3b8);
    /* Colour AND opacity, so a step that has not started reads as pending on a
       screen where the two greys are hard to tell apart. */
    opacity: 0.55;
    transition: opacity 0.25s ease, color 0.25s ease;
}
.v2-step.is-active,
.v2-step.is-done { opacity: 1; }
.v2-step.is-done { color: var(--text-body, #e2e8f0); }

/* The mark: three pulsing dots while active, a tick once done. Fixed width so
   the labels stay in a column whichever state each line is in. */
.v2-step-mark {
    flex: none;
    width: 26px;
    height: 18px;
    position: relative;
}
.v2-step-mark::before {
    /* The resting state: a single hollow dot. */
    content: '';
    position: absolute;
    left: 9px;
    top: 6px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    border: 1.5px solid currentColor;
    opacity: 0.6;
}

/* ACTIVE: three dots pulsing in sequence, built from the one element's
   pseudo-elements plus a box-shadow pair — no extra markup per step. */
.v2-step.is-active .v2-step-mark::before {
    left: 2px;
    border: none;
    background: currentColor;
    opacity: 1;
    box-shadow: 9px 0 0 currentColor, 18px 0 0 currentColor;
    animation: v2-dots 1.1s ease-in-out infinite;
}
@keyframes v2-dots {
    0%, 100% { box-shadow: 9px 0 0 rgba(148, 163, 184, 0.3), 18px 0 0 rgba(148, 163, 184, 0.3); }
    33%      { box-shadow: 9px 0 0 currentColor,              18px 0 0 rgba(148, 163, 184, 0.3); }
    66%      { box-shadow: 9px 0 0 currentColor,              18px 0 0 currentColor; }
}

/* DONE: a tick, drawn rather than a glyph so it inherits colour and needs no
   font. */
.v2-step.is-done .v2-step-mark::before {
    content: '';
    left: 6px;
    top: 2px;
    width: 5px;
    height: 10px;
    border: solid var(--intent-success-text, #5eae9c);
    border-width: 0 2px 2px 0;
    border-radius: 0;
    background: none;
    opacity: 1;
    transform: rotate(45deg);
    animation: none;
    box-shadow: none;
}

/* FAILED: the same slot, crossed. A step that did not happen must not be left
   looking like one still in progress. */
.v2-step.is-failed { opacity: 1; color: var(--intent-danger-text, #e05252); }
.v2-step.is-failed .v2-step-mark::before {
    content: '\00d7';
    left: 6px;
    top: -4px;
    width: auto;
    height: auto;
    border: none;
    border-radius: 0;
    background: none;
    box-shadow: none;
    animation: none;
    opacity: 1;
    font-size: 1.1rem;
    line-height: 1;
}

@media (prefers-reduced-motion: reduce) {
    /* No sequence, just a steady state — the step's colour already says which
       one is active. */
    .v2-step.is-active .v2-step-mark::before { animation: none; }
}

/* ── Success tick ────────────────────────────────────────────────────────────
   Draws itself once when the confirmation appears. Deliberately quick: it is a
   confirmation, not a celebration, and it sits above information the clinician
   is about to read. */
/* BESIDE THE HEADING, AND OUT OF THE FLOW. The panel is width:fit-content, so a
   tick laid out in the head row would widen every created modal by its own size
   — the "shouldnt flex the modal" Daniel asked for. Absolutely positioned
   against the head, it takes the line's spare height and contributes nothing to
   the measured width.

   padding-right on the head reserves the space instead, so a long heading wraps
   before it reaches the tick rather than running underneath it. */
.v2-modal-head--tick {
    position: relative;
    padding-right: 48px;
}
.v2-modal-tick {
    position: absolute;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
    display: flex;
    line-height: 0;
}
.v2-modal-tick[hidden] { display: none; }
.v2-modal-tick svg { width: 34px; height: 34px; }
.v2-modal-tick-ring,
.v2-modal-tick-mark {
    fill: none;
    stroke: var(--intent-success-text, #5eae9c);
    stroke-width: 3;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.v2-modal-tick-ring {
    stroke-dasharray: 145;
    stroke-dashoffset: 145;
    opacity: 0.45;
    animation: v2-tick-draw 0.45s ease-out forwards;
}
.v2-modal-tick-mark {
    stroke-dasharray: 40;
    stroke-dashoffset: 40;
    animation: v2-tick-draw 0.3s ease-out 0.3s forwards;
}
@keyframes v2-tick-draw { to { stroke-dashoffset: 0; } }

@media (prefers-reduced-motion: reduce) {
    /* Drawn already, rather than drawing. */
    .v2-modal-tick-ring,
    .v2-modal-tick-mark { animation: none; stroke-dashoffset: 0; }
}

/* ── Ghost button ────────────────────────────────────────────────────────────
   A real button that is quieter than a secondary one: no fill, no border until
   hovered. For an action worth offering and not worth pushing — currently the
   clinician's PDF under the created modal. */
.v2-button--ghost {
    background: transparent;
    border: 1px solid transparent;
    color: var(--brand-primary, #2c5aa0);
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
}
.v2-button--ghost:hover {
    border-color: var(--border-subtle, rgba(148, 163, 184, 0.35));
    background: var(--surface-raised, rgba(148, 163, 184, 0.08));
}
.v2-button-icon {
    display: inline-flex;
    width: 16px;
    height: 16px;
    flex: none;
}
.v2-button-icon svg { width: 100%; height: 100%; }

/* ── Roomy modal ─────────────────────────────────────────────────────────────
   A wider measure and more air inside, for a modal whose body is a card of
   facts rather than a sentence. Daniel, 1-Sep-2026: "the success modal is still
   cramped".

   Applied per modal rather than raised for all of them, for the reason the
   sizing comment above gives: a short confirmation should still be allowed to
   be small, and paying the widest case everywhere is what got that wrong before. */
.v2-modal--roomy .v2-modal-panel {
    --modal-max: 620px;
    padding: var(--space-6, 32px);
    gap: var(--space-5, 24px);
}
.v2-modal--roomy .v2-modal-card { padding: var(--space-5, 24px); }
.v2-modal--roomy .v2-modal-actions { gap: var(--space-4, 16px); }


/* ═══════════════════════════════════════════════════════════════════════════
   REQUEST PATIENT ORDER — the selection gate
   views/v2/panels/request-order.php + assets/v2/request-order.js

   Figma: RegisterPatient/Mobile/Dark 1492:13685, Selection 1525:14175.

   MOBILE FIRST, and that is not a habit here but a measurement: the mobile
   frame gives this panel 402x719, and Justin's brand grid alone was 367x311 of
   it — 43%. Every block below is sized against that budget, and the desktop
   rules widen rather than rebuild.
   ═══════════════════════════════════════════════════════════════════════════ */

.v2-ro {
    display: flex;
    flex-direction: column;
    gap: var(--space-4, 16px);
    /* THE BODY DOES NOT STRETCH THIS — the old comment above assumed it did, and
       that assumption is what left every column short of the floor.

       The measurement settled it: body 548, this 515, catalogue 515. A flex item
       is `flex: 0 1 auto` unless told otherwise, so this wrapper sat at its
       content height inside a body that had the room, and everything below
       inherited the shortfall. I had been fixing the links further down — the
       workspace's row, the body's display — while the one that was actually
       holding short was a div I had not noticed was in the chain at all.

       flex-grow: 1 is the whole fix. min-height: 0 stays for the reason the old
       comment gave: without it the stage below cannot shrink past its content. */
    flex: 1 1 auto;
    min-height: 0;
}

/* A dead end, said plainly. Not a card: there is nothing to act on, and giving
   it card chrome would suggest otherwise. */
.v2-ro-blocked {
    margin: 0;
    padding: var(--space-4, 16px);
    border: 1px dashed var(--border-medium);
    border-radius: var(--radius-card, 12px);
    color: var(--text-muted);
    font-size: 0.875rem;
    line-height: 1.5;
}

/* ── the subject, in the panel head ───────────────────────────────────────
   Beside the title, not above the rail (Daniel's mockup, 11-Sep). The head is
   a two-column grid — .v2-panel-head--control, title left and control right —
   so this only has to declare its own width and not stretch.

   Capped rather than fluid: it holds two short facts and a control, and a
   summary that grows with the window stops reading as a summary. */
/* TWO WIDTHS, because it is two different things. While it is a chooser it
   holds a select and wants room to show a name in — 22rem made it "fair narrow"
   (Daniel, 11-Sep). Once it resolves it is a summary of two short facts, and a
   summary that keeps the chooser's width reads as a box with a hole in it.

   Keyed with :has() off the state the JS already maintains, so there is no
   second flag to keep in step — the same trick the panel heads use to spot an
   empty aside. */
/* AN EXPLICIT WIDTH, NOT A MAX. .v2-panel-head--control's second column is
   `auto`, so it is sized by its content — and `width: 100%` inside a
   content-sized column is circular: the column is as wide as the content, so
   100% resolves to exactly that and a max-width never binds. That is why the
   first attempt at this changed nothing at all.

   max-width: 100% keeps it from pushing the title out on a narrow panel. */
/* ONE WIDTH. This went 22 (too narrow) → 30 (too wide) → 26 → 20, and the
   travel is the useful part: at 20rem the chooser and the resolved summary want
   the same room, so the two-width behaviour that solved "too narrow" has nothing
   left to solve. It is gone rather than left in at matching values — a :has()
   rule and a width transition that produce no visible change are just moving
   parts for the next person to explain to themselves.

   width, not max-width: .v2-panel-head--control's second column is `auto`, so a
   percentage resolves against the content and a cap never binds. */
.v2-ro-subject-wrap {
    width: 20rem;
    max-width: 100%;
    justify-self: end;
}

/* ── THE HEAD IS THREE COLUMNS HERE, AND THE RIGHT ONE IS THE BASKET'S ──────
   Daniel, 16-Sep: "we almost need the patient info to have as much room as the
   basket and have 2 clean columns i feel", and "The staff questions should be
   to the left of the patient info and in the same style as the patient info. in
   line so the header doesnt expand down at all."

   So: title, staff answers, patient card — three tracks, and the last one is
   --v2-ro-aside-w, the same width the basket column below it uses. The two
   right-hand edges line up down the whole panel, which is what makes it read as
   two columns rather than a head that happens to have a box in it.

   PLACED EXPLICITLY, not flowed. .v2-panel-head--control is grid-auto-flow:
   column, so with three tracks and two children a nurse's card would take the
   middle one and be sized by its content instead of the track. Naming the
   column is what makes the alignment hold whether or not the staff block
   exists. */
/* Desktop only, and the same breakpoint the workspace columns use — below it
   there is no basket column to line up with, and a 300px track on a phone is
   most of the screen. The head stacks there as it always did.

   TWO tracks, not three. The middle one held the answered staff questions; they
   are hidden once answered now, so a column for them would be an empty track
   and a gap either side of nothing. */
@media (min-width: 1280px) {
    /* The head's own two-track layout goes with the card that used to sit in
       its second track — see the aside note above. It is only ever the title
       now, and only while selecting. */

    .v2-panel[data-v2-panel="request-order"] .v2-panel-head .v2-ro-subject-wrap {
        width: auto;           /* the track is the width now, not the card */

        /* NAMED, NOT FLOWED, and hiding the title is what made that necessary.
           Daniel, 16-Sep: "the patient info is now madly in the wrong place
           should be in line above the basket."

           .v2-panel-head--control is grid-auto-flow: column, so children fill
           the tracks in order. With the title displayed the card was the second
           child and landed in the second track; with the title display: none it
           became the first child and took the 1fr track instead — which is most
           of the panel, so the card drifted left by the width of the catalogue.

           Saying which column it belongs in makes that independent of how many
           siblings happen to be rendered. */
        grid-column: 2;
    }
}

/* Nothing here dresses the staff block for the head any more — it never gets
   there. Asked in the body, then hidden; see the note in request-order.js. */

/* ── the pinned card ──────────────────────────────────────────────────────
   One box through all three states, so resolving a patient does not move the
   rail. Only its CONTENTS change. */
/* --surface-card, not --surface-raised. The spec is explicit that a card INSIDE
   a panel takes the NEUTRAL step and "deliberately not --surface-raised": Pebble
   100 in light, Flint 600 in dark, both read off LeadTimes and Resources rather
   than derived. --surface-raised is the panel's own token, and its dark value is
   marked [inferred] because no dark frame uses one — so the first version was
   off-family in light and guessing in dark. */
.v2-ro-subject {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 12px);
    padding: var(--space-4, 16px);
    background: var(--surface-card);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-card, 12px);
}

.v2-ro-field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
}

/* ── the patient option ───────────────────────────────────────────────────
   THE PICKER ITSELF ADDS NO CSS. It builds select.js's own DOM — .v2-combo,
   .v2-combo-button, .v2-combo-list, .v2-combo-search, .v2-combo-option — so the
   control, its open animation and its is-above flip all come from the existing
   component and the two selects on this card cannot drift apart.

   The only difference is that a patient row is TWO lines, because a name alone
   does not separate two Margaret Whitfields — so these three rules stack the
   row and nothing else. The hover/keyboard fill deliberately reuses
   .v2-combo-option.is-active's own colour rather than introducing a second
   idea of "where you are". */
.v2-ro-option {
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* 44px: this is chosen with a thumb far more often than a mouse. */
    min-height: 44px;
    justify-content: center;
}

.v2-ro-option:hover,
.v2-ro-option:focus {
    background-color: var(--colour-seafoam-200);
    outline: none;
}

.v2-ro-option-meta {
    font-size: 0.75rem;
    font-weight: 400;
    /* PINNED, like the name above it (.v2-combo-option, Flint 500): the list
       is white in both themes, and --text-muted is the DARK theme's pale grey
       there — Daniel, 2-Oct: "in dark mode, the nhs number DOB is too light to
       read against the white background". Flint 400 is ~6.4:1 on white and
       still a step quieter than the name. */
    color: var(--colour-flint-400);
    font-variant-numeric: tabular-nums;   /* NHS numbers line up while scanning */
}

/* ── the resolved card ───────────────────────────────────────────────────── */
/* A SUMMARY LINE, NOT A FORM. Justin's MyPatientsCard (1492:13730) is 367x91.6
   for the whole thing. The first version here was three loose rows at ~140px,
   spending the panel's best space restating facts the nurse had just chosen
   (Daniel: "the drop downs take up alot of room").

   Daniel's shape (11-Sep):
       Patient                    Clinic
       Delivery address: the full address

   Patient and clinic pair because both are short identifiers answering one
   question — who, and where they belong. The address is a sentence, so it takes
   the full width rather than wrapping inside half of one. */
.v2-ro-resolved {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
}

/* Facts left, action right. The action column is `auto`, so a row with no
   button simply has none — nothing has to be spanned or spacered. */
.v2-ro-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-4, 16px);
}

/* The short facts share the left cell and wrap as a group, so a long clinic
   name drops to its own line instead of squeezing the patient's. */
.v2-ro-facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px) var(--space-6, 32px);
    min-width: 0;
}

/* One fact: its icon, its value, and the control that changes it — or no
   control, where there is nothing to change.

   align-items: center, not baseline: an icon has no baseline to sit on, so
   baseline alignment hangs it off the text's and it reads as dropped. */
.v2-ro-item {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    min-width: 0;
}

.v2-ro-item-value {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--text-strong);
    font-weight: 500;
    overflow-wrap: anywhere;
}

/* ── the stage container ──────────────────────────────────────────────────
   The panel's floor. A panel that takes the column's height needs something to
   end on, or an unstarted order is a title, a select and several hundred pixels
   of nothing.

   A WELL, not another raised card. The subject card above it is a thing you act
   on; this is the space things go INTO, and inset surfaces read that way round.
   It also stops the two boxes competing — one raised card above one sunken well
   is a hierarchy, two raised cards is a list. */
/* NO SLACK. This carried flex: 1 1 auto — "takes the panel's slack, so the floor
   really is the floor" — which was written when the panel filled the column and
   the floor was a real thing to reach. The panel is compact during selection
   now, so there is no slack to take and asking for it only inflates this box:
   two selects in a 275px container with a hundred empty pixels under them, which
   is the "why sucgh a big panel for the selection" complaint surviving in a
   smaller box.

   Sized by its contents, like the panel around it. */
.v2-ro-stages {
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* NO BOX ROUND THE CARD (Daniel, 13-Oct: "there isnt a need for a
       container in a container at this screen? let the field sit on the dark
       background in dark mode, and the shaded one in light"). The selection
       card sits on the panel itself and takes this box's card fill instead
       (.v2-ro-selection .v2-ro-subject). */
}

.v2-ro-empty {
    flex: 1 1 auto;
    display: grid;
    place-items: center;
    /* Only on a tall panel. On a phone the window scrolls and a forced empty
       region would be a screenful of nothing to scroll past. */
    min-height: 0;
}

.v2-ro-empty-line {
    margin: 0;
    padding: var(--space-6, 32px) var(--space-4, 16px);
    color: var(--text-muted);
    text-align: center;
    text-wrap: balance;
}

/* Centred while a stage holds only its placeholder line. Real stage content
   will set its own alignment — grid, list, whatever it needs — so this is the
   resting state, not a constraint on what goes in. */
.v2-ro-stage-body {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    place-items: center;
}

/* ── the rail ─────────────────────────────────────────────────────────────
   Three stages, horizontal at every width. It stays visible while inert so the
   shape of the job is legible before a patient exists — but it cannot be
   operated, and it says so in both directions: dimmed, and pointer-events off
   so a tap does nothing rather than doing something confusing. */
.v2-ro-rail {
    display: flex;
    gap: var(--space-2, 8px);
    margin: 0;
    padding: 0;
    list-style: none;
}

.v2-ro-rail[data-v2-ro-inert] {
    opacity: .45;
    pointer-events: none;
}

.v2-ro-stage {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1, 4px);
    padding: var(--space-2, 8px) var(--space-1, 4px);
    border-radius: var(--radius-button, 8px);
    text-align: center;
}

/* --surface-sunken, not --surface-inset. --surface-inset is Flint 500 in dark,
   which is EXACTLY --surface-page — so an unreached dot was invisible on the
   dark panel, which is visible in Daniel's own screenshot: 2 and 3 read as bare
   numerals there and as circles in light. --surface-sunken is one step down from
   the page in BOTH themes, which is what a not-yet-reached marker wants. */
.v2-ro-stage-dot {
    display: grid;
    place-items: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    background: var(--surface-sunken);
    color: var(--text-muted);
    font-size: 0.75rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.v2-ro-stage-label {
    font-size: 0.75rem;
    color: var(--text-muted);
    line-height: 1.2;
}

.v2-ro-stage.is-current .v2-ro-stage-dot {
    background: var(--accent-request-order);
    color: var(--text-on-brand);
}

.v2-ro-stage.is-current .v2-ro-stage-label {
    color: var(--text-strong);
    font-weight: 600;
}

.v2-ro-stage.is-done .v2-ro-stage-dot {
    background: var(--colour-meadow);
    color: var(--text-on-accent);
}

/* ── desktop and tablet ───────────────────────────────────────────────────
   The card has room for two columns above 700px, which shortens it by a row
   and brings the rail into the first screenful. The suggestion list stays full
   width, because a name plus NHS number in half a column is a truncation
   waiting to happen. */
@media (min-width: 700px) {
    /* NOT A TWO-COLUMN GRID. The card holds one field at a time until a patient
       is chosen, so a two-column card spent half its width on nothing — the
       first version did exactly that and it read as a broken layout rather than
       a roomy one. The card stays a single column and the CONTROLS are capped
       instead, which is also what the frames do: a select is 366-440px wide
       there, never the full panel. */
    .v2-ro-field {
        max-width: 27rem;
    }

    /* The staff pair is the one place two columns are filled, because both
       answers are always present together. */
    .v2-ro-staff {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--space-4, 16px);
    }

    .v2-ro-staff .v2-ro-field { max-width: none; }

    .v2-ro-stage { flex-direction: row; justify-content: center; gap: var(--space-2, 8px); }
    .v2-ro-stage-label { font-size: 0.8125rem; }
}

/* Reduced motion has nothing to disable here yet — the gate does not animate.
   Stated so the next person adding a transition knows to add the guard too. */

/* ── the subject at head width ────────────────────────────────────────────
   Narrower than the body it used to sit in, so three things change: the facts
   stack instead of pairing, the address truncates to one line with the full
   value on its title attribute, and the field cap goes — 27rem inside a 22rem
   box is not a cap, it is a scrollbar. */
.v2-ro-subject-wrap .v2-ro-field { max-width: none; }

.v2-ro-subject-wrap .v2-ro-facts {
    flex-direction: column;
    gap: var(--space-1, 4px);
}

/* One line, and the whole address is in the title attribute — see the fetch in
   request-order.js. min-width: 0 is what lets a flex child shrink enough to
   ellipsise at all; without it the text sets the row's width and nothing ever
   truncates. */
.v2-ro-subject-wrap .v2-ro-item-value {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow-wrap: normal;
    min-width: 0;
}

/* Below the head's own breakpoint the control drops under the title and can
   have the width back, so the address stops truncating. */
@media (max-width: 1279px) {
    /* width: auto, not just max-width: none — the base rule sets an explicit
       width, so releasing only the cap would leave the card pinned at 20rem on a
       phone. Below this breakpoint the head is a single column and the card
       takes it. */
    .v2-ro-subject-wrap {
        width: auto;
        max-width: none;
        justify-self: stretch;
    }

    .v2-ro-subject-wrap .v2-ro-item-value {
        overflow: visible;
        text-overflow: clip;
        white-space: normal;
        overflow-wrap: anywhere;
    }
}

/* The icon that replaced the word. Muted like the label was, so the value still
   carries the emphasis — an icon at full strength competes with the name beside
   it for no reason. */
.v2-ro-item-icon {
    flex: none;
    display: inline-flex;
    color: var(--text-muted);
}

.v2-ro-item-icon svg { display: block; }

/* The NHS number, to the right of the name behind a rule in the TITLE's colour.
   --heading-colour is what .v2-panel-title uses, set on the panel wrapper from
   the overlay's accent — so this tracks the title rather than restating a blue,
   and a panel that changes accent takes this with it. */
/* CLIPPED AT THE COLUMN EDGE. Daniel, 16-Sep: "the name | NHS number slightly
   overlaps the icon?" The row is a grid of facts and actions, but nothing was
   stopping the facts PAINTING past their own track — the name ellipsises, then
   the NHS number, which never shrinks, carried on into the gap and under the
   Change button. A grid track constrains layout, not overflow. */
.v2-ro-subject-wrap .v2-ro-item { overflow: hidden; }

.v2-ro-item-nhs {
    /* IT HAS TO BE ABLE TO SHRINK. flex: none was the overlap: clipping the row
       stopped it painting past the card, but a piece that refuses to shrink
       still LAYS OUT past it, so the number sat under the Change button and the
       clip merely cut it in half. Shrinkable and ellipsised, it gives way like
       the name does and the two share what room there is. */
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    padding-left: var(--space-2, 8px);
    border-left: 2px solid var(--heading-colour, var(--accent-request-order));
    color: var(--text-body);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* The identifier never gives way; the name does. Half a name is still a name —
   half an NHS number is a different patient. */
.v2-ro-subject-wrap .v2-ro-item-value { flex: 0 1 auto; }

/* No number recorded. Muted and un-tabular: it is words now, not digits to
   compare, and at full strength an absence would read as loudly as a value. */
.v2-ro-item-nhs.is-unknown {
    color: var(--text-muted);
    font-variant-numeric: normal;
    font-style: italic;
}

/* ═══════════════════════════════════════════════════════════════════════════
   REQUEST PATIENT ORDER — the workspace
   Catalogue left, basket right; on a phone the basket becomes a drawer over
   the catalogue, which is classic's own arrangement (#mobile-basket-drawer).
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── TWO COLUMNS ON DESKTOP, STACKED BELOW IT ───────────────────────────────
   Justin's desktop frames put the right column at x=1030..1280 of a 1200 panel
   — 250px, against 650px of catalogue. Wide mode is what makes that affordable:
   at 930px a permanent column took a fifth of the workspace, which is why the
   basket became a modal in the first place; at 1400px it costs a sixth of a much
   bigger number.

   align-items: start, not stretch. The columns are different lengths by nature —
   a catalogue that grows with its results against two fixed cards — and
   stretching the short one paints a 600px empty card under Order details. */
.v2-ro-workspace {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    /* start as the default; each column opts in to stretching for its own
       reasons. */
    align-items: start;
    gap: var(--space-4, 16px);

    /* ── THE ROW IS BOUNDED, AND THAT IS WHAT DETACHES THE TWO COLUMNS ──────
       Daniel, 16-Sep: "the basket is spanning thw container length of the
       catalogue. it shouldnt. they should be detached in behaviour."

       They were never coupled by anything either column does — they were coupled
       by the GRID. With no explicit row the track is sized by its tallest
       content, so four hundred products made the row four hundred products tall
       and the stretched aside faithfully matched it. The basket was not
       following the catalogue; both were following the row.

       minmax(0, 1fr) ties the row to the workspace's own height instead, and
       min-height: 0 is what lets the workspace be shorter than its contents at
       all. Between them the catalogue gets a definite height — which is also
       what makes its products scroll rather than run on down the page — and the
       aside gets the panel's height rather than the catalogue's. */
    grid-template-rows: minmax(0, 1fr);
    min-height: 0;
    /* min-WIDTH too. The brand strip scrolls sideways, and a flex child refuses
       to shrink below its content width without this — so ~40 tiles pushed the
       catalogue wide, which pushed the panel off the screen. overflow-x on the
       strip cannot help while an ancestor is sized by that same content. */
    min-width: 0;
}

/* ONE NUMBER, TWO PLACES. The head's patient card and this column have to be
   the same width or the panel does not read as two columns, and a number written
   down twice is a number that drifts. */
/* 360, up from 300. Daniel, 17-Sep: "we need to get more real estate for the
   basket i feel… which would require slightly more width on that right panel
   column."

   ONE variable moves both columns: the panel head's patient card measures from
   this too, so the basket and the patient info above it stay on the same edge —
   which is the alignment that took three attempts to get right on 16-Sep and is
   not being broken to win 60px. The width comes out of the catalogue's 1fr,
   which has it to give. */
.v2-panel[data-v2-panel="request-order"] { --v2-ro-aside-w: 360px; }

/* ── THREE COLUMNS: FILTERS, WORK, ORDER ───────────────────────────────────
   Justin's desktop frame puts the rail at 164px against 650 of catalogue and 250
   of basket. 200px here because wide mode gives the row 1400px rather than the
   frame's 1200, and a rail that stays at the frame's width while everything
   around it grows starts to read as a leftover rather than a column. */
/* ── THE RAIL AND THE PAGE GROW IN LOCKSTEP ────────────────────────────────
   Daniel, 16-Sep: "the transition is wrong as it momentarily eats into the
   catologue then lands back to normal affecting the image sizing… almost like
   after clicking the filter you see the room grow out from the container to the
   left and the panel shift right slightly. leaving the catalogue basket etc
   untouched in scale."

   The logic was right and the timing was not. The rail's column appeared
   INSTANTLY while the shell's extra width arrived over 700ms, so for that
   700ms the 200px came out of the catalogue and then sprang back — and the
   brand tiles, sized by their column, resized twice on the way.

   ALWAYS THREE TRACKS, the first of them 0 when closed. Same count in both
   states is what makes grid-template-columns interpolate at all, and with the
   rail growing at exactly the rate the page does, the 1fr between them never
   changes width. The catalogue and the basket are untouched in scale because
   nothing is ever taking space from them — the room genuinely arrives from
   outside.

   Same duration and easing as the shell's, quite deliberately: two animations
   that are meant to cancel each other out have to be the same animation. */
@media (min-width: 1280px) {
    .v2-ro-workspace {
        grid-template-columns: 0 minmax(0, 1fr) var(--v2-ro-aside-w);
        transition: grid-template-columns .7s cubic-bezier(.4, 0, .2, 1);
    }

    .v2-panel[data-v2-ro-filters-open] .v2-ro-workspace {
        grid-template-columns: var(--v2-ro-rail-w) minmax(0, 1fr) var(--v2-ro-aside-w);
    }

    /* THE CATALOGUE TAKES THE CLOSED RAIL'S GAP (Daniel, 13-Oct: "the left
       side of the panel is still too much padding when the filter is closed
       ... there is a space being taken by the non open filter bar"). The
       rail's negative margin below never could: a margin on one grid item does
       not move the next, whose place comes from the tracks, so the 0 track
       still cost its 16px gap and the catalogue sat 56px from the panel's edge
       against the basket's 40. The catalogue stretches 16px left over it while
       the rail is shut and gives it back on the rail's own curve.

       THE SELECTOR OUT-RANKS the work stage's margin-bottom rule further down,
       which sets this box's transition too and, coming later at a lower
       specificity than it needed, quietly replaced this one: the margin snapped
       while the columns took 0.7s, and the catalogue jolted 16px (Daniel,
       13-Oct: "a slight jolt when activating the filter"). So both transitions
       are declared here together. */
    .v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] .v2-ro-workspace > .v2-ro-catalogue {
        margin-left: calc(-1 * var(--space-4, 16px));
        transition: margin-bottom .18s ease,
                    margin-left .7s cubic-bezier(.4, 0, .2, 1);
    }

    .v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"][data-v2-ro-filters-open] .v2-ro-workspace > .v2-ro-catalogue { margin-left: 0; }
}

/* The gap the zero-width track would otherwise leave behind. A grid's
   column-gap applies to every boundary, so a 0 track still costs 16px on the
   left; the rail cancels it with its own negative margin and releases it on the
   same curve. Without this the catalogue sits 16px in from where it should when
   the rail is shut. */
.v2-ro-filters {
    margin-right: calc(-1 * var(--space-4, 16px));
    visibility: hidden;
    opacity: 0;
    transition: margin-right .7s cubic-bezier(.4, 0, .2, 1),
                opacity .3s ease,
                visibility 0s linear .7s;
}

.v2-panel[data-v2-ro-filters-open] .v2-ro-filters {
    margin-right: 0;
    visibility: visible;
    opacity: 1;
    transition: margin-right .7s cubic-bezier(.4, 0, .2, 1),
                opacity .3s ease .25s,
                visibility 0s;
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-workspace,
    .v2-ro-filters,
    .v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] .v2-ro-workspace > .v2-ro-catalogue { transition: none; }
}

/* One number again, used by the workspace column and by the shell's extra
   width. They have to agree exactly or opening the rail takes a slice off the
   catalogue, which is the whole thing this is avoiding. */
.v2-panel[data-v2-panel="request-order"] { --v2-ro-rail-w: 200px; }

/* ── THE STEP'S BACK ROW ─────────────────────────────────────────────────────
   Declared BEFORE the media query that turns it on, and that ordering is the
   whole point. A later rule beats a media query of equal specificity whatever
   the query says, so the base `display: none` written after the query would
   have won at every width and the Back button would never have appeared — the
   trap this stylesheet has already paid for twice.

   Off on desktop because the rail is a column beside the grid there and needs
   no way out of itself. */
.v2-ro-filters-head {
    display: none;
    align-items: center;
    gap: var(--space-3, 12px);
    margin-bottom: var(--space-4, 16px);
}

.v2-ro-filters-title {
    margin: 0;
    font-size: 1.375rem;
    font-weight: 700;
    color: var(--text-strong);
}

/* The pill from the Figma mobile frames: a chevron and the word, on a quiet
   outline. 44px tall because that is this shell's touch floor below 1024 and
   this is the control a nurse reaches for most on these screens — getting back
   out. */
/* SMALLER TO LOOK AT, NOT TO TAP (Daniel, 2-Oct: "make the back smaller on in
   design"). It was 44px tall to the eye, the heaviest thing in either step's
   head; it is 32 now, with the 44px floor kept by the ::after below — the same
   way every small control on these screens keeps it. */
.v2-ro-step-back {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 4px);
    min-height: 32px;
    padding: 0 var(--space-3, 12px) 0 var(--space-2, 8px);
    border: 1px solid var(--border-subtle, var(--border));
    border-radius: 999px;
    background: transparent;
    color: var(--text-strong);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 500;
    cursor: pointer;
}

.v2-ro-step-back svg { width: 16px; height: 16px; flex: none; }

.v2-ro-step-back::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 44px;
    transform: translateY(-50%);
}

.v2-ro-step-back:hover { background: var(--surface-sunken); }

.v2-ro-step-back:focus-visible {
    outline: 2px solid var(--focus-ring, var(--colour-harbour-500));
    outline-offset: 2px;
}

/* ── THE RAIL'S BASE RULE SITS HERE, ABOVE THE STEP THAT OVERRIDES IT ──────
   It used to come after the media query below, at equal specificity, so it won
   at every width: the step lost its safe-area padding, its own scroll and its
   square corners, and picked up the card fill (2-Oct review). The same trap as
   .v2-ro-filters-head above — base first, then the query that changes it. */
.v2-ro-filters {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 12px);
    min-width: 0;
    min-height: 0;
    padding: var(--space-3, 12px);
    background: var(--surface-card);
    border-radius: var(--radius-card);
    /* The groups scroll, not the rail: the search and Clear stay reachable
       however many manufacturers a brand turns out to have. */
    overflow: hidden;

    /* ── AND IT HAS TO BE BOUNDED FOR THAT TO MEAN ANYTHING ────────────────
       Daniel, 18-Sep: "filter needs a scroll? as spans off page and i cant get
       to it".

       The groups have had overflow-y: auto all along; what they did not have is
       a height to overflow. .v2-ro-workspace sets align-items: start and its own
       comment says "each column opts in to stretching for its own reasons" —
       this column never opted in, so it sized to its content and ran off the
       bottom of the page with its scroll region taller than the screen.

       With the full manufacturer list in it that is ~40 rows rather than the
       handful of facets it used to hold, which is why it only started biting
       now. */
    align-self: stretch;
    max-height: 100%;
}

/* ── REQUEST ORDER STEPS BELOW 1280, NOT 1025 (Daniel, 5-Oct) ───────────────
   "a squashing issue on new order when the mobile view isnt triggered yet ...
   especially when the filter is locked open. i guess we need to trigger tablet
   view sooner on the ordering page?" Between 1025 and 1279 the side nav, the
   200px rail and the 360px basket column left the catalogue ~120px. So THIS
   PANEL'S layout blocks (workspace columns, rail and basket as steps, brand
   strip, chips, the round basket, touch targets) switch at 1279/1280, while the
   shell's own (side nav, panel heads, section grid, the page-length panel)
   stay at 1024. request-order*.js ask the same 1279. Where a note below says
   1025 for this panel, read 1280. */

/* ── BELOW 1025 THE RAIL IS A STEP, NOT A RAIL ───────────────────────────────
   It used to be hidden here, along with its button, because 200px of a phone is
   half the screen and there was nowhere to widen into. The note said the mobile
   frame was a full-screen sheet and its own piece of work. This is that work.

   Daniel, 22-Sep: "i think filter should be drawer left and basket drawer right
   with step through logic? youll not need filter when in basket vice versa."
   And the Figma mobile frames agree — Filters and Basket are each a full screen
   with a Back button, not an overlay you peer around.

   SO IT ARRIVES FROM THE LEFT AND COVERS EVERYTHING. Full width rather than a
   partial drawer, because at 402px a partial drawer leaves a strip of catalogue
   too narrow to read and too easy to tap by mistake. The direction is what makes
   it feel like a drawer; the coverage is what makes it a step.

   The desktop rail animates on margin-right, which is a width change. That is
   exactly wrong here — a width transition on a fixed full-screen layer reflows
   its contents for the whole animation. Transform instead: it moves the painted
   layer and touches no layout. */
/* ── OVER THE PANEL, NOT OVER THE SCREEN (Daniel, 2-Oct) ─────────────────────
   "Filter opens full page when panel isnt full screen, should filter only open
   over the panel for consistency?" Yes: below 1025 the panel is a card on the
   wallpaper, and a step that covered the wallpaper and the top row as well read
   as a different screen rather than a layer of this one.

   So both steps, and the scrim, take the PANEL'S VISIBLE BOX: the part of the
   panel on screen, worked out once as a step opens (request-order-catalogue.js,
   measureStepBox) and handed over as --v2-ro-box-*. The page is locked while a
   step is open, so the box cannot move under it; a resize or a rotation
   measures again. A corner is rounded only where the panel's own corner is on
   screen — scrolled down, the panel's top is off the top of the screen and so
   is the step's.

   The defaults are the whole screen, which is what a step was before, so a
   step that opens before anything has measured is still a working step. */
@media (max-width: 1279px) {
    .v2-ro-filters {
        position: fixed;
        top: var(--v2-ro-box-top, 0px);
        bottom: var(--v2-ro-box-bottom, 0px);
        left: var(--v2-ro-box-left, 0px);
        right: auto;
        width: var(--v2-ro-box-w, 100%);
        max-width: var(--v2-ro-box-w, 100%);
        z-index: 71;
        box-sizing: border-box;
        padding: var(--space-4, 16px);
        /* Its height is the box's (top and bottom above), which is already
           inside the visible viewport — the dvh reasoning, done by measuring. */
        height: auto;
        max-height: none;
        /* The safe area, so the last checkbox is not under the home indicator. */
        padding-bottom: calc(var(--space-4, 16px) + env(safe-area-inset-bottom, 0px));
        background: var(--surface-page);
        border-radius: var(--v2-ro-box-rt, 0px) var(--v2-ro-box-rt, 0px)
                       var(--v2-ro-box-rb, 0px) var(--v2-ro-box-rb, 0px);
        overflow-y: auto;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;

        /* The desktop rule's margin/opacity dance is stood down entirely — two
           transitions racing on one element is how you get a rail that fades in
           before it has finished arriving. */
        /* IN FROM THE LEFT, A SHORT WAY, as it fades. It used to slide the
           whole width in from off screen; now that it sits inside the panel a
           full slide would cross the wallpaper beside it on the way. A short
           travel keeps the direction — filters come from the left, the order
           from the right — and it is transparent while it is outside its box. */
        margin-right: 0;
        opacity: 0;
        transform: translateX(-24px);
        visibility: hidden;
        transition: transform .28s cubic-bezier(.4, 0, .2, 1),
                    opacity .2s ease,
                    visibility 0s linear .28s;
    }

    .v2-panel[data-v2-ro-filters-open] .v2-ro-filters {
        margin-right: 0;
        opacity: 1;
        transform: none;
        visibility: visible;
        transition: transform .28s cubic-bezier(.4, 0, .2, 1),
                    opacity .2s ease,
                    visibility 0s;
    }

    /* 101%, not 100%: a box shadow drawn on the leading edge still shows at
       exactly 100%, as a dark seam down the side of the catalogue. */

    /* The Back row only exists in step mode. On desktop the rail has no head of
       its own — it is a column beside the grid and needs no way out. */
    .v2-ro-filters-head { display: flex; }
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-filters { transition: none; }
}


.v2-ro-filter-groups {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);

    /* ── THE BAR SITS IN THE PANEL'S PADDING, NOT IN THE LIST ──────────────
       Daniel, 18-Sep: "wrong scroll bar here too on filter, and it shouldnt eat
       into the filter."

       Both halves of that. The styling comes from .v2-scroll in the markup —
       without it the platform draws its own, which is the grey slab in the
       screenshot. And a scrollbar takes its width from the CONTENT box, which
       is exactly the note already written down about scrollbar-gutter: so the
       rows were losing ~12px to it and the manufacturer names were clipping.

       Pulling the region out into the rail's own padding and putting the same
       amount back as padding-right gives the bar somewhere to live that was
       never the list's to begin with. */
    margin-right: calc(-1 * var(--space-3, 12px));
    padding-right: var(--space-3, 12px);
}

/* <details>, so the group opens and closes with no JS and keeps its state in
   the element rather than in a variable this file would have to maintain. */
.v2-ro-filter-group {
    border-bottom: 1px solid var(--border-subtle);
    padding-bottom: var(--space-2, 8px);
}

.v2-ro-filter-group:last-child { border-bottom: 0; }

.v2-ro-filter-summary {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: var(--space-1, 4px) 0;
    cursor: pointer;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-strong);
    list-style: none;
}

.v2-ro-filter-summary::-webkit-details-marker { display: none; }

/* A CATEGORY heading, as opposed to a facet heading: this is the level a nurse
   navigates by, so it takes the accent and the rows beneath it stay plain. */
.v2-ro-filter-group--cat > .v2-ro-filter-summary {
    color: var(--accent-request-order);
}

/* The frame's count chip, and it counts VALUES rather than products — it says
   how many things are inside the group you are about to open. */
.v2-ro-filter-count {
    margin-left: auto;
    min-width: 1.25rem;
    padding: 0 var(--space-1, 4px);
    border-radius: var(--radius-pill, 999px);
    background: var(--surface-card);
    color: var(--text-muted);
    font-size: 0.6875rem;
    font-weight: 700;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.v2-ro-filter-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: var(--space-1, 4px) 0;
    cursor: pointer;
    font-size: 0.8125rem;
    color: var(--text-body);
    min-width: 0;
}

.v2-ro-filter-row:hover { color: var(--text-strong); }

/* The value can be a long manufacturer name in a 200px rail; the count must not
   be pushed out of sight by it. */
.v2-ro-filter-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.v2-ro-filter-n {
    flex: none;
    color: var(--text-muted);
    font-size: 0.6875rem;
    font-variant-numeric: tabular-nums;
}


/* ── KEEP FILTERS OPEN ─────────────────────────────────────────────────────
   Daniel, 20-Sep: "should we allow clinicians to stick that open if they wish?
   i feel some may want it to allways show is all."

   Quiet by default and quiet when on. This decides how the rail BEHAVES rather
   than what the catalogue shows, so it must not compete with the facets it sits
   above — pressed, it takes the accent as text and rim only, not as a fill. A
   filled pill here would read as a filter that is doing something to the
   results, which is the one thing it does not do. */
.v2-ro-filter-pin {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    width: 100%;
    margin-bottom: var(--space-2, 8px);
    padding: 6px 8px;
    border: 1px solid transparent;
    border-radius: var(--radius-button, 8px);
    background: none;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.75rem;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}

.v2-ro-filter-pin:hover { color: var(--text-strong); }

.v2-ro-filter-pin[aria-pressed="true"] {
    border-color: var(--accent-request-order);
    color: var(--accent-request-order);
}

.v2-ro-filter-pin:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}
.v2-ro-filter-clear {
    flex: none;
    padding: var(--space-2, 8px);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-button, 8px);
    background: none;
    color: var(--text-body);
    font-size: 0.8125rem;
    cursor: pointer;
}

.v2-ro-filter-clear:hover { border-color: var(--border-medium); color: var(--text-strong); }

/* ── the toggle ────────────────────────────────────────────────────────────
   Beside the search rather than above it: the two are the same job — narrowing
   what the grid shows — and separating them would suggest otherwise. */
.v2-ro-search-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: var(--space-4, 16px);
}

.v2-ro-search-row .v2-ro-search { flex: 1 1 auto; padding: 0; }

.v2-ro-filter-toggle {
    position: relative;
    flex: none;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card);
    color: var(--text-body);
    cursor: pointer;
}

.v2-ro-filter-toggle:hover { border-color: var(--border-medium); color: var(--text-strong); }

.v2-ro-filter-toggle[aria-expanded="true"] {
    border-color: var(--accent-request-order);
    color: var(--accent-request-order);
}

.v2-ro-filter-toggle:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

/* The mark that says the grid is narrowed even with the rail shut. */
.v2-ro-filter-dot {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 7px;
    height: 7px;
    border-radius: var(--radius-pill, 999px);
    background: var(--accent-request-order);
}

/* ── THE CATALOGUE IS A NEUTRAL CARD, NOT A SEAFOAM WELL ───────────────────
   Daniel, 16-Sep: "the products panel is using a green background but look at
   the SPEC and its more a pinkie colour?"

   --surface-sunken is Seafoam 300, and Seafoam is the green: #eff7f5. The
   Product/Light frame's variables are Pebble 100 (#eeeeee) and White, with no
   Seafoam well anywhere in the product area.

   The token file already said so, in the note above --surface-card: "Cards
   INSIDE a panel. Deliberately not --surface-raised or --surface-sunken: the
   screens use a NEUTRAL step here, not a Seafoam one… On a Seafoam page a
   neutral card reads as a distinct material rather than as the same surface at
   a different height, which is the intent." This panel's containers were the
   one place not following it.

   Dark is unchanged either way — --surface-card and --surface-sunken are both
   Flint 600 there — which is exactly why this only ever showed up in light. */
.v2-ro-catalogue {
    display: flex;
    flex-direction: column;
    gap: var(--space-4, 16px);
    flex: 1 1 auto;
    min-height: 0;
    min-width: 0;              /* same reason as the workspace above */
    padding: var(--space-4, 16px);
    background: var(--surface-card);
    border-radius: var(--radius-card, 12px);
}

.v2-ro-section-title {
    margin: 0 0 var(--space-2, 8px);
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-strong);
}

/* ── THE BREADCRUMB AND THE ON-PAGE FILTER SHARE A LINE ────────────────────
   Daniel, 20-Sep: "under the brand images on the right side in that space?"

   That space was empty beside the breadcrumb, and it is the right home: the
   filter acts on the results this line names. Wrapping rather than shrinking,
   because a breadcrumb that has to compete with a search box for width loses
   the range name — which is the one thing on that line a nurse is reading. */
.v2-ro-results-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3, 12px);
    flex-wrap: wrap;
}

.v2-ro-results-head > .v2-ro-section-title { flex: 1 1 auto; min-width: 0; }

/* Positioned for the icon inside it, and capped: this narrows what is already
   on screen, so it never needs the room the bar at the top has. */
.v2-ro-onpage {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    flex: 0 1 auto;
}

.v2-ro-onpage[hidden] { display: none; }

/* Click-through, and a fixed dark value rather than --text-muted: .v2-field is
   pinned white in both themes, so the theme's muted would go pale on it. The
   same reasoning as the address lookup's magnifier. */
.v2-ro-onpage-icon {
    position: absolute;
    top: 50%;
    left: 10px;
    transform: translateY(-50%);
    color: #6B7B82;
    pointer-events: none;
}

.v2-ro-onpage-field {
    width: 15rem;
    max-width: 100%;
    height: 34px;
    padding-left: 30px;
}

/* The phone's way in to the field above (_request-order-catalogue.php). The
   field is always there from 1025 up, so this is too. Opened and closed in the
   narrow block at the end of this file. */
.v2-ro-onpage-open { display: none; }

/* What the filter did, beside it rather than under it — a count that moves the
   grid down every time a letter is typed is worse than no count. */
.v2-ro-onpage-count {
    font-size: 0.75rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}


/* ── the product grid ───────────────────────────────────────────────────── */
/* min-width: 0 on BOTH, and it is why the cards ran past the card they live in
   (Daniel: "the product results span past the product container lol"). The
   results region is a flex item, and a flex item will not shrink below its
   content's minimum unless told to — so auto-fill's 13rem floor set the width
   and the catalogue was sized by its contents instead of the other way round.

   Same failure as the brand strip bursting the panel earlier. Any grid inside a
   flex column needs this; it is not a property of THIS grid. */
.v2-ro-results {
    min-width: 0;
}

/* ── ONE TRACK SIZE FOR BOTH STEPS ─────────────────────────────────────────
   Daniel, 17-Sep: "i was also thinking we should actually make the product
   cards match range and be columns of 3 instead of 4 by default to allow better
   product name spanning".

   Right on both halves. Ranges were 15rem and products 13rem, so stepping from
   one to the other re-flowed the whole grid — three columns became four — which
   is the reflow that had to be worked around when entering a range. Matching
   them removes the cause rather than hiding it.

   And 15rem is what buys the names their second line: "6D Tape 5cm x 24cm (Pack
   of 4)" wrapped to three at 13rem and had to be clamped; at 15rem it fits two.
   Fewer, wider cards is the right trade when the thing being read is a long
   product name. */
.v2-ro-product-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--space-3, 12px);
    min-width: 0;
}

/* THE CARD NEEDS AN EDGE, NOT A FILL. --surface-card and the catalogue's own
   --surface-sunken are BOTH Flint 600 in dark, so a filled card was painted the
   exact colour of the surface behind it and vanished — Daniel: "product cards
   are wrong have no container?". Same mistake as the icon buttons, same fix: a
   border works on any surface without the rule having to know which one it
   landed on. */
/* ── THE TILES ARE LIFTED OFF THE CARD, NOT RECESSED INTO IT ───────────────
   Daniel, 16-Sep: "look at the product cards themselves, wrong style and colour
   both in light and dark mode."

   Read off both frames rather than reasoned from the light one:
   Product/Light 1535:16788 has White tiles on the Pebble 100 container;
   Product/Dark 1535:15626 has colour/flint (#3a4a50, Flint 500) on Flint 600.
   Lighter than the card in BOTH — which is the opposite of what I had.

   I had reached for --surface-inset, "a cell recessed into a card", because the
   container had just gone neutral and inset is the neutral step. It is the
   right family and the wrong direction: Pebble 200 is darker than Pebble 100,
   so in light the tiles sank into the container instead of standing off it. It
   only looked plausible in dark, where inset happens to be lighter.

   --surface-card-raised is the pair the frames actually use, added to
   semantic.css as its own role because nothing existing carries it: it equals
   --surface-inset in dark and is nowhere near it in light.

   NO BORDER. The frame's tiles have none — the fill does the separating now
   that it genuinely differs from the container in both themes, and an edge on
   top of that reads as a second, competing boundary. */
/* ── EVERY CARD THE SAME HEIGHT ────────────────────────────────────────────
   Daniel, 17-Sep: "Cards shouldnt flex to fit content either, we should fix the
   content to fix a locked size product card. i dont want product names 3 rows
   long."

   A grid row is as tall as its tallest cell, so one three-line name made every
   card in that row tall and left the short ones half empty — the grid looked
   ragged because the CONTENT was setting the size.

   Fixed height and the name clamped to two lines instead. The Add button is
   pushed to the bottom with margin-top: auto, so it sits on the same line across
   the whole row and a nurse's pointer lands in the same place on every card.

   Two lines rather than one, unlike the basket: here there is no variant row to
   carry the distinguishing tail, so "…10cm x 6m (singles)" has to survive, and
   at 13rem wide that needs two. */
.v2-ro-product {
    display: flex;
    flex-direction: column;
    /* ── 8rem, AND THE FLOOR IS WHAT IT IS ─────────────────────────────────
       Daniel, 17-Sep: "should the button not go up a bit, still leaving plenty
       room for the added to basket text and the SKU when needed replaces the
       variant count (as cant have both)".

       The card's tallest state is fixed and countable: 24px of padding, a name
       clamped to two lines, one facts row — variant count OR the PIP code,
       never both, which is the rule he is confirming — one marker row when
       something is in the basket, and the button. That comes to about 128px,
       which is this 8rem.

       So the gap he is seeing is not slack in the card: it is the marker's row
       standing empty on the cards that have nothing in the basket. Taking the
       height below this would clip the ones that do. The button keeps
       margin-top: auto so it lands on the same line across a row — a control
       that moves card to card is worse than a gap.

       ── A FLOOR, NOT A CEILING ────────────────────────────────────────────
       Daniel, 22-Sep: "when a product variant is added to the basket, the
       product card loses have its name, its like the product name gets cut
       off."

       It did, and the arithmetic above was simply short. Counted properly, the
       tallest state is 24 padding + 36 name (two lines at 0.875rem/1.3) + 24
       meta + 16 marker + 36 button + 12 of gaps ~= 148px, against the 128 this
       said. So the moment the marker appeared the card was ~20px over its own
       fixed height — and a flex column resolves that by SHRINKING whichever
       child will give, which is the name: it carries overflow: hidden for the
       two-line clamp, so it shrank to about one line and cut mid-word with no
       ellipsis to say so. Nothing looked broken; the name was just wrong.

       min-height keeps everything the 8rem was for — the empty marker row still
       holds the short cards to one height, and grid items stretch, so a row is
       still uniform — while letting the genuinely taller state have the room it
       needs instead of taking it out of the product's name. */
    min-height: 8rem;
    gap: var(--space-1, 4px);
    padding: var(--space-3, 12px);
    background: var(--surface-card-raised);
    border: 1px solid transparent;
    border-radius: var(--radius-card, 12px);
    transition: border-color .16s ease;
}

/* Hover is where the edge earns its place: something to show the tile is a
   target, without drawing a box around every one of fifty at rest. */
.v2-ro-product:hover { border-color: var(--border-medium); }
/* The card takes the click (request-order-catalogue.js), so it says so. */
.v2-ro-product { cursor: pointer; }

/* TWO LINES, then an ellipsis — "i dont want product names 3 rows long". The
   card carries the full name in a title attribute, so the tail these products
   differ by is still reachable on hover and to a screen reader.

   -webkit-line-clamp with -webkit-box is the only clamp with real support; the
   -webkit prefix is not a Chrome-ism here, it is what Firefox and Safari
   implement too. */
.v2-ro-product-name {
    /* NEVER SHRUNK. The clamp below is what decides how much of a long name is
       shown, and a flex parent short of room must not get a second opinion —
       that is how this came to display one cut line where two were asked for.
       If a card is ever over its height again, the overflow shows somewhere
       visible instead of silently eating the product's name. */
    flex: 0 0 auto;
    margin: 0;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text-strong);
    line-height: 1.3;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}


.v2-ro-product-meta {
    margin: 0 0 var(--space-2, 8px);
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* margin-top: auto has moved up to .v2-ro-product-bottom — the row is what sits
   at the foot of the card now, and two things claiming the same slack is how a
   layout starts disagreeing with itself. */
.v2-ro-product .v2-button { align-self: flex-end; }

.v2-ro-results-note {
    margin: var(--space-3, 12px) 0 0;
    color: var(--text-muted);
    font-size: 0.8125rem;
}


/* ── THE SUBJECT'S SELECTING SHAPE ────────────────────────────────────────
   Same card, different moment. While clinic and patient are still a job to do
   it lives in the body — centred, roomy, the only thing on screen (Daniel,
   14-Sep: "should be front and centre"). Once answered, request-order.js moves
   the SAME node back to the head and the 20rem rule above takes over.

   Everything here is scoped under .v2-ro-selection, so it applies by WHERE the
   card is rather than by a class the card carries — nothing to toggle, nothing
   to get out of step with the move. */
/* ── THE PANEL STOPS FILLING WHILE IT IS ONLY A QUESTION ───────────────────
   Daniel, 16-Sep: "why sucgh a big panel for the selection. looks an odd style
   choice?"

   It is the 'fill' key, doing its job in the wrong moment. The panel takes the
   column's whole height so the catalogue has a definite box to scroll inside and
   does not grow and shrink as a basket fills — right for the workspace, and
   absurd for one select, which got a 600px empty box to float in.

   Both are satisfiable because they never apply at once: while the selection
   stage is up there is no catalogue to give a box to. Same conditional shape as
   the clip rules above it. */
/* KEYED ON THE WORKSPACE, NOT ON THE STAGES. Daniel, 16-Sep: "panel collapsed
   on reload", and "Panel should always be to the bottom of the page after the
   patient select stage."

   .v2-ro-workspace is server-rendered `hidden` and stays hidden until a patient
   is settled, so it is the one signal that is already correct in the very first
   frame. .v2-ro-stages was the wrong side of the same coin: it renders VISIBLE,
   so on a reload the panel read as compact before any JS had run, and if
   anything left it visible the panel stayed collapsed with a catalogue in it.

   Compact while choosing, full height the moment there is a catalogue — and
   full height is the resting state, which is the one to get right by default. */
.v2-panel[data-v2-panel="request-order"][data-v2-fill]:not([data-v2-ro-stage="work"]) {
    align-self: start;
}

/* ── AND THE SQUARED EDGE GOES BACK WHEN THE CATALOGUE DOES ────────────────
   Daniel, 18-Sep: "when we choose a product which expands the catalogue and
   thus squaring off the bottom of the panel, when you then go back to patient
   select, the container is still squared off? it doesnt revert back to the
   rounded edges at the bottom."

   data-v2-overflowing is a MEASUREMENT, and going back to the selecting stage
   changes the panel's width, its alignment and which of its two regions is
   hidden — all at once. Whatever the flag reads while that is settling is what
   it keeps, because once the layout is final nothing resizes again to prompt
   another look. Chasing it with one more re-measure would only move the race.

   So the stage decides instead of the measurement, which is not a workaround:
   the note above this block already says it — "a panel shrunk to fit its
   contents has no surplus to hide", and a compact panel measures as overflowing
   almost by definition, being sized to its own card with the body permanently a
   hair over. The selecting stage is that panel. It ends where its card ends, so
   it keeps its bottom edge whatever the measurement happened to catch.

   Specificity carries it over .v2-panel[data-v2-overflowing] rather than source
   order, so it holds wherever this block ends up. */
.v2-panel[data-v2-panel="request-order"]:not([data-v2-ro-stage="work"])[data-v2-overflowing] {
    border-bottom-left-radius: var(--radius-panel);
    border-bottom-right-radius: var(--radius-panel);
    padding-bottom: 60px;
}

/* The 60px that moves INSIDE the scroller when a panel spans the fold comes
   back out with it — the panel's own foot is doing that job again, and leaving
   both would be 120px of floor under two selects. */
.v2-panel[data-v2-panel="request-order"]:not([data-v2-ro-stage="work"])[data-v2-overflowing] .v2-panel-body {
    padding-bottom: 0;
}

/* AND THE PANEL'S OWN CLIP GOES WITH IT. "why is the patient select panel cut
   off?" — because there are TWO clips above that list and I had only lifted one.
   .v2-panel-body's went last commit; .v2-panel[data-v2-overflowing] is the other,
   and it is exactly the trap already written down twice: a .v2-combo-list escapes
   its field and never an ancestor that clips.

   A compact panel measures as overflowing almost by definition — it is sized to
   its own card, so the body is permanently a hair over — which means that clip is
   effectively always on during selection, and it lands on the panel's bottom edge
   where the list opens. Nothing is gained by it here: a panel shrunk to fit its
   contents has no surplus to hide. */
/* ── NOTHING ELSE. THE SHELL ALREADY DOES THIS ─────────────────────────────
   Daniel, 8-Sep, about Queries — and it is written down in components.css above
   .v2-q-messages: "that doesnt match my patients and my orders, if the panel is
   genuinely short then it folds rounded… if it has content enough to span length
   of page then it falls off the page like my orders and my patients does."

   That is the shell's own behaviour and it needs nothing added: a panel grows
   with its content, and .v2-panel[data-v2-overflowing] squares the edge off when
   that content spans the fold.

   I had stacked overrides on top of it — overflow: visible, then max-height:
   none — each to fix what the previous one broke, and between them they took the
   panel out of the system that was already producing the behaviour being asked
   for. The squared corner was not a rounding bug and not a height bug; it was
   this panel no longer being allowed to behave like a panel.

   The ONE thing that genuinely does not belong here is 'fill', which pins the
   panel to the column height whatever it holds — right for a catalogue, absurd
   for two selects. That override stays, above. Everything else comes out. */

/* NO FLOOR ONCE THE CATALOGUE IS UP. "no bottom padding" — the panel's own 60px
   foot is right for a page that ends, and wrong for a list that carries on past
   the edge. Products now run to the panel's rounded bottom and are clipped by
   it, which is the whole "rolling off screen" effect; a band of padding under
   them says the list stopped there. */
.v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] {
    padding-bottom: 0;
}

.v2-ro-selection {
    display: grid;
    place-items: center;
    /* No flex: 1 1 auto. That was what forced the stage to fill a panel that is
       no longer filling, which would have put the empty box back inside it. */
    min-height: 0;
    /* 32px was breathing room inside a box that was going to be tall anyway.
       With the box sized to the card, it is just a second margin around a card
       that already has its own. */
    padding: var(--space-3, 12px);
}

/* ── THE SCROLL STANDS DOWN WHILE THE CARD IS BEING FILLED IN ───────────────
   Daniel, 15-Sep: "the Drop downs collide and dont overlap the containers."

   THIS is the one the previous fix could not reach. Scoping the head fold
   stopped that clip, but the selection card lives in .v2-panel-body, and
   .v2-panel-body is overflow-y: auto — a scroll container. An absolutely
   positioned .v2-combo-list escapes its FIELD and nothing else; it cannot
   escape a scroll container, so the options were cut at the body's edge.
   select.js flips the list UPWARD when the viewport has no room below it
   (`is-above`), which is why it was the TOP edge doing the cutting on a card
   sitting mid-panel rather than the bottom one.

   Nothing else in this panel can fix that: no z-index, no position on the card,
   no further selector scoping. The clip itself has to go.

   Conditional, on the shell's own measurement, exactly as .v2-panel's clip is
   and for the same reason. The selection stage is ONE CARD centred in a whole
   panel: it has room to spare and the scroll is doing no work. Where it
   genuinely is short of room — a phone, a staff order with four controls —
   overlays.js has already set data-v2-overflowing, this rule stops matching,
   and the scroll comes back with the dropdown reachable by scrolling. That is
   the honest trade rather than a dropdown that cannot be reached at all.

   THE data-v2-overflowing GUARD IS BACK, and taking it out was a mistake I made
   from a wrong diagnosis. I decided a compact panel must sit "forever a hair
   from overflowing" and removed the condition; the real reason it was
   overflowing was .v2-ro-stages asking for flex: 1 1 auto in a panel that no
   longer had slack to give, which is fixed at its source now.

   With the guard the two states are both right and they are the shell's own:
   short panel, nothing overflowing, the clip lifts and the dropdown escapes into
   a rounded panel; long panel, overflowing, the body scrolls and the panel
   squares off and runs past the fold like My Orders. Without it, a long
   selection card would have had no scroll at all — the body would not clip and
   the panel would, so the content would simply be cut with no way to reach it.

   Superseded note kept for the reasoning only: "That guard made sense while the
   panel filled the column and a short card floated in it — there was room to
   spare, so overflowing meant something had genuinely outgrown the space. The
   panel is COMPACT during selection now: it shrinks to the card, which means the
   body is forever a hair from overflowing, the flag sets, the rule stops
   matching and
   the dropdown is clipped again. The condition was measuring a situation that no
   longer exists.

   Nothing needs guarding either. A panel sized to its own card has no surplus
   content to hide, so there is nothing for a clip to do but cut the one thing
   that has to escape.

   Keyed on the workspace like everything else here: the moment a catalogue
   exists this stops matching and the body is a scroll region again, which the
   rest of the panel depends on absolutely. */
/* ═══════════════════════════════════════════════════════════════════════════
   THE BODY NEVER SCROLLS IN THIS PANEL, IN EITHER STAGE
   ═══════════════════════════════════════════════════════════════════════════
   This one rule replaces four conditional variants that have each been wrong in
   turn, and it is what makes both of today's faults impossible rather than
   fixed.

   NOTHING IN THIS PANEL NEEDS THE BODY TO SCROLL. Selecting is one small card.
   The catalogue scrolls its products inside itself, in a region with its own
   bar. So the body carrying overflow-y: auto was never buying anything — it was
   only clipping.

   AND IT WAS CLIPPING THE TWO THINGS THAT HAD TO ESCAPE:

   the patient dropdown — "the patient select panel has once again broken same
   as before" — because a scroll container cuts an absolutely positioned list at
   its edge, and the compact panel kept tipping into data-v2-overflowing, which
   is what my last guard was conditioned on;

   and the search bar — "First scroll point we already lose the search bar". It
   was never lost. It rises into the panel head, the head sits OUTSIDE the body,
   and the body clipped everything that crossed its top edge. The bar went
   exactly where it was told and was cut off on arrival, which is why the brand
   strip below it started disappearing next.

   One rule, both stages, no condition to get wrong. */
.v2-panel[data-v2-panel="request-order"] .v2-panel-body {
    overflow: visible;

    /* AND NO SCROLLBAR GUTTER, which is the misalignment. Daniel, 16-Sep: "the
       patient info is slightly to the left of its correct possition."

       .v2-panel-body reserves 12px on the right with scrollbar-gutter: stable so
       a bar appearing does not shift the content under it. The panel HEAD has no
       such reservation — so the body's 300px basket column and the head's 300px
       patient column were measuring from right edges 12px apart, and the two
       could never line up however carefully they shared a width variable.

       Nothing scrolls in this body any more, so there is no bar to reserve for,
       and both grids get the same right edge to work from. */
    scrollbar-gutter: auto;

    /* AND NO PULL INTO THE PADDING EITHER (Daniel, 13-Oct: "ensure, actually
       always that the left and right padding on the panel is uniform"). The
       shared rule pulls the body 26px into the right padding and gives 14 back,
       which only nets out when the 12px gutter is reserved. With the gutter
       off, the content ran 12px further right than it starts on the left: 40px
       of padding on the left, 28 on the right. No gutter, no pull: the body is
       the panel's content box, 40 and 40. */
    margin-right: 0;
    padding-right: 0;
}

/* ── NO TITLE OVER THE CATALOGUE ───────────────────────────────────────────
   Daniel, 16-Sep: "we just remove the title when on the catalogue page, its
   there at selecting a patient. thats fine. when the catalogue opens, just have
   it land in its scroll position then we dont have to worry about the shift."

   Which removes the problem rather than solving it. Every version of the lift
   existed to reclaim the title's height once browsing started, and each one
   traded a new bug for the last: a sticky bar losing its frame of reference, a
   negative margin resizing a grid row, a collapse resizing the scrollport
   mid-gesture. If the title is not there, the catalogue simply starts where it
   should and nothing has to move at all.

   The title still does its job where it has one — above the selection card,
   telling a nurse what this screen is for. By the time a catalogue is open that
   question is long answered and the patient card carries the only context that
   still matters. */
/* THE WHOLE HEAD GOES, not just the title. With the patient card moved into the
   basket column there is nothing left in it, and an empty head still reserves a
   band across the panel — which is why the catalogue started below the card
   rather than beside it: "the catalogue should be starting up the top now the
   title is hidden". */
.v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] .v2-panel-head {
    display: none;
}

/* The card in its new home: full width of the column, so its edges are the
   basket's edges rather than something aligned to them. */
.v2-ro-aside .v2-ro-subject-wrap {
    width: auto;
    max-width: none;
    justify-self: stretch;
    flex: 0 0 auto;
}

/* ── THE COLUMNS REACH THE FLOOR ───────────────────────────────────────────
   Daniel, 16-Sep: "ensure the product catalogue container spans to the page
   height same with the basket / order details. shouldnt stop early."

   They were stopping early one level further up than I had been looking.
   .v2-panel-body is a GRID, and a grid with no explicit rows gives its child an
   implicit `auto` track — so the workspace was as tall as its contents no matter
   how carefully the workspace's own row was bounded. Every fix below it was
   dividing up a height that had already been cut short.

   minmax(0, 1fr) hands it the body's full height instead, and the chain from
   there already works: the workspace fills it, its own 1fr row fills the
   workspace, and both columns stretch into that. */
.v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] .v2-panel-body {
    /* FLEX, NOT A GRID ROW. minmax(0, 1fr) on the body's implicit track should
       have handed the workspace the whole height and did not — the body has two
       children in the markup (the workspace and the hidden stages), so which
       track the visible one lands in depends on what is displayed, and an
       explicit single row is the wrong shape for that.

       A flex column with one growing child has no such ambiguity: whatever is
       displayed takes the space, and min-height: 0 lets it be shorter than its
       contents so the products still scroll inside it. This is the last link in
       the chain — panel stretches, body fills the panel, workspace fills the
       body, both columns stretch into the workspace — and it was the one that
       was quietly not holding, which is why everything stopped ~185px short of
       the floor. */
    display: flex;
    flex-direction: column;
}

.v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] .v2-ro-workspace {
    flex: 1 1 auto;
    min-height: 0;
}

/* ── A BUFFER UNTIL THERE IS A REASON NOT TO HAVE ONE ──────────────────────
   Daniel, 16-Sep: "at default and for the right column, have a small buffer from
   the page edge, only when content spans should the container actually hit the
   edge? and lose the curve edges too."

   Which is the shell's own rule for panels — short means rounded and floating,
   spanning means flush and cut off — applied one level in, to the columns rather
   than the panel. The panel has to keep its floor at zero so a column CAN reach
   the edge; the buffer belongs to the columns, and each gets to answer for
   itself whether it has outgrown the space.

   A MARGIN, NOT PANEL PADDING. These are stretch items in a bounded row, so a
   bottom margin shortens them without touching the row. A negative margin or a
   height change here is what caused the runaway basket, and there is no reason
   to go near either. */
.v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] .v2-ro-catalogue,
.v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] .v2-ro-aside {
    margin-bottom: var(--space-5, 20px);
    transition: margin-bottom .18s ease;
}

/* ── THE INNER CONTAINER KEEPS ITS CURVE, AND THAT FIXES THE FLICKER ───────
   Daniel, 17-Sep: "we should finish any content with the curved internal
   container here no? main panel continous off, inner one curves round
   finished." — and, in the same breath, "i have a glitch on the scroll bar
   showing and hiding rapidly … i think its likely to do with the height of
   haddenham range is at the exact trigger spot".

   Both are answered by deleting this rule, and the second explains itself once
   the first is understood.

   syncSpans() MEASURES overflow and then set a flag that REMOVED 20px of
   margin-bottom from this very container. Removing the margin made the column
   taller, which reduced the overflow, which cleared the flag, which put the
   margin back, which increased the overflow — and a ResizeObserver on the same
   element re-ran the measurement each time. A measure-then-mutate-what-you-
   measured loop. It only showed on Haddenham because a brand whose content sits
   nowhere near the threshold never crosses it in either direction; Haddenham's
   sat exactly on it.

   So the inner container now always keeps its 20px buffer and its rounded
   bottom — which is what Daniel is asking for anyway. The flag survives for the
   PANEL below, where it only changes a corner radius and cannot alter height,
   so there is nothing left to feed back. The fix is removing the cause rather
   than damping the symptom.

   This reverses the 16-Sep reading that a curve must go when content does not
   end. On the PANEL that still holds. On a container sitting inside it with a
   visible gap beneath, the curve is what says the box is a box. */

/* ── SQUARE WHILE THERE IS MORE BELOW; CURVED ONCE THERE IS NOT ────────────
   Daniel, 17-Sep: "when the contents is still going the dark inner container
   should spill off like the outer panel. only at the end of the content should
   it then curve round with that bottom padding".

   The distinction is SCROLL POSITION, not overflow — "there is more than fits"
   against "there is more BELOW THIS". A rounded corner answers the second: it
   says the content ends here, which is true at the bottom of a long list and
   false three screens up it.

   RADIUS ONLY, and that is a hard constraint rather than a preference. The flag
   is set from scrollTop, so keying a margin or padding off it would recreate
   the loop fixed an hour ago from the other side: 20px less margin makes the
   column taller, which changes whether you are at the end, which restores the
   margin. A corner radius cannot alter height, so it cannot feed back.

   The bottom padding Daniel wants at the end is already there and always has
   been — .v2-ro-products carries 16px inside the scroll region, so scrolling to
   the last card reveals it under the final row, and the corner rounds over it.
   It did not need to be toggled, only uncovered. */
/* ── AND IT REACHES THE EDGE WHILE IT SPILLS ───────────────────────────────
   Daniel, 17-Sep: "still has that padding before it reaches end of the
   content?" — the 20px buffer was holding the container clear of the panel
   floor even while content was visibly running off it.

   The margin comes back off, keyed on spans-work. THIS IS THE TOGGLE THAT WAS
   OSCILLATING this morning, and it is safe now only because syncSpans() gained
   hysteresis: removing 20px reduces the overflow by 20, and the old
   symmetrical test flipped straight back. With a 24px-on / 1px-off dead band a
   list overflowing by 60 settles on (40 left), and one overflowing by 10 never
   turns on at all — which is correct, since 10px of hidden content is not
   something that should square a corner.

   Deliberately NOT keyed on at-end, even though the margin is what Daniel is
   describing at the end. at-end is read from scrollTop, and a margin changes
   clientHeight, which changes whether you are at the end: that is the same loop
   again with a different trigger, and no dead band saves it because the
   measurement moves by exactly the amount the rule changes. So the margin
   answers the stable question (does this overflow at all) and the radius
   answers the moving one (is there more below this). */
/* ── 0: WHILE IT SPILLS IT REACHES THE FLOOR ───────────────────────────────
   Daniel, 17-Sep: "i do not want a line on scrolling, only at the bottom of the
   page, if not posble then leave how it was".

   The 8px I tried was meant to keep a gap under a marginally-overflowing list,
   and it did — but an 8px gap under a list that is still scrolling draws the
   container's bottom edge as a LINE across the panel, which is worse than the
   thing it was solving. A container that is spilling should read as spilling.

   So it reaches the floor again. The gap only at the end of the content is what
   was wanted and is not buildable: it is a height change keyed on scroll
   position, and a height change moves the scroll position. His own fallback
   applies — leave it how it was. */
.v2-panel[data-v2-panel="request-order"][data-v2-ro-spans-work] .v2-ro-catalogue {
    margin-bottom: 0;
}

.v2-panel[data-v2-panel="request-order"][data-v2-ro-spans-work]:not([data-v2-ro-at-end]) .v2-ro-catalogue {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

/* ── AND THE PANEL SQUARES OFF WITH IT ─────────────────────────────────────
   Daniel, 16-Sep: "upon a product load and many results, the page spans but the
   panel curves remain, should they not go straight? matching the my-orders and
   my patents logic?"

   Yes — and the shell already does exactly that, off data-v2-overflowing. It
   cannot fire here: that flag measures whether .v2-panel-body scrolls, and this
   panel's body deliberately does not. The products scroll inside their own
   region instead, so the panel is never "overflowing" by the shell's definition
   while being very obviously full by any other.

   data-v2-ro-spans-work is the same question asked of the box that actually
   scrolls. Same rule, same look, measured where the scrolling really happens. */
.v2-panel[data-v2-panel="request-order"][data-v2-ro-spans-work] {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

/* The basket column keeps its buffer for the same reason: it is an inner
   container too, and its flag was driven by the same measure-then-mutate
   pattern off basketItems. Gone, so both columns finish cleanly and neither
   can oscillate. */

/* The aside is a column of cards rather than a card, so the squaring lands on
   whichever card is at the bottom of it. */
.v2-panel[data-v2-panel="request-order"][data-v2-ro-spans-aside] .v2-ro-details {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

/* ── AND THE FILTER RAIL SQUARES OFF THE SAME WAY ──────────────────────────
   Daniel, 20-Sep: "the filter column should act the same as the product
   catalogue panel. when spanning the length of the page extend past, only
   curve when at the bottom of content."

   Same rule as the catalogue, keyed on the same kind of flag, because it is
   the same situation: forty manufacturers spill past the fold exactly as four
   hundred products do, and a rounded corner under a list that carries on says
   the list has ended when it has not.

   AND IT READS ITS OWN SCROLL. Daniel, 20-Sep: "its the product catlogue
   dictating if the filter pane curves or not, it should be the filter content
   that does that". It was — I had this on the catalogue's at-end, which is a
   different box with a different scroll position. The rail has its own now,
   which is what the note above syncSpans asked for in the first place. */
.v2-panel[data-v2-panel="request-order"][data-v2-ro-spans-filters]:not([data-v2-ro-filters-at-end]) .v2-ro-filters {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
/* ── THE RAIL'S FLOOR IS A CONSTANT, AND HAS TO BE ─────────────────────────
   Two bugs came out of making this 12px conditional, and they are the same
   bug from both ends. Recorded because it will look like an easy win again.

   Daniel, 20-Sep: "there is a small padding on the filter bottom that shouldnt
   be there?" — so I removed it while the rail was spilling, keyed on
   `:not([data-v2-ro-filters-at-end])`.

   Then 21-Sep: "the scroll bar on the filter pane is also bouncing endlessly
   at the bottom (looks like it doesnt know the resting state it should be at
   when all the way down)". Of course it did. `atEnd` is a bare scroll-position
   test with NO hysteresis — spanning() has OFF_SLACK for exactly this reason
   and atEnd deliberately has none, because it only ever drove a border-radius,
   and "radius cannot alter height, so it cannot feed back". I hung a height
   change on it. Reaching the bottom returned the padding, which made the
   scroller 12px shorter, which un-reached the bottom, which removed it again.

   Removing the padding unconditionally stopped the bounce and bought the other
   half of the trade: with no floor the thumb runs into the rail's rounded
   corner and is clipped by it — "scroll bar still doing it looks cut off".

   So it is constant. The 12px is what keeps the track clear of the radius, and
   any rule that varies it is varying the thing being measured. What Daniel
   originally objected to — a strip of card under a list that carries on — is
   answered by the CORNER instead: while the rail spills, its bottom squares
   off, so the list runs into a straight edge rather than a rounded pocket.
   That rule changes no geometry, which is the whole reason it is safe. */

/* The panel's own clip goes too, for the same reason: with nothing scrolling
   there is no surplus for it to protect, and it sits exactly where the dropdown
   and the search bar have to pass. */
.v2-panel[data-v2-panel="request-order"] {
    overflow: visible;
}

/* THE HEIGHT CAP ONLY LIFTS WHILE SELECTING, and that scoping matters. .v2-shell
   is height: 100dvh with overflow: hidden — the wallpaper crop — so the cap is
   what keeps a panel inside the one ancestor that will silently cut it. Lifting
   it for the catalogue would trade a capped panel for one the shell clips at the
   window's edge, which is the failure the note at .v2-panel-col calls silent.

   A selection card is four selects at most and will never approach 100dvh, so
   here there is nothing for the cap to protect and it only stops the panel
   growing to fit its own contents. */
.v2-panel[data-v2-panel="request-order"]:not([data-v2-ro-stage="work"]) {
    max-height: none;
}

/* The cap that keeps the head compact is exactly wrong here. */
.v2-ro-selection .v2-ro-subject-wrap {
    width: 100%;
    max-width: 34rem;
    justify-self: center;
}

/* ── AND THE SELECTION STAGE MIRRORS IT ────────────────────────────────────
   Daniel, 16-Sep: "the patient select shuld mirror this."

   Same two steps as the catalogue: the container takes the neutral card and the
   thing inside it steps down to the inset. .v2-ro-stages carries the first half
   (see its own rule); this is the second, and the two have to move together —
   a --surface-card card on a --surface-card container is the same-colour trap
   that has cost this panel three fixes, and it is invisible in dark where both
   are Flint 600 either way.

   Daniel, 16-Sep: "if e can update the patient choosing section with the same
   design" — so it takes --surface-card-raised, the tiles' own step, rather than
   the inset I first gave it. Lighter than its container in both themes, which
   is the pattern the Product frames set and the one this stage should share:
   the two screens are the same panel a moment apart and should not be lit
   differently.

   Scoped to .v2-ro-selection, because the SAME card is --surface-card when it
   lives in the aside: there it sits on the panel's own page surface with
   nothing behind it to collide with. A fill has to know what it is sitting on,
   and this card sits on two different things at two different moments. */
.v2-ro-selection .v2-ro-subject {
    gap: var(--space-4, 16px);
    padding: var(--space-6, 32px);
    /* The card fill now, not the raised step: the box it was raised off has
       gone (13-Oct, see .v2-ro-stages), so it sits on the panel as the same
       card does in the basket column: Flint 600 on Flint 500 in dark, Pebble
       100 on Seafoam in light. */
    background: var(--surface-card);
}

/* The fields get their own width back — the head's cap exists because a
   resolved summary is two short facts, and this is four controls. */
.v2-ro-selection .v2-ro-field { max-width: none; }

/* And the truncation goes: the head truncates because it is 20rem, and a
   control you are about to use should never hide its own value. */
.v2-ro-selection .v2-ro-item-value {
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
}

/* ── the basket, as a floating trigger ───────────────────────────────────
   The column is gone (Daniel, 14-Sep: "lets take the basket out and have it
   under a button thats an overlay modal?"), so the catalogue takes the whole
   width and the basket arrives when asked for.

   The COUNT rides on the button, which is what a modal costs and this repays:
   the basket can be judged without opening it. */
/* OUTSIDE THE PANEL. It began inside the catalogue card, where it read as part
   of the results; then on the panel body, where it still sat over the work.
   Daniel, 14-Sep: "we need to move the basket out of the panel i think is
   best?"

   Fixed to the viewport, which is what "out of the panel" means. The usual
   objection — that a fixed element hangs over other panels — does not apply:
   this button exists ONLY in this panel's markup and a hidden panel renders
   none of its children, so it is scoped by EXISTENCE rather than by
   positioning. */
/* ── THE RIGHT COLUMN ──────────────────────────────────────────────────────
   It follows the products down. The body is the scroll region and the catalogue
   runs long, so a column pinned to the top of it would be off screen by the
   third row of results — and the whole argument for a column over a modal is
   that you can see the order building. align-self: start is what lets sticky
   work at all: a stretched grid item is already as tall as its track and has
   nowhere to stick to. */
.v2-ro-aside {
    display: flex;
    flex-direction: column;
    gap: var(--space-4, 16px);
    min-width: 0;
}

/* Everything that scrolls: the patient card, the basket, Order details. Its own
   flex column so the cards keep the same rhythm they had when the aside held
   them directly — the wrapper is structural, and should not be visible in the
   spacing. */
.v2-ro-aside-scroll {
    display: flex;
    flex-direction: column;
    gap: var(--space-4, 16px);
    min-width: 0;
}


/* ── THE COMMIT BUTTON DOES NOT SCROLL AWAY ────────────────────────────────
   Daniel, 16-Sep: "The submit and schedule order buttons shouldd stay locked to
   bottom not scroll off view."

   The column takes the panel's full height and Order details is pushed to the
   foot of it with margin-top: auto, so Schedule order sits on the bottom edge
   whatever is above it. The basket takes the room that leaves and scrolls its
   own list if it outgrows it — a long order pushes items out of sight, never
   the button that submits it.

   Not sticky: sticky needs something to scroll past, and nothing here scrolls
   any more. That offset was also why the basket sat lower than the catalogue
   once the page had moved at all — "the basket seems to start too low". */
/* ── HOW THE TWO CARDS SHARE THE COLUMN ────────────────────────────────────
   Daniel, 16-Sep: "When the basket expands will it expand to a point before
   creating a scroll bar within itself?" Yes, and this is the order it happens
   in:

     1. The basket is its natural height. An empty one is small, which is why it
        does NOT get flex: 1 — a card stretched to fill 400px to say "Your basket
        is empty" would be the same mistake as the oversized selection panel.
     2. margin-top: auto on Order details holds it against the bottom edge and
        absorbs whatever is spare, so the gap between them closes as items are
        added. Nothing moves except the gap.
     3. Once the gap is gone the basket starts scrolling its OWN list, because
        min-height: 0 is what lets a flex item agree to be shorter than its
        content and the list inside it carries the overflow.

   So a long order pushes items out of sight, never Order details off the bottom
   — and Schedule order is outside the scrolling part of its own card, so it
   cannot be pushed away either. */
/* STICKY, because the body is the scroll again. The catalogue column grows as
   long as its results, and without this the basket would scroll off the top of
   the page after two rows of products — the exact thing a column is supposed to
   prevent. Sticky to the top of the same scroll the products move in, so it
   holds its place while they go past.

   Capped to the visible height so Schedule order stays on screen: the column
   fills what it can see, Order details is pushed to its foot, and the basket
   takes what is left and scrolls its own list. */
/* ── THE BASKET TAKES THE ROOM, ORDER DETAILS SITS UNDER IT ────────────────
   Daniel, 16-Sep: "make the Order Details lower and Basket bigger. as per mock
   up i provided."

   flex: 1 1 auto on the basket does both at once — it grows into whatever the
   column has spare, which pushes Order details to the bottom without needing a
   margin to hold it there. It replaces margin-top: auto rather than joining it:
   two things claiming the same slack is how a layout starts disagreeing with
   itself.

   The earlier objection to growing the basket was that an empty one would become
   a 400px card saying "Your basket is empty". That was about the CATALOGUE
   column, where an inflated card sat next to nothing. Here the column is a fixed
   width with a second card beneath it, and the mock says the space reads better
   as basket than as gap.

   It stretches, so the column no longer needs to be sticky: nothing scrolls past
   it any more. */
@media (min-width: 1280px) {
    .v2-ro-aside {
        align-self: stretch;
        min-height: 0;
    }

    /* ── NEITHER CARD SQUASHES; THE COLUMN SCROLLS ─────────────────────────
       Daniel, 17-Sep: "the ORder details should never squash (currently basket
       expanding squashes the upload / notes section) it should just be push
       down as a whole block. thus activating a scroll bar to get too. (i know
       this is counter to my 'submit should be sticky' but i think thats the
       better action UX)"

       He is overruling his own earlier requirement and he is right to. Both
       previous arrangements were a fight over one fixed height: first Order
       details refused to shrink and starved the basket, then the basket took
       priority and Order details lost its dropzone and notes to a scrollbar.
       Either way one region was being compressed to fit the other, and a
       compressed dropzone or a two-line basket are both unusable.

       Nothing is compressed now. Each card is its own natural height —
       flex: 0 0 auto, both of them — and a long basket simply pushes Order
       details further down the column. The COLUMN is what scrolls, once, with
       our own scrollbar (the aside carries .v2-scroll in the markup), instead
       of two nested scroll regions competing inside it.

       THE COST, ACCEPTED DELIBERATELY: Submit and Schedule no longer sit pinned
       to the bottom edge. They travel with Order details and a full basket puts
       them below the fold. That reverses "The submit and schedule order buttons
       shouldd stay locked to bottom not scroll off view" from 16-Sep, and it is
       the right trade — a nurse reaches those two buttons once per order, but
       fights a squashed notes field on every one. */
    /* ── THE BASKET TAKES UP THE SLACK, AND ONLY THE SLACK ─────────────────
       Daniel, 17-Sep: "give the basket enough room so by default the Order
       details box is in line with the bottom of the product cat at rest. -
       looks to be about doubel the size"

       With both cards at their natural height the leftover column space
       collected BELOW Order details, so an empty basket left it floating some
       65px above the catalogue's bottom edge. Both columns already carry the
       same 20px bottom margin, so they share an edge — the gap was slack in the
       wrong place, not a mismatch.

       grow 1, SHRINK 0. The basket expands into whatever the column has spare,
       which puts Order details on the bottom edge at rest and sizes the gap
       exactly rather than guessing a height that would only be right at one
       panel size. And because it cannot shrink, a full basket still refuses to
       compress — it grows past the column and the aside scrolls, which is the
       behaviour agreed above. */
    /* ── THE GROWTH WENT TO THE CARD AND THE CARD COULD NOT USE IT ─────────
       Daniel, 22-Sep: "still a large amount of padding in the basket there?
       look how it is when i had a MTM form. its correct. then if i remove that
       form ... it goes back to broken."

       Measured, and the reasoning above was half right. From the console with
       three lines in the basket and no form required:

           aside-scroll   783
           basket         447          <- grown into the column's slack
           items          320, content 367, max-height 320px
           forms            0 (hidden)
           details        202

       The basket card took the spare 73px because flex-grow told it to, and
       then had nowhere to put it: its only stretchy child is the list, and the
       list was already pinned at its 20rem cap with more content than it could
       show. So the growth became a band of dead space between the last line and
       the bottom of the card — and it vanished when a measurement form appeared
       only because the forms card had taken the slack first.

       Two rules were each claiming the same space with different ideas about
       what it was for: "the basket expands into whatever the column has spare"
       and "the list only grows so far". The second is Daniel's explicit ask
       (4/5 products then scroll) and is about the LIST filling with content.
       The first was only ever a means to an end — putting Order details on the
       bottom edge — and there is a way to do that which cannot leave a gap
       inside a card.

       So the card is its content again, and the slack goes BETWEEN the two
       cards rather than inside the top one. margin-top: auto on Order details
       is what puts it on the bottom edge; the earlier note says this "collected
       BELOW Order details", which is what happens when nothing claims the
       slack — an auto margin claims it, and claims it in the right place. ONE
       rule owns it now, which is the point the earlier note was reaching for. */
    .v2-ro-basket  { flex: 0 0 auto; }
    .v2-ro-details { flex: 0 0 auto; margin-top: auto; }

    /* ── AND IT ONLY GROWS SO FAR ──────────────────────────────────────────
       Daniel, 17-Sep: "i think the basket should only expand so far when being
       filled before it gets an internal scroll? stops the column scroll getting
       too big then but allows UX fre3edom for the most part? I think 4/5
       product cards before scroll?"

       Right on both counts. Unbounded growth meant a twelve-line order pushed
       Order details a screen and a half down, so reaching Submit became a long
       scroll past products already chosen — and the column scrollbar stopped
       meaning anything, because it was measuring the basket rather than the
       column.

       The CARD is the limiter and the LIST scrolls inside it, so the heading,
       the count and the lead-time caveat all stay put while the lines move.

       20rem, and the number is MEASURED rather than derived. I first reasoned
       a line to 92-100px from its padding, wrapped name, gap and control row,
       set 30rem for "about five" — and Daniel: "it seems to allow 6, i think 4
       is better." Six in 480px puts a real line at ~80px, so the estimate was
       high by a fifth and the observation is the number that counts. 4 x 5rem.

       Still a LENGTH and not a line count, for the reason that made the first
       guess wrong: these lines have no fixed height, because the product name
       is the part that wraps and Haddenham's run to two lines where shorter
       brands take one. A count would give four sometimes and five others. */
    .v2-ro-basket-items {
        /* flex 1 1 so the list takes the card's spare height rather than
           leaving a band of dead space under itself when the basket grows into
           the column — the card is a flex column, and this is its only
           stretchy child. max-height is what actually stops the growth. */
        flex: 1 1 auto;
        max-height: 20rem;
        overflow-y: auto;
        min-height: 0;

        /* ── IT SCROLLS, IT JUST DOES NOT SAY SO ───────────────────────────
           Daniel, 17-Sep: "i feel the scroll bars clash, in fact why dont we
           hide the scroll bar on the product basket? it still scrolls at the
           same point. but no blue scroll bar visual?"

           Two bars inside a 300px column — the column's and this list's — read
           as two things to operate when they are one list inside one column,
           and the inner one is the less important of the two. Scrolling is
           untouched: the wheel, a trackpad, a drag and tabbing between the
           steppers all behave exactly as before, and the clipped line at the
           fold is its own affordance.

           Scroll chaining is deliberately left ON (no overscroll-behavior,
           which is why .v2-scroll is not used here): reaching the bottom of the
           list should carry straight on into the column rather than dead-end
           against a boundary the nurse cannot see. */
        scrollbar-width: none;        /* Firefox */
        -ms-overflow-style: none;     /* legacy Edge */
    }

    .v2-ro-basket-items::-webkit-scrollbar { width: 0; height: 0; }

    /* ── THE BAR SITS OUT IN THE GUTTER ───────────────────────────────────
       Daniel, 17-Sep: "the scroll bar on the right, could that be a little to
       the right".

       It was hard against the cards, because the scroll container and the cards
       share an edge — the bar is drawn at the container's inline end, which is
       exactly where the card stops. Widening the container rightwards by the
       negative margin moves the bar out into the panel's own padding, and the
       matching padding puts the cards back where they were. Net effect: the bar
       moves, nothing else does. */
    /* The SCROLL REGION, not the column — the column also holds the pinned
       actions now, and those must sit outside anything that scrolls. */
    .v2-ro-aside-scroll {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;

        /* ── THE BAR COSTS THE CARDS NOTHING ───────────────────────────────
           Daniel, 17-Sep: "when the scrol bar becomes active, it pushes the
           containers for basket and that in a little narrow - it hsould just
           fill the gap there."

           A scrollbar takes its width out of the CONTENT box, so the cards were
           reflowing 12px narrower the moment a fifth line appeared and back
           again when one was removed — a layout shift triggered by adding to
           the basket, which is the worst possible moment for one.

           scrollbar-gutter: stable reserves that 12px whether or not the bar is
           showing, so the width never changes. The negative margin then widens
           the element by exactly the reserved amount, which puts the cards back
           at the column's full width and leaves the bar sitting out in the
           panel's padding — the same place the earlier 8px nudge put it, but
           now it is the gutter rather than a guess.

           No padding-right to go with it: the gutter IS the inset. Adding both
           would take the 12px twice and narrow the cards permanently, which is
           the bug this replaces rather than fixes. */
        scrollbar-gutter: stable;
        /* THE BAR SITS IN THE MIDDLE OF THE PADDING (Daniel, 13-Oct: "ensure
           when the scroll bar is active on the basket it sits centrally in the
           space"). The panel's own body arithmetic, here: the scroller reaches
           half the padding plus half the 12px bar into it, and the cards keep
           the column's edge with the rest as padding. 40px padding: the bar
           runs 14 to 26 into it, 14px either side. */
        margin-right: calc(-1 * (var(--panel-pad-x) / 2 + 6px));
        padding-right: calc(var(--panel-pad-x) / 2 - 6px);
    }
}

.v2-ro-details-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
    /* IT DOES NOT SCROLL ANY MORE, and that is the point — see the note in the
       min-width: 1025px block above. This region existed to absorb the squash
       when Order details had to fit a height it did not choose; now the column
       scrolls instead and the card is simply as tall as its contents.

       Kept as a wrapper rather than unwound, because the MTM forms slot still
       has to land between the notes and the actions, and it is the grouping
       that says which parts are the FORM and which are the commit. */
    overflow: visible;
}

/* Outside the scroll region, so nothing above it can push it off. */
.v2-ro-schedule { flex: 0 0 auto; }

/* ── the (i) ───────────────────────────────────────────────────────────────
   A CSS-only tooltip: the text is real markup, revealed on hover and on focus,
   so the keyboard and the pointer get the same thing and a screen reader has it
   through aria-describedby whether or not it is visible.

   Anchored to the RIGHT edge and opening downward, because this sits in a 300px
   column against the panel's right side — a left-anchored bubble would run off
   it. */
/* ── THE TOOLTIP IS ANCHORED TO THE TITLE, NOT TO THE (i) ──────────────────
   Daniel, 17-Sep: "the tooltip gets cut off?" … "all the tool tips do in fact"

   And I caused it. Giving .v2-ro-aside overflow-y: auto so the column could
   scroll made it a clipping ancestor, and there is no such thing as clipping one
   axis — overflow-y: auto computes the x axis to auto as well. Anything
   positioned outside the column's box is now cut, which is the same trap already
   written down for .v2-combo-list: a dropdown never escapes a clipping ancestor.

   The combo's answer was to make the clip conditional. That is not available
   here, because this clip IS the feature — it is what makes the column scroll.

   So instead of escaping the box, the tooltip is kept inside it. Anchoring to
   .v2-ro-card-title rather than to the (i) means the tooltip spans the card's
   inner width and CANNOT overflow horizontally, whatever the (i) sits next to —
   which also fixes it for a hint placed mid-title, like Basket's, where right: 0
   threw a 15rem box leftwards from a point 90px into a 300px column.

   .v2-ro-hint therefore stops being the containing block. It keeps its
   inline-flex so the glyph still sits on the text baseline. */
.v2-ro-hint { position: static; display: inline-flex; }

.v2-ro-hint-btn {
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-pill, 999px);
    background: none;
    color: var(--text-muted);
    cursor: help;
}

.v2-ro-hint-btn:hover { color: var(--text-strong); }

.v2-ro-hint-btn:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 1px;
}

.v2-ro-hint-text {
    position: absolute;
    top: calc(100% + 6px);
    /* BOTH edges, so the box is the card's width rather than its content's.
       width: max-content with a single anchored edge is what let it size itself
       and then hang off the side; pinning left AND right makes overflow
       impossible by construction instead of by choosing the right side to open
       towards — which would only ever be right for one of the two hints. */
    left: 0;
    right: 0;
    z-index: 5;
    padding: var(--space-2, 8px) var(--space-3, 12px);
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-button, 8px);
    background: var(--surface-raised, var(--surface-card));
    color: var(--text-body);
    font-size: 0.75rem;
    font-weight: 400;
    line-height: 1.4;
    box-shadow: 0 8px 20px -10px rgb(23 30 32 / 55%);
    /* ── display, NOT visibility ────────────────────────────────────────────
       This said "visibility, not display: it stays in the accessibility tree
       either way, and it can be transitioned". The second half was true and the
       first half was not — visibility: hidden prunes an element from the
       accessibility tree exactly as display: none does. Nothing was being
       bought, and something was being paid:

       A visibility-hidden box IS STILL LAID OUT, and an absolutely positioned
       one still contributes SCROLLABLE OVERFLOW to its nearest scroll
       container. This tooltip is 85px tall and sits below its trigger, so on the
       confirm dialog it claimed ten pixels the body could not give it — a
       scrollbar on a modal with half the viewport spare, which is exactly what
       Daniel reported on 21-Sep and what took three passes to find because
       nothing on screen was in the wrong place.

       It applies to every hint, not only that one: the basket's lead-time hint
       sits in a column that scrolls too, and was making the same claim.

       aria-describedby is unaffected either way — a referenced description is
       announced whether its target is display:none or visibility:hidden, which
       is the one thing hiding does NOT take away.

       THE COST, stated rather than glossed: display cannot be transitioned
       without allow-discrete, so the 140ms fade becomes an instant show on
       anything that does not support it. For a hover tooltip that is the
       ordinary behaviour, and it is a fair price for a dialog that stops
       scrolling for no reason. */
    display: none;
    opacity: 0;
    transition: opacity .14s ease;
}

.v2-ro-hint:hover .v2-ro-hint-text,
.v2-ro-hint:focus-within .v2-ro-hint-text {
    display: block;
    opacity: 1;
}

/* Both cards. A BORDER, not a fill — the third time this has been written down:
   --surface-card and the panel body's own surface are both Flint 600 in dark, so
   a filled card is painted the colour of the thing behind it and disappears.
   The edge works on any ground without the rule knowing which one it landed on. */
.v2-ro-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
    padding: var(--space-4, 16px);
    background: var(--surface-card);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-card, 12px);
}

.v2-ro-card-title {
    /* The containing block for .v2-ro-hint-text — see the note there. The
       tooltip spans this element, so it is the card's width and cannot reach
       the clip that .v2-ro-aside's scroll introduces. */
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    margin: 0;
    font-size: 1.125rem;
    font-weight: 700;
    color: var(--text-strong);
}

.v2-ro-card-sub,
.v2-ro-card-empty {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--text-muted);
}

/* Hidden at zero by the markup — "Basket 0" above "Your basket is empty" says
   the same thing twice. */
.v2-ro-basket-count {
    display: grid;
    place-items: center;
    /* HARD RIGHT on the title row. Daniel, 17-Sep: "then have the total in
       basket on the top right side". A running total belongs at the end of the
       line it totals; against the word "Basket" it read as part of the label
       rather than as a figure that moves. The (i) keeps its place beside the
       heading, because that annotates the word and not the number. */
    margin-left: auto;
    min-width: 1.5rem;
    height: 1.5rem;
    padding: 0 var(--space-1, 4px);
    border-radius: var(--radius-pill, 999px);
    background: var(--accent-request-order);
    color: var(--text-on-brand);
    font-size: 0.8125rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.v2-ro-basket-items { margin: 0; padding: 0; list-style: none; }

/* ── a basket line ─────────────────────────────────────────────────────────
   Name, stepper, remove. The name takes the room and gives it back first,
   because the controls are the part that must stay reachable in a 300px column
   however long a Haddenham product name turns out to be. */
/* ROOM TO READ. Daniel, 17-Sep: "i worry the products within are a little
   cramped." They were: 4px between a wrapping two-line product name, its
   variant, its PIP code and a row of controls, inside 8px of padding. Every
   part of a line was the same distance from every other part, so nothing
   grouped and the whole block read as one dense paragraph.

   The column can afford it now that it scrolls as a whole rather than fitting
   a fixed height — which is exactly what made 4px look necessary before. */
.v2-ro-line {
    display: flex;
    flex-direction: column;
    /* Back to 6/10 from 8/12. Daniel, 17-Sep: "the height is too much per
       card." Yesterday's 8/12 answered "a little cramped" and overshot; this
       sits between the two, and the extra 60px of column width is doing most of
       the real work by taking names off a second row. */
    gap: 6px;
    padding: 10px;
    margin-bottom: var(--space-2, 8px);
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card-raised);
    min-width: 0;
    font-size: 0.8125rem;
    color: var(--text-body);
}

/* ── THE REMOVE CONTROL IS NOT A HEAD BUTTON ───────────────────────────────
   Daniel, 17-Sep: "the X is rather large."

   It is — .v2-ro-icon-btn is a 30px bordered square, and it was drawn for the
   PANEL HEAD, where it sits alone beside a 16px title and needs an edge to aim
   at. On a basket line it sits beside a 13px name and is the least important
   thing in the row, yet it was the heaviest.

   So it loses the container here. The row already has an obvious hit area and
   the stepper beside it carries the visual weight; removing a line is the one
   action that should NOT invite the eye. */
.v2-ro-line .v2-ro-icon-btn {
    width: 22px;
    height: 22px;
    border-color: transparent;
    border-radius: var(--radius-sm, 6px);
}

/* AND THE TOUCH TARGET COMES BACK. components.css grows these to 44px below
   1024px, but that rule is .v2-ro-icon-btn — one class — and the rule above is
   two, so it would win at every width and quietly shrink a destructive control
   to 22px on the device where it is hardest to hit. Restated here at matching
   specificity, and placed AFTER the base rule so the media query is not the one
   that loses. Both halves of that are traps this file has been caught by
   before. */
@media (max-width: 1279px) {
    .v2-ro-line .v2-ro-icon-btn { width: 44px; height: 44px; }
}

.v2-ro-line:last-child { margin-bottom: 0; }

.v2-ro-line-head {
    display: flex;
    align-items: start;
    gap: var(--space-2, 8px);
    min-width: 0;
}

/* WRAPS RATHER THAN TRUNCATES. A basket line is the last chance to notice the
   wrong product, and "Haddenham Comfiwave (15-21mmhg) below knee" is exactly the
   kind of name where the distinguishing part is at the end. The catalogue card
   can ellipsise; this cannot. */
/* A ROW OF ITS OWN now, not a flex child of the head — the SKU shares that row
   with the controls instead. So no flex, and a paragraph's margin to clear. */
/* A ROW OF ITS OWN now, not a flex child of the head — the SKU shares that row
   with the controls instead. So no flex, and a paragraph's margin to clear.

   AND EXACTLY ONE ROW, as asked twice: "i need the product name to only be on
   one row", "i explicitely said 1 row only". The name element carries a title
   with the full string, because these products differ at the tail — "…10cm x 6m
   (singles)" against "…12cm x 6m" — and the ellipsis is therefore eating the
   part that tells them apart. Hover and the accessibility tree keep it. */
.v2-ro-line-name {
    margin: 0;
    min-width: 0;
    font-weight: 600;
    color: var(--text-strong);
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The chosen variant — Size, Model. classic colours these, and the colour is
   doing work: it separates what was CHOSEN from what the product is called. */
.v2-ro-line-attrs {
    margin: 0;
    font-size: 0.75rem;
    color: var(--accent-request-order);
}

/* ── AN OPTION BELONGS TO THE LINE ABOVE IT ────────────────────────────────
   Indented and set back a step, so a basket holding one configured garment
   reads as one thing with parts rather than as four products. The rail is what
   carries the relationship — an indent alone reads as a typo once the list
   scrolls and the base garment is off-screen. */
/* ── A LINER THE ORDER ADDED FOR YOU ───────────────────────────────────────
   Resolved from the wrap by the server rather than chosen, and free. It reads
   as quieter than the lines the nurse picked without being hard to read: it is
   still a device the patient receives and she has to be able to check it. */
.v2-ro-line--free {
    border-left: 2px solid var(--accent-request-order);
}

.v2-ro-free-tag {
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--accent-request-order);
}

/* ── A REMOVED LINER, TO ADD BACK (Jake, 21-Oct) ───────────────────────────
   NOT IN THE FRAMES — Jake asked for it to be tried and judged on screen. A
   quiet row where the liner was: no fill, a soft dashed 1px edge all round
   (Daniel: no left-border accents) and muted text, so it reads as an absence
   rather than a line. The button is the existing secondary, small. */
.v2-ro-line--removed {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 8px);
    background: transparent;
    border: 1px dashed var(--border-strong);
    color: var(--text-muted);
}

.v2-ro-removed-text {
    min-width: 0;
    font-size: 0.75rem;
}

/* Touch widths: a 44px target, as every control on a phone. */
@media (max-width: 1024px) {
    .v2-ro-add-back { min-height: 44px; }
}

/* ── A COMPOSED GARMENT IS A SYSTEM ────────────────────────────────────────
   Leads its section, because where one exists it is usually what the nurse
   wants — ordering the parts separately is the fallback, not the intent. The
   parts line tells her it is a system before she opens it. */
/* ── UPPER LIMB / LOWER LIMB ───────────────────────────────────────────────
   The limb step gets headings, so the grid stops laying the cards out and each
   group lays out its own — a grid cannot have a full-width heading between two
   of its tracks without the tracks being told to span, and one block per group
   says what it is more plainly than a span rule would. */
.v2-ro-product-grid.is-limb-split {
    display: block;
}

.v2-ro-limb-head {
    margin: var(--space-4, 16px) 0 var(--space-2, 8px);
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--text-muted);
}

.v2-ro-limb-head:first-child { margin-top: 0; }

/* The same track rule the grid itself uses, so a card is the same size either
   side of a heading and the split reads as grouping rather than as two
   different lists. */
.v2-ro-limb-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--space-3, 12px);
}

.v2-ro-comp-parts {
    font-size: 0.6875rem;
    color: var(--accent-request-order);
    font-weight: 600;
}

.v2-ro-line-opts {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent-request-order);
    font-size: 0.75rem;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
}

.v2-ro-line-opts:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
    border-radius: 3px;
}

/* The chevron points the way it will move: right while closed, down once the
   panel is open beneath it. */
.v2-ro-line-chev {
    display: inline-flex;
    transition: transform .14s ease;
}

.v2-ro-line-opts[aria-expanded="true"] .v2-ro-line-chev {
    transform: rotate(90deg);
}

.v2-ro-line-options {
    list-style: none;
    margin: 6px 0 0;
    padding: 6px 0 0;
    border-top: 1px solid var(--border-subtle, var(--border-medium));
}

.v2-ro-line-options li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2, 8px);
    padding: 2px 0;
    font-size: 0.75rem;
    min-width: 0;
}

/* One row each, like the product name above it — options differ at the tail
   too, and a wrapped one undoes the compactness the fold just bought. */
.v2-ro-line-option-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-body);
}

.v2-ro-line-option-qty {
    flex: 0 0 auto;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-line-chev { transition: none; }
}

/* ── WHAT IS ALREADY IN FLIGHT ─────────────────────────────────────────────
   A notice, not an error: the nurse may well have a good reason to place a
   second order, and this is here so the decision is informed rather than
   prevented. Warning tone, no red — nothing has gone wrong. */
.v2-ro-open-orders {
    margin: var(--space-3, 12px) 0 0;
    padding: var(--space-3, 12px);
    border: 1px solid var(--border-subtle, var(--border-medium));
    border-radius: var(--radius-button, 8px);
    /* ── SOFT, NOT ALARMED ─────────────────────────────────────────────────
       Daniel, 18-Sep: "make the info box less harsh too. it should be soft in
       light and dark mode."

       It was golden-hour-subtle on a warning border, which in dark resolves to
       golden-hour-900 — a heavy brown panel for something that is not a
       problem. Nothing has gone wrong here: the patient has orders, and placing
       another is frequently correct. So it takes the neutral inset surface,
       which is soft in both themes by construction, and the information does
       the work instead of the colour. */
    background: var(--surface-inset, transparent);
}

.v2-ro-open-orders-title {
    margin: 0 0 var(--space-2, 8px);
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-strong);
}

.v2-ro-open-orders ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 6px;
}

/* An order is a BLOCK now, not a row: its reference and status on top, its
   products stacked underneath. Daniel, 18-Sep — "could it stack the Products in
   the orders? for better visuals on whats inside??" Comma-joined, two products
   read as one long string that had to be parsed before it could be recognised,
   and the clip landed mid-name on exactly the orders with enough in them to
   matter. */
.v2-ro-open-orders > ul > li {
    display: block;
    min-width: 0;
}

/* Each order sits off the one above it, so the stack below a reference is
   visibly attached to that reference and not to the next one. */
.v2-ro-open-orders > ul > li + li {
    margin-top: var(--space-3, 12px);
    padding-top: var(--space-3, 12px);
    border-top: 1px solid var(--border-subtle, var(--border-medium));
}

.v2-ro-open-order-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
    font-size: 0.75rem;
    min-width: 0;
}

.v2-ro-open-order-items {
    list-style: none;
    margin: 4px 0 0;
    padding: 0;
    display: grid;
    gap: 2px;
}

/* A fixed quantity column, so the names start on one edge and the stack can be
   read down rather than across. auto sizes to the widest count present, which
   keeps a single-digit order from paying for a three-digit one. */
.v2-ro-open-order-items li {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-2, 8px);
    align-items: baseline;
    font-size: 0.75rem;
    min-width: 0;
}

.v2-ro-open-item-qty {
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    text-align: right;
}

/* One row each. These names differ at the tail — "…class 1 (18-21mmhg) seamless
   glove" — so a wrapped one costs two rows to say what the first row already
   distinguished; the full text is on the title. */
.v2-ro-open-item-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-body);
}

.v2-ro-open-item-more {
    display: block;
    color: var(--text-muted);
    font-size: 0.75rem;
}

/* Plain text on an order that cannot be opened, a link on one that can — so
   whether it is clickable is visible before it is clicked, rather than being
   something the nurse discovers by trying. */
.v2-ro-open-order-ref {
    font-weight: 600;
    color: var(--text-strong);
    font-variant-numeric: tabular-nums;
}

a.v2-ro-open-order-ref,
button.v2-ro-open-order-ref {
    color: var(--accent-request-order);
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* A button now — it opens the order in Brease's own edit rather than
   navigating to classic (Jake, 27-Oct) — dressed exactly as the link was. */
button.v2-ro-open-order-ref {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}

a.v2-ro-open-order-ref:hover,
button.v2-ro-open-order-ref:hover { text-decoration-thickness: 2px; }

button.v2-ro-open-order-ref:focus-visible,
a.v2-ro-open-order-ref:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
    border-radius: 3px;
}

.v2-ro-open-order-status {
    color: var(--text-muted);
}

/* The one-line, clipped version of the item list is gone — it is a stacked
   <ul> now, styled above. Left here as a note because this rule sat AFTER the
   new one and a later rule of equal specificity wins: the products would have
   gone back to a single clipped line with no error anywhere. */

/* The affirmative action in a modal whose default button reads as neutral.
   Named for what it DOES rather than for the panel it appears on, so the next
   dialog with a "carry on" button has one to reach for. */
.v2-button--go {
    background: var(--accent-request-order);
    color: var(--text-on-brand);
    font-weight: 600;
}

.v2-button--go:hover { filter: brightness(1.05); }

.v2-button--go:focus-visible {
    outline: 2px solid var(--text-strong);
    outline-offset: 2px;
}

/* ── THE LAST QUESTION BEFORE AN ORDER EXISTS ──────────────────────────────
   Two nested questions in one dialog, so the nesting has to read as nesting:
   the method only exists because the request toggle is off, and the barcodes
   only because the method is EPS. Each indent step is a consequence of the
   answer above it. */
/* The reference is the point of this screen, so it is the size of the point. */
.v2-ro-placed-ref {
    margin: 0 0 var(--space-2, 8px);
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--accent-request-order);
    font-variant-numeric: tabular-nums;
}

/* ── WHAT HAPPENS NEXT ─────────────────────────────────────────────────────
   Daniel's copy, 20-Sep. Body text under the reference: the reference is the
   thing to quote, these are the things to know. */
.v2-ro-placed-thanks {
    margin: var(--space-2, 8px) 0 0;
    font-size: 0.875rem;
    color: var(--text-body);
}

/* A list, because they are three separate facts and a paragraph would make a
   nurse read all of it to find the time. */
.v2-ro-placed-facts {
    margin: var(--space-3, 12px) 0 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: var(--space-2, 8px);
    font-size: 0.8125rem;
    color: var(--text-muted);
}

.v2-ro-placed-facts li[hidden] { display: none; }

/* A button that reads as a link, because it IS a navigation — the shell opens
   the panel off data-v2-overlay, and making it an <a> would need an href that
   is not where it goes. */
.v2-ro-placed-link {
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent-request-order);
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
}

.v2-ro-placed-link:hover { filter: brightness(1.1); }

.v2-ro-placed-link:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

/* Three across at this width would give each about a hundred pixels, so they
   share the row and wrap rather than being capped at 260 like a pair. */
.v2-ro-placed-actions > .v2-button { max-width: none; }

/* ── TWO COLUMNS ───────────────────────────────────────────────────────────
   Daniel, 20-Sep: "left side is patient and address information? editable ,
   post code look up etc / right side is the product information and the options
   for the order?"

   A real division rather than a way to use the width: the left column is the
   DESTINATION and everything in it edits something about a person; the right is
   the ORDER and what happens to it afterwards. Reviewing is then two short
   passes instead of one long scroll, which is the whole argument for a wide
   dialog here.

   auto-fit with a floor, so it collapses to one column on a narrow screen in
   source order — who, where, then what — which is also the order it is checked
   in. */
/* ── AND THE DIALOG NEEDS A REAL WIDTH FOR THEM ────────────────────────────
   Daniel, 20-Sep: "still 1 column, not 2 as asked." It was, and the columns
   were never going to appear.

   .v2-modal-panel is `width: fit-content` with max-width as a CEILING — which
   is right for a confirmation that should be allowed to stay small. But a grid
   inside a fit-content parent has no definite width to divide, so auto-fit
   resolves to one column, the panel then shrinks to that, and the two agree on
   the wrong answer. Chicken and egg, and no amount of minmax() tuning escapes
   it.

   So this dialog asks for a width rather than inheriting one. Scoped to the
   confirmation, because every other modal genuinely should size to its
   content. */
.v2-modal[data-v2-modal="ro-confirm"] .v2-modal-panel {
    width: min(92vw, 760px);
}

.v2-ro-cf-cols {
    display: grid;
    gap: 0 var(--space-5, 20px);
    align-items: start;
}

/* Two explicit columns above the breakpoint rather than auto-fit: with a
   definite width there is nothing to discover, and an explicit rule cannot
   silently collapse again. */
/* 641, not 640: this stylesheet's phone rules are max-width 640, so at exactly
   640 both shapes applied at once (2-Oct review). */
@media (min-width: 641px) {
    /* ── 50/50 ─────────────────────────────────────────────────────────────
       Daniel, 20-Sep: "should be 50/50 for a start".

       It was 1.25fr to give the address form room. That room comes from
       somewhere better now: the address card COLLAPSES into the form rather
       than sitting above it, so the column never has to hold both at once and
       an even split is enough. */
    .v2-ro-cf-cols {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }

    /* The rule between them belongs to the layout, not to either column, so it
       is drawn here and disappears with the second column. */
    .v2-ro-cf-col + .v2-ro-cf-col {
        padding-left: var(--space-5, 20px);
        border-left: 1px solid var(--border-subtle, var(--border-medium));
    }
}

/* ── THE CONFIRM DIALOG'S SECTIONS ─────────────────────────────────────────
   Classic's order — who, where, what, then the two questions that change what
   happens afterwards. Separated by rules rather than cards: this is one
   document being read, and a box per fact would make it several. */
.v2-ro-cf-block {
    padding: var(--space-4, 16px) 0;
    border-bottom: 1px solid var(--border-subtle, var(--border-medium));
}

.v2-ro-cf-col > .v2-ro-cf-block:first-child { padding-top: 0; }
.v2-ro-cf-col > .v2-ro-cf-block:last-child,
.v2-ro-cf-block--last { border-bottom: 0; }

/* ── THE LAST CARD IN EACH COLUMN ENDS ON THE SAME LINE ────────────────────
   Daniel, 20-Sep: "can we make the card for the delivery address match the
   after this order card in length."

   The columns are already the same height — the grid stretches them — so the
   left one simply stopped early and left its remainder as dead space below the
   address. Handing that remainder to the address block puts the two cards on
   one bottom edge, and both blocks carry the same 16px of padding under the
   card, so they agree without either being measured against the other.

   It follows whichever of the two is showing: the card, or the form the card
   collapses into when Change is pressed. */
.v2-ro-cf-block--fill {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
}

.v2-ro-cf-block--fill > .v2-ro-cf-card,
.v2-ro-cf-block--fill > .v2-ro-cf-picker:not([hidden]) {
    flex: 1 1 auto;
}

/* The picker's own bottom margin would push it 12px clear of the edge the card
   lands on, so on the filling block it goes. */
.v2-ro-cf-block--fill > .v2-ro-cf-picker { margin-bottom: 0; }

/* ── A FACT, IN A CARD ─────────────────────────────────────────────────────
   The patient, the address and the date are the three things being confirmed,
   so each sits on its own surface rather than as loose text — the rest of v2
   uses the same inset card for a fact that has been settled. */
/* ── ONE SURFACE, ONE DIFFERENTIATOR ───────────────────────────────────────
   Daniel, 20-Sep: "its very inconsistent", and then "i like the speration
   though. no boxes make the sepration merky."

   Both are true and the resolution is not fewer boxes, it is fewer TREATMENTS.
   Every fact card is the same surface; the address is told apart by its border
   alone rather than by a second fill. One thing varies, for one reason: the
   address is the only card here that can be changed.

   The white inputs are deliberately outside this scheme. .v2-field is pinned
   white in BOTH themes by design — a field is a field and a card is a card, and
   they only meet inside Change, where the white is the signal that something is
   editable. */
/* ── THE BASKET'S OWN RELATIONSHIP ─────────────────────────────────────────
   Daniel, 20-Sep: "i think we should use the 2 greys like the basket does with
   the darker grey? light grey panel with darker grey cards."

   --surface-card is what .v2-ro-card uses, and in dark it resolves to flint-600
   against a flint-500 page: the card is DARKER than the ground it sits on. That
   is the relationship the basket has, and the one being pointed at.

   Two wrong answers preceded it. --surface-inset is the RECESSED step, for a
   well set into a card, which read as a flat grey block. --surface-raised is
   flint-400 — LIGHTER than the page, so the cards lifted off it instead of
   sinking into it, which is right for a floating panel and the inverse of
   everything beside it here. */
.v2-ro-cf-card {
    padding: var(--space-3, 12px);
    border: 1px solid var(--border-subtle, var(--border-medium));
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card);
}

/* The one that can change. Border only — same ground as its neighbours, so the
   accent reads as "this is the editable one" rather than as a second kind of
   card. */
.v2-ro-cf-card--accent {
    border-color: var(--accent-request-order);
}

/* ── THE CARD FILLS THE SPACE IT WAS GIVEN ─────────────────────────────────
   Daniel, 20-Sep: "we should better fill the space we have now for the
   delivery address? feels empty."

   It was: the card grew to meet the right column's bottom edge and the four
   lines stayed pooled at the top. A column with the address at the head and
   its provenance on the bottom edge reads as a card with structure; the same
   card with everything at the top reads as one that ran out of things to say.

   The gap is pushed by the source line rather than by a height on anything, so
   a long address, an instruction line, or no space to spare all still land
   correctly. */
.v2-ro-cf-card--accent {
    display: flex;
    flex-direction: column;
}

.v2-ro-cf-addr-src {
    /* auto pushes it to the bottom edge whatever is above it, and there is at
       least space-3 of clearance because the padding below the rule pairs with
       the address block's own trailing line. */
    margin: auto 0 0;
    padding-top: var(--space-2, 8px);
    border-top: 1px solid var(--border-subtle, var(--border-medium));
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--text-muted);
}

/* The instructions that came WITH the address, read back on it. */
.v2-ro-cf-addr-note {
    margin: var(--space-2, 8px) 0 0;
    padding-top: var(--space-2, 8px);
    border-top: 1px dashed var(--border-subtle, var(--border-medium));
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* ── THE ACTIONS FOLLOW THE CONTENT ────────────────────────────────────────
   Daniel, 20-Sep: "cancel and place order would be side by side not stacked.
   and below the content not at the bottom of the modal."

   They were pushed to the bottom of the column, which on a short order left
   them floating under a stretch of nothing. Directly under the last question
   they read as the end of it.

   Side by side because at half width a stacked pair is a column of buttons
   rather than a choice — and equal widths, because Cancel is a real option
   here and sizing it to its label would make it look like an afterthought. */
.v2-ro-cf-actions {
    display: flex;
    flex-direction: row;
    gap: var(--space-2, 8px);
    padding-top: var(--space-4, 16px);
}

.v2-ro-cf-actions > .v2-button {
    flex: 1 1 0;
    min-width: 0;
    max-width: none;
}

.v2-ro-cf-col {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* Both columns take the row's full height, which is what lets margin-top: auto
   above have something to push against. */
@media (min-width: 641px) {
    .v2-ro-cf-cols { align-items: stretch; }
}

.v2-ro-cf-addr-name {
    margin: 0 0 2px;
    font-size: 0.8125rem;
    font-weight: 700;
    color: var(--accent-request-order);
}

.v2-ro-cf-addr-name:empty { display: none; }

/* A line per line, as a delivery label reads. Comma-joining an address into one
   sentence is how a nurse stops seeing the postcode. */
.v2-ro-cf-addr-line {
    margin: 0;
    font-size: 0.8125rem;
    line-height: 1.45;
    color: var(--text-body);
}

/* ── THE FIGURES ───────────────────────────────────────────────────────────
   Not a product list — the basket is two feet away and has been read. What
   belongs here is whether the totals match what she thinks she built, so the
   numbers are the size of the answer. */
.v2-ro-cf-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr));
    gap: var(--space-2, 8px);
}

/* A figure that OPENS. It is a button, so it says so: the border lifts to the
   accent on hover and it keeps a real focus ring.

   ── ONE LINE, NOT A STACK ─────────────────────────────────────────────────
   Daniel, 20-Sep: "Maybe Items: 1 instead of stacked".

   Stacked, each figure was a little monument to itself, and the extra line ran
   the right column's first rule well below the left's — which is the
   misalignment he spotted. Read across, it is a fact with a number on the end,
   which is all it ever was.

   Wrapping stays allowed, because "made to measure" is written out in full here
   as it is everywhere else a nurse reads it. The shared height below absorbs
   that second line without moving the rule. */
.v2-ro-cf-stat {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 2px var(--space-2, 8px);
    padding: var(--space-2, 8px);
    border: 1px solid var(--border-subtle, var(--border-medium));
    border-radius: var(--radius-button, 8px);
    /* The same card surface as everything else on this screen — it was
       transparent, which made it the one box on the dialog sitting directly on
       the page ground. */
    background: var(--surface-card);
    font: inherit;
    text-align: center;
    cursor: pointer;
    transition: border-color .14s ease;
}

.v2-ro-cf-stat:hover { border-color: var(--accent-request-order); }

.v2-ro-cf-stat:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

/* ── THE TWO COLUMNS START LEVEL ───────────────────────────────────────────
   Daniel, 20-Sep: "The sperator line below the patient name and the order
   summary should match? if that means either the name field gets wider or the
   stats card ges thinner?"

   Both first blocks carry the same head, the same padding and the same rule, so
   the only thing that ever put those two lines at different heights was the
   card between them: the name is one line, the stacked figure was two. Naming
   one height and holding both cards to it makes the rules agree BY
   CONSTRUCTION rather than by two paddings happening to add up — nothing has to
   be re-tuned when a figure grows a digit or a label is reworded.

   A minimum, not a fixed height: a name long enough to wrap, or a third figure
   whose label runs to two lines, is a genuine difference and is allowed to
   show. It is set above both cards' natural single-line height so that in the
   ordinary case the minimum is what wins. */
.v2-ro-cf-ident,
.v2-ro-cf-stat {
    min-height: 3.25rem;
}

/* ── THE DETAIL, IN THE FIGURES' PLACE ─────────────────────────────────────
   Not underneath them. The section is the same height either way, so opening a
   detail never moves the questions below it — which is the whole reason this is
   a swap rather than an expansion. */
.v2-ro-cf-detail-list {
    list-style: none;
    margin: var(--space-2, 8px) 0 0;
    padding: 0;
    display: grid;
    gap: 3px;
    max-height: 9.5rem;
    overflow-y: auto;
}

.v2-ro-cf-detail-list li {
    display: grid;
    grid-template-columns: 2rem minmax(0, 1fr) auto;
    gap: var(--space-2, 8px);
    align-items: baseline;
    font-size: 0.75rem;
    min-width: 0;
}

/* A files list has no quantity column, so its name starts where the numbers
   would — one rule, both shapes. */
.v2-ro-cf-detail-list li:not(:has(.v2-ro-cf-detail-qty)) {
    grid-template-columns: minmax(0, 1fr) auto;
}

.v2-ro-cf-detail-qty {
    text-align: right;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.v2-ro-cf-detail-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-body);
}

.v2-ro-cf-detail-note {
    font-size: 0.625rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--text-muted);
}

.v2-ro-cf-stat-n {
    font-size: 1.375rem;
    font-weight: 700;
    line-height: 1;
    color: var(--text-strong);
    font-variant-numeric: tabular-nums;
}

.v2-ro-cf-stat-l {
    font-size: 0.625rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--text-muted);
}

/* ── A DECISION WITH A SWITCH ──────────────────────────────────────────────
   Label and explanation on the left, the control hard right, which is how
   classic reads and how a setting reads anywhere. The sub-line is doing real
   work in both cases: "the patient confirms before each one is placed" is what
   makes an automatic repeat safe to agree to. */
.v2-ro-cf-row {
    /* THE CONTAINING BLOCK FOR .v2-ro-hint-text, the same job .v2-ro-card-title
       does for the basket's hint: the tooltip pins left: 0 / right: 0, so it
       takes the width of whichever positioned ancestor it finds. On the row it
       is the row's width and opens directly under the control that carries it;
       without this it would measure against the dialog and hang off the card. */
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3, 12px);
    padding: var(--space-2, 8px) 0;
}

.v2-ro-cf-row-label {
    display: grid;
    gap: 2px;
    min-width: 0;
}

.v2-ro-cf-row-title {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-strong);
}

.v2-ro-cf-row-sub {
    font-size: 0.6875rem;
    color: var(--text-muted);
}

/* A switch, not a checkbox: these are on/off states of the order rather than
   items being ticked off a list, and classic already reads them that way. */
.v2-ro-switch {
    flex: 0 0 auto;
    display: inline-flex;
    cursor: pointer;
}

.v2-ro-switch input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.v2-ro-switch-track {
    display: block;
    width: 40px;
    height: 22px;
    border-radius: 999px;
    background: var(--border-medium);
    transition: background-color .16s ease;
    position: relative;
}

.v2-ro-switch-track::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #fff;
    transition: transform .16s ease;
}

/* A repeat an edit can't change (15-Oct): the switch shows it, faded. */
.v2-ro-switch input:disabled + .v2-ro-switch-track { opacity: 0.5; cursor: default; }

.v2-ro-switch input:checked + .v2-ro-switch-track {
    background: var(--accent-request-order);
}

.v2-ro-switch input:checked + .v2-ro-switch-track::after {
    transform: translateX(18px);
}

.v2-ro-switch input:focus-visible + .v2-ro-switch-track {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-switch-track,
    .v2-ro-switch-track::after { transition: none; }
}

.v2-ro-cf-new-head {
    margin: 0 0 var(--space-2, 8px);
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--text-muted);
}

.v2-ro-cf-result-text {
    display: block;
    font-weight: 600;
}

.v2-ro-cf-result-sub {
    display: block;
    font-size: 0.6875rem;
    color: var(--text-muted);
}

.v2-ro-cf-head {
    margin: 0 0 var(--space-2, 8px);
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--text-muted);
}

.v2-ro-cf-head-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2, 8px);
}

.v2-ro-cf-head-row .v2-ro-cf-head { margin-bottom: var(--space-2, 8px); }

.v2-ro-cf-name {
    margin: 0;
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--text-strong);
}

/* ── NAME | NHS NUMBER ─────────────────────────────────────────────────────
   The pair the patient card and My Patients already show, on one line with a
   rule between them. Wraps rather than truncating: a name and an NHS number
   are both things you read in full or not at all. */
.v2-ro-cf-ident {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
}

.v2-ro-cf-ident .v2-ro-cf-sub { margin: 0; }

.v2-ro-cf-ident-sep {
    width: 1px;
    align-self: stretch;
    min-height: 1em;
    background: var(--border-medium);
}

.v2-ro-cf-sub {
    margin: 2px 0 0;
    font-size: 0.75rem;
    color: var(--text-muted);
}

.v2-ro-cf-addr {
    margin: 0 0 var(--space-3, 12px);
    font-size: 0.8125rem;
    color: var(--text-body);
}

/* Change and Change date: a link in weight, a button in behaviour. */
.v2-ro-cf-change {
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent-request-order);
    font: inherit;
    font-size: 0.75rem;
    font-weight: 600;
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.v2-ro-cf-change:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
    border-radius: 3px;
}

/* ── THE ADDRESS CHOOSER ───────────────────────────────────────────────────
   Inline rather than a second dialog: two or three saved addresses and four
   fields is not worth a modal over a modal, and opening it by mistake must not
   cost the confirmation being read. */
.v2-ro-cf-picker {
    margin: 0 0 var(--space-3, 12px);
    padding: var(--space-3, 12px);
    border: 1px solid var(--border-subtle, var(--border-medium));
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card);
}

/* ── THE PATIENT'S ADDRESSES, BEHIND ONE BUTTON ────────────────────────────
   Daniel, 20-Sep: "when its just stacking the address its stretching the modal
   really long again. maybe a Saved address button with a drop down overlay
   instead? i like that?"

   Three addresses inline pushed Place order off the bottom of a dialog whose
   whole job is to be answered. Open, the list costs the column nothing — which
   is the entire reason dropdowns exist and the same argument the postcode
   results already won on this screen.

   Positioned, so the list hangs off the button rather than pushing the form
   down. The list is a CHILD of this, never a sibling: a sibling resolves its
   absolute position against whatever is positioned further up and lands
   somewhere else entirely. */
.v2-ro-cf-pick {
    position: relative;
    margin-bottom: var(--space-3, 12px);
}

/* Reads as a control, not as a field: it opens something rather than taking
   anything typed, so it takes the card surface the facts use rather than the
   white .v2-field. */
.v2-ro-cf-pick-btn {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 8px);
    width: 100%;
    padding: var(--space-2, 8px) var(--space-3, 12px);
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card);
    color: var(--text-body);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}

.v2-ro-cf-pick-btn:hover { border-color: var(--accent-request-order); }

.v2-ro-cf-pick-btn[hidden] { display: none; }

.v2-ro-cf-pick-btn svg { flex: none; transition: transform .14s ease; }

.v2-ro-cf-pick-btn[aria-expanded="true"] {
    border-color: var(--accent-request-order);
}

.v2-ro-cf-pick-btn[aria-expanded="true"] svg { transform: rotate(180deg); }

/* THE LIST ITSELF, now an overlay. `display: grid` is an AUTHOR declaration and
   beats the browser's own [hidden] { display: none }, so hidden has to be
   restated or the dropdown is never shut — the same trap .v2-combo-list and
   .v2-form-collapse each restate for themselves, since nothing in v2 declares
   it once globally. */
.v2-ro-cf-saved {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    z-index: 6;
    display: grid;
    gap: var(--space-2, 8px);
    /* ── NO CAP, BECAUSE THE CONTENT HAS ONE ───────────────────────────────
       Daniel, 22-Sep: "allow the drop down room to display the 3 options and
       the helper text. it gets lost in the drop down if you dont scroll down to
       find it."

       This carried max-height: 13rem and a scrollbar, copied from the dropdowns
       that genuinely need one — the postcode lookup can return twenty results
       and the catalogue's combo can hold forty manufacturers. This list cannot:
       ajax-patient-addresses caps a patient at the record address plus two, so
       it is three rows at the very most and always has been.

       A scroll region over content that can never overflow does nothing except
       hide whatever is last, which here was the line saying where these are
       managed — a note nobody would think to scroll for because nothing
       suggests there is anything below.

       If the cap ever moves, this is the rule that moves with it. */
    padding: var(--space-2, 8px);
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card-raised, var(--surface-card));
    box-shadow: 0 10px 24px rgba(0, 0, 0, .18);
}

.v2-ro-cf-saved[hidden] { display: none; }

/* Each saved address is one target carrying two lines — the label the patient
   gave it, and the address, because two can share a label. */
.v2-ro-cf-saved-btn {
    display: grid;
    gap: 2px;
    width: 100%;
    padding: var(--space-2, 8px) var(--space-3, 12px);
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.v2-ro-cf-saved-btn:hover { border-color: var(--accent-request-order); }

.v2-ro-cf-saved-label {
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--text-strong);
}

.v2-ro-cf-saved-line {
    font-size: 0.75rem;
    color: var(--text-muted);
}

.v2-ro-cf-new {
    display: grid;
    gap: var(--space-2, 8px);
    margin-top: var(--space-3, 12px);
}

/* ── SAVE THIS ADDRESS TO THE PATIENT ──────────────────────────────────────
   A checkbox in a column of text fields, so it takes the row treatment rather
   than the field one: no white fill, because it is not somewhere you type. */
.v2-ro-cf-save {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: 2px 0;
    color: var(--text-body);
    font-size: 0.8125rem;
    cursor: pointer;
}

.v2-ro-cf-save input { flex: none; cursor: pointer; }

/* The script hides this whole row when the patient's two address slots are
   full, and display: flex would have beaten that — so the offer to save a
   third would have shown and only failed at the server. */
.v2-ro-cf-save[hidden] { display: none; }

/* The currently chosen address in the list, marked rather than removed — a
   list that hides the thing you are on makes you wonder where it went. */
.v2-ro-cf-saved-btn[aria-current="true"] {
    border-color: var(--accent-request-order);
}

/* Positioned, so the results hang off it rather than pushing the fields below
   it down the column. */
.v2-ro-cf-pc-row {
    position: relative;
    display: flex;
    gap: var(--space-2, 8px);
}

.v2-ro-cf-pc-row .v2-field { flex: 1 1 auto; min-width: 0; width: 100%; }

/* ── AND THE LOOKUP LOOKS LIKE ONE ─────────────────────────────────────────
   Daniel, 20-Sep: "also worth making the look up a little more obvious than
   just a standard text field?"

   It was four identical white boxes in a column and only the first of them did
   anything when you typed. The magnifier says which one searches before the
   clinician discovers it by typing into the wrong box.

   Absolutely placed and click-through, so the icon never steals the press that
   was meant for the field it sits in. .v2-field is pinned white in both themes
   by design, so the glyph takes a fixed dark muted value rather than the
   theme's --text-muted, which would go pale on a white box in dark mode. */
.v2-ro-cf-pc-icon {
    position: absolute;
    top: 50%;
    left: 10px;
    z-index: 1;
    transform: translateY(-50%);
    color: #6B7B82;
    pointer-events: none;
}

.v2-ro-cf-pc-row .v2-ro-cf-pc { padding-left: 32px; }

/* ── THE LOOKUP FLOATS ─────────────────────────────────────────────────────
   Daniel, 20-Sep: "make the post code lookup an overlay not inset? because the
   submit button spans all the way off modal requiring a scroll over dead
   space."

   Inset, a postcode with thirty houses on it pushed the rest of the column down
   by three hundred pixels — so Place order left the dialog and the nurse
   scrolled past empty space to reach it. A dropdown should cost nothing while
   it is open, which is the whole reason dropdowns overlay.

   On its own surface with a shadow, because it now sits OVER content and has to
   read as a layer rather than as more of the form. */
.v2-ro-cf-results {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    z-index: 5;
    display: grid;
    gap: 2px;
    max-height: 13rem;
    overflow-y: auto;
    padding: 4px;
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card-raised, var(--surface-card));
    box-shadow: 0 10px 24px rgba(0, 0, 0, .18);
}

/* Restated for the same reason .v2-ro-cf-saved restates it: display: grid
   above is an author declaration and beats the browser's own [hidden]. */
.v2-ro-cf-results[hidden] { display: none; }

.v2-ro-cf-result {
    padding: 5px 8px;
    border: 0;
    border-radius: var(--radius-button, 8px);
    background: none;
    color: var(--text-body);
    font: inherit;
    font-size: 0.75rem;
    text-align: left;
    cursor: pointer;
}

.v2-ro-cf-result:hover { background: var(--surface-card); }

/* ── THE TWO TOGGLES ───────────────────────────────────────────────────────
   Prescription and repeat. Both are yes/no decisions about what happens AFTER
   this order, so they read as a pair rather than as more fields. */
.v2-ro-cf-toggle { margin: var(--space-2, 8px) 0; }

/* ── A BLOCK SET APART BY A BORDER ALL THE WAY ROUND ───────────────────────
   Daniel, 21-Sep: "i dont like that left side sperator for no reason".

   It was a 2px rule down the left, which is a house rule broken rather than a
   taste: a left-border accent is an AI tell, and a block that belongs to the
   control above it is set apart with a soft 1px border all the way round.

   The NHS note is NOT in here — it rides in the row title above, where the
   decision is made, and .v2-ro-cf-row is its containing block. */
.v2-ro-cf-repeat {
    margin: var(--space-2, 8px) 0 0;
    padding: var(--space-3, 12px);
    border: 1px solid var(--border-subtle, var(--border-medium));
    border-radius: var(--radius-button, 8px);
}

/* ── THE INTERVAL SLIDER ───────────────────────────────────────────────────
   classic's, and the third way of naming one number: presets for the common
   answer, the type-in for an exact one, this for finding a number by feel. */
/* One row: the low end, the track, the high end. The ends are auto tracks so
   the slider takes everything left over whatever the bounds turn out to be —
   "1" and "12" are different widths and the track should not know. */
.v2-ro-cf-slider {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-2, 8px);
    margin-top: var(--space-3, 12px);
}

.v2-ro-cf-slider-input {
    width: 100%;
    min-width: 0;
    margin: 0;
    accent-color: var(--accent-request-order);
    cursor: pointer;
}

.v2-ro-cf-slider-end {
    font-size: 0.6875rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

/* ── THE NHS NOTE, AS A MARK RATHER THAN A PARAGRAPH ───────────────────────
   VISIBILITY, never display or [hidden]. It sits in the row title at every
   interval and only becomes visible under six months, so the card is the same
   height whatever is chosen — which is the whole fix for "the helper text
   appears and makes the address field span". The two columns are stretched to
   a shared height on purpose, so anything that changes this column's height
   changes the address card's with it. */
.v2-ro-hint--warn {
    visibility: hidden;
    margin-left: 2px;
    vertical-align: -3px;
}

.v2-ro-hint--warn .v2-ro-hint-btn { color: var(--intent-warning-text); }

.v2-ro-hint--warn .v2-ro-hint-btn:hover { color: var(--intent-warning-text); }

/* ONE mechanism, a class on the hint itself. A second selector keyed off an
   ancestor would be a second answer to one question, and the two would
   eventually disagree about when the note applies. */
.v2-ro-hint--warn.is-on { visibility: visible; }

.v2-ro-cf-every {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
    margin: var(--space-1, 4px) 0 var(--space-2, 8px);
}

.v2-ro-cf-every-btn {
    padding: 5px 12px;
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-pill, 999px);
    background: transparent;
    color: var(--text-body);
    font: inherit;
    font-size: 0.75rem;
    font-weight: 600;
    cursor: pointer;
}

.v2-ro-cf-every-btn.is-on {
    background: var(--accent-request-order);
    border-color: var(--accent-request-order);
    color: var(--text-on-brand);
}

/* ── A NUMBER THAT IS NOT ONE OF THE PRESETS ───────────────────────────────
   classic's "#". Dashed while it is only an invitation, and it takes the solid
   accent like any other button once it holds the chosen value — because at that
   point it IS the choice, not a way to make one. */
.v2-ro-cf-every-custom {
    border-style: dashed;
    color: var(--text-muted);
}

.v2-ro-cf-every-custom.is-on {
    border-style: solid;
    color: var(--text-on-brand);
}

/* The type-in, on its own line under the row. "Every [ 4 ] months" reads as the
   sentence the presets are shorthand for, so the unit is never in doubt — 4
   alone could be weeks to somebody scanning. */
.v2-ro-cf-custom {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    margin-top: var(--space-2, 8px);
    font-size: 0.75rem;
    color: var(--text-body);
}

.v2-ro-cf-custom[hidden] { display: none; }

.v2-ro-cf-custom-input {
    /* Wide enough for two digits and no wider: the bounds cap at 12 months, and
       a full-width field would read as somewhere to type a sentence. */
    width: 5.5rem;
    flex: 0 0 auto;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* ── "YOU ARE CHANGING ORDER NP12345" ──────────────────────────────────────
   The strip at the top of the aside while an existing order is loaded. An edit
   repopulates this screen, so it looks exactly like a new order — and the one
   thing a nurse cannot be unsure about is whether the button at the bottom
   writes a second order or changes the one she came from.

   Harbour, the same colour the Edit control itself wears on the order panel, so
   the strip reads as a continuation of the thing she just pressed rather than
   as a warning. Not the danger tone: nothing has gone wrong.

   [hidden] is honoured EXPLICITLY. An author `display` beats the browser's own
   [hidden] rule, and this panel has paid for that four times — see the note on
   .v2-ro-mtm-topqty. */
.v2-ro-editing-note {
    margin: 0 0 var(--space-3, 12px);
    padding: var(--space-2, 8px) var(--space-3, 12px);
    /* One soft border all the way round. The first version wore a 3px harbour
       rule down its left, which is the tell Daniel named on 21-Sep — a block is
       set apart by a border, not by a stripe. */
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-md, 8px);
    background: var(--surface-raised);
    font-size: 0.75rem;
    line-height: 1.45;
    color: var(--text-body);
}

.v2-ro-editing-note[hidden] { display: none; }

/* The NHS-allowance note, shown only under six months — a consequence of the
   choice just made rather than standing boilerplate. */
.v2-ro-cf-warn {
    margin: 0 0 var(--space-2, 8px);
    font-size: 0.6875rem;
    color: var(--intent-warning-text);
}

/* ── THE CALENDAR ──────────────────────────────────────────────────────────
   Presets first — scheduling the next cycle is the common case — with the grid
   underneath for anything else. */
/* ── ONE ROW, AND A GRID RATHER THAN A FLEX ────────────────────────────────
   Daniel, 21-Sep: "can we make the 12 months go onto the same row as the
   others please. no flex."

   Wrapping was the whole problem: four chips sized by their own text, and "12
   months" is the widest of them, so it was the one that fell off the end and
   sat alone on a second row. A grid of four equal tracks sizes them to the
   ROW instead of to their labels — they cannot wrap because there is nowhere
   to wrap to, and they line up as a set of four alternatives rather than as a
   sentence that ran out of space. */
.v2-ro-cal-quick {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-2, 8px);
    margin-bottom: var(--space-4, 16px);
}

.v2-ro-cal-quick-btn {
    /* The track is the width now. min-width: 0 so a long label ellipses inside
       its share rather than pushing the track wider and breaking the four. */
    min-width: 0;
    padding: 6px 10px;
    text-align: center;
    white-space: nowrap;
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-pill, 999px);
    background: transparent;
    color: var(--text-body);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
}

.v2-ro-cal-quick-btn:hover {
    border-color: var(--accent-request-order);
    color: var(--text-strong);
}

/* ── THE CHOSEN INTERVAL ───────────────────────────────────────────────────
   Daniel, 20-Sep: "when clikcing on a timeframe (1,2,6,12 months) should it
   not have an active state?"

   Filled, as Justin's frame fills it. It takes the panel's own accent rather
   than a new colour, and :hover is excluded below so the chosen chip does not
   change under the pointer — it is already at its final state and there is
   nothing for hover to add. Same reasoning as the selected nav item. */
.v2-ro-cal-quick-btn[aria-pressed="true"],
.v2-ro-cal-quick-btn[aria-pressed="true"]:hover {
    border-color: var(--accent-request-order);
    background: var(--accent-request-order);
    color: var(--text-on-brand);
}

/* RAISED ON THE SHELL'S GLASS. The border is gone because .v2-glass draws a
   gradient rim in its place — a flat hairline underneath it reads as a second,
   duller outline on the same edge. Radius stays here so the rim, which inherits
   it, follows the right shape. */
.v2-ro-cal {
    padding: var(--space-3, 12px);
    border-radius: var(--radius-card, 12px);
    /* ── AND A SHADOW THAT DOES NOT GET CUT ────────────────────────────────
       Daniel, 20-Sep: "just below the schedule calander is a shadow effect
       that appears to have a blocky effect".

       --glass-shadow is the DOCK's, and its widest stop spreads 119px. The
       dock floats on a wallpaper with room all round it; the calendar sits
       inside .v2-modal-body, which scrolls — so the shadow was being clipped
       at that box's edge and the cut showed as a hard rectangular band.

       The two CONTACT stops of the same five survive that box, so the shadow
       is still the file's values, just the part of it that belongs on a card
       nested inside a panel rather than on one floating over a photograph. */
    box-shadow:
        0 19px 19px rgb(0 0 0 / 12%),
        0 5px 10px rgb(0 0 0 / 14%);
}

/* .v2-glass's no-backdrop-filter fallback is a 62% FLINT wash — written for the
   dock, which sits on a photograph and has to mute it in both themes. Inherited
   here it would paint the calendar dark inside a light modal, so this one takes
   the theme's own raised surface instead. Unreachable on every browser we
   target; wrong in a way nothing would report, which is why it is stated. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .v2-ro-cal { background-color: var(--surface-raised); }
}

.v2-ro-cal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 8px);
    margin-bottom: var(--space-3, 12px);
}

.v2-ro-cal-month {
    font-size: 0.875rem;
    font-weight: 700;
    color: var(--text-strong);
}

.v2-ro-cal-nav {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-button, 8px);
    background: none;
    color: var(--text-body);
    cursor: pointer;
}

.v2-ro-cal-nav:hover:not(:disabled) { border-color: var(--accent-request-order); }

/* At the edge of the range. Dimmed rather than removed, so the control stays
   where the eye expects it and the calendar reads as bounded rather than as
   having lost a button. */
.v2-ro-cal-nav:disabled {
    opacity: .35;
    cursor: not-allowed;
}

/* One track rule for the headings and the grid, so a column heading sits over
   its own column — two rules would drift the moment either changed. */
.v2-ro-cal-dow,
.v2-ro-cal-grid {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 2px;
}

.v2-ro-cal-dow {
    margin-bottom: 4px;
}

.v2-ro-cal-dow span {
    text-align: center;
    font-size: 0.625rem;
    font-weight: 700;
    color: var(--text-muted);
}

.v2-ro-cal-day {
    aspect-ratio: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--radius-button, 8px);
    background: none;
    color: var(--text-body);
    font: inherit;
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}

/* :not(.is-on), because hover was BEATING the chosen date. Daniel, 20-Sep:
   "the hover effect on the active date, cancels the active state so it looks
   unselected". It did: :hover:not(:disabled) is three classes against .is-on's
   two, so pointing at the chosen day repainted it as an ordinary one and the
   only date that mattered was the only one you could not see. The chosen day is
   already at its final state and has nothing for hover to add — same reasoning
   as the selected nav item and the chosen interval chip. */
.v2-ro-cal-day:hover:not(:disabled):not(.is-on) { background: var(--surface-inset); }

.v2-ro-cal-day.is-on {
    background: var(--accent-request-order);
    color: var(--text-on-brand);
    font-weight: 700;
}

/* Out of range at either end — a date already gone, or one past the twelve-month
   horizon. SHOWN and disabled rather than blanked: the month has to read as a
   month, and a gap where the 1st should be reads as broken. One treatment for
   both ends, because it is one fact — this day is readable and not orderable. */
.v2-ro-cal-day.is-out {
    opacity: .3;
    cursor: not-allowed;
}

.v2-ro-cal-day:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: -2px;
}

.v2-ro-cal-chosen {
    margin: var(--space-3, 12px) 0 0;
    text-align: center;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--accent-request-order);
    min-height: 1.25rem;
}

/* The per-line review is gone — Daniel, 20-Sep: "Dont need each product
   displayed either. just stats (number of items (no options) no. of files
   etc." Its selectors go with it rather than sitting here waiting to style
   markup nothing emits any more. */


.v2-ro-confirm-where {
    margin: 0 0 var(--space-4, 16px);
}

.v2-ro-confirm-addr {
    margin: 0 0 var(--space-2, 8px);
    font-size: 0.75rem;
    color: var(--text-muted);
}

.v2-ro-confirm-where textarea { resize: vertical; }

.v2-ro-confirm-rx {
    margin: var(--space-4, 16px) 0 0;
    padding: 0;
    border: 0;
}

.v2-ro-confirm-when {
    margin: 0 0 var(--space-4, 16px);
}

.v2-ro-confirm-check {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2, 8px);
    padding: 6px 0;
    font-size: 0.8125rem;
    color: var(--text-body);
    cursor: pointer;
}

/* The control keeps its own size against a 0.8125rem label, or it shrinks with
   the text and the hit target goes with it. */
.v2-ro-confirm-check input {
    flex: 0 0 auto;
    margin: 1px 0 0;
    width: 15px;
    height: 15px;
    accent-color: var(--accent-request-order);
}

/* The same change as .v2-ro-cf-repeat above, and for the same reason — these
   three are the only dependent blocks in this dialog and they were all wearing
   the same left rule. A border all the way round, 1px and soft. */
.v2-ro-confirm-method,
.v2-ro-confirm-eps {
    margin-top: var(--space-2, 8px);
    padding: var(--space-3, 12px);
    border: 1px solid var(--border-subtle, var(--border-medium));
    border-radius: var(--radius-button, 8px);
}

.v2-ro-confirm-eps textarea {
    resize: vertical;
    font-variant-numeric: tabular-nums;
}

/* ── MEASUREMENT FORMS ─────────────────────────────────────────────────────
   One row per SUPPLIER still outstanding, because that is the unit the rule
   works in: one form covers every garment from that maker on the order.

   The section appears only when something needs it, so it can afford to state
   its requirement in full — it is never in the way of an order that does not
   have one. */
/* A CARD NOW, not a footer inside one — it sits between the basket and the
   order details, so it carries .v2-ro-card and needs nothing of its own except
   the [hidden] every display-setting rule in this file owes. Its margin, its
   top rule and its padding all belonged to being the last thing on somebody
   else's card. */
.v2-ro-forms[hidden] { display: none; }

.v2-ro-forms-title {
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-strong);
}

.v2-ro-forms-lede {
    margin: var(--space-1, 4px) 0 var(--space-3, 12px);
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* An outstanding row is BORDERED, not filled with a warning colour: it is a
   thing still to do, not an error the nurse has made. It earns the accent only
   once it is satisfied, which is the moment worth marking. */
.v2-ro-form-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: var(--space-2, 8px) var(--space-3, 12px);
    margin-bottom: var(--space-2, 8px);
    border: 1px dashed var(--border-medium);
    border-radius: var(--radius-button, 8px);
}

.v2-ro-form-row:last-child { margin-bottom: 0; }

/* ── A MANUFACTURER IN THE FILTER PANEL ────────────────────────────────────
   A row that NAVIGATES, so it is a button and not a checkbox — the facet rows
   beside it narrow what is on screen, and these choose what goes on it. The
   difference has to be visible before it is clicked. */
.v2-ro-filter-brand {
    display: block;
    width: 100%;
    padding: 5px 8px;
    border: 0;
    border-radius: var(--radius-button, 8px);
    background: none;
    /* ── THE CATEGORY IS THE COLOURED THING, NOT THE LIST ─────────────────
       Daniel, 18-Sep: "should we also maybe make the category the coloured
       version here? rather than every manufactuere?"

       Right: forty accent-coloured rows is not emphasis, it is a wall with no
       hierarchy — the eye has nothing to land on and the colour stops meaning
       anything. The category headings take it instead, which is what a nurse
       scans by, and the manufacturers read as the plain list they are. */
    color: var(--text-body);
    font: inherit;
    font-size: 0.8125rem;
    text-align: left;
    cursor: pointer;
}

.v2-ro-filter-brand:hover {
    background: var(--surface-inset, transparent);
    color: var(--text-strong);
}

.v2-ro-filter-brand:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: -2px;
}

/* ── ASKED FOR, NOT ERRORED ────────────────────────────────────────────────
   Pressing a Submit blocked only by a form brings the nurse here. Two pulses
   and stop: enough to find it on a scrolled column, not so much that it reads
   as an error state she has to clear. */
@keyframes v2-ro-ask {
    0%, 100% { border-color: var(--border-medium); }
    50%      { border-color: var(--intent-warning); }
}

.v2-ro-form-row.is-asking {
    animation: v2-ro-ask .5s ease-in-out 2;
}

/* The same two pulses on the upload box and the clinical notes when a staff
   order is submitted with neither (Jake, 16-Oct; request-order-catalogue.js
   showEvidenceNeeded). Only the warning midpoint is set, so each box starts
   and ends on its own border colour — the notes field's is not the drop's. */
@keyframes v2-ro-ask-own {
    50% { border-color: var(--intent-warning); }
}
.v2-ro-drop.is-asking,
.v2-ro-notes.is-asking {
    animation: v2-ro-ask-own .5s ease-in-out 2;
}

/* A visually-disabled Submit: it still takes the press, because the press is
   what shows her the tile. Only the look says "not yet". */
.v2-ro-submit.is-blocked {
    opacity: .5;
    cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-form-row.is-asking { animation: none; border-color: var(--intent-warning); }
    .v2-ro-drop.is-asking,
    .v2-ro-notes.is-asking { animation: none; border-color: var(--intent-warning); }
}

.v2-ro-form-row.is-ok {
    border-style: solid;
    border-color: var(--accent-request-order);
    background: var(--surface-inset, transparent);
}

.v2-ro-form-supplier {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-body);
}

.v2-ro-form-files {
    flex: 1 1 100%;
    list-style: none;
    margin: 0;
    padding: 0;
    min-width: 0;
}

/* A label wrapping a hidden input, so it must LOOK like the control it is —
   and focus has to be visible on the label, since the input it belongs to is
   the thing that is off-screen. */
.v2-ro-form-pick {
    flex: 0 0 auto;
    padding: 4px 10px;
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-pill, 999px);
    background: transparent;
    color: var(--text-body);
    font-size: 0.75rem;
    font-weight: 600;
    cursor: pointer;
}

.v2-ro-form-pick:hover {
    border-color: var(--accent-request-order);
    color: var(--text-strong);
}

.v2-ro-form-pick:focus-within {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

/* WHAT MAKES A MADE-TO-MEASURE LINE PLACEABLE, so it is stated rather than
   implied — and in tabular figures, because it is read back digit by digit.

   On the SKU row, as classic has it: both are reference numbers for the same
   line, and giving the schema its own row made a two-row garment a three-row
   one for nothing. */
.v2-ro-line-schema {
    font-variant-numeric: tabular-nums;
}

.v2-ro-line-schema::before {
    content: "·";
    margin: 0 5px;
    color: var(--border-medium);
}

/* Shares the head row with the remove control, so it takes the room and gives
   it back first — the button must stay reachable however the code is spelled. */
.v2-ro-line-sku {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    font-size: 0.6875rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.v2-ro-line-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 8px);
    min-width: 0;
}

/* ── LEAD TIME: SILENT WHEN THERE IS NOTHING TO SAY ────────────────────────
   Daniel, 16-Sep: under 7 working days — 14 for made-to-measure — or no data at
   all reads "No delays expected"; over that gets "the traffic light system from
   amber to red (using our globals) with the time".

   So the calm state is deliberately NOT a chip. A pill on every line would make
   the reassuring case exactly as loud as the actionable one, and a nurse
   scanning a basket of twelve needs the two that are late to be the two that
   catch the eye. Plain text while there is nothing to do; a filled chip only
   once there is a number worth reading.

   The tones are the globals rather than new colours — Golden Hour for slow, Sea
   Pink for late — each taking its own -subtle ground with its matching -text
   foreground, so both themes resolve as a pair instead of one being inherited
   from the other. */
.v2-ro-lead {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--intent-success-text);
    white-space: nowrap;
}

/* NO PILL. Daniel, 17-Sep: "lose the pill container for the working days so they
   display coloured text only like the no delays expected".

   Right, and it makes the set consistent: three states that differ only by
   colour read as one scale, where a chip for two of them and plain text for the
   third read as two different kinds of thing. The colour is doing the work
   already — a filled ground on top of it was saying the same thing twice, and
   costing 4px of line height per basket row to do it. */
.v2-ro-lead--slow,
.v2-ro-lead--late {
    font-variant-numeric: tabular-nums;
}

.v2-ro-lead--slow { color: var(--intent-warning-text); }
.v2-ro-lead--late { color: var(--intent-danger-text); }

.v2-ro-qty {
    display: flex;
    align-items: center;
    gap: var(--space-1, 4px);
    margin-left: auto;
}

/* ── THE RTW / MTM FORK ────────────────────────────────────────────────────
   Built to RegisterPatient/Selection/Desktop/Dark (1525:14175) and its Light
   twin, which Daniel pointed at with "the cards should span on spec? see".

   They span because there are exactly two, and two cards at a range card's
   width leave two thirds of the row empty — which reads as a list that has
   failed to load rather than a choice of two. Equal halves of the row say
   "these are the options, and there are two".

   The frame's own colours: the card is colour/flint #3A4A50 on a flint/600
   panel, which is --surface-inset in both themes, and the blurb is flint/200.
   No count on these — the frame does not show one, and it is right not to: a
   nurse choosing here is deciding what KIND of thing she is ordering, and
   "166 against 124" is noise against that question. */
.v2-ro-product-grid.is-fork {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
}

.v2-ro-fork-card {
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas:
        "icon  icon"
        "title title"
        "blurb go";
    align-items: end;
    gap: var(--space-2, 8px);
    padding: var(--space-4, 16px);
    text-align: left;
    /* ── THE SAME CARD AS EVERY OTHER CARD ────────────────────────────────
       Daniel, 18-Sep, with the frame beside the render: "can we match the card
       colour".

       It was on --surface-inset, which is the RECESSED surface — right for
       something set into a card, wrong for a card sitting on the panel. Against
       the panel's own ground the two were nearly the same value, so the cards
       read as flat regions rather than as things to press, which is exactly
       what the screenshots show.

       --surface-card is what the range cards and the product cards already use,
       so the fork now matches the step either side of it rather than being the
       one card on the journey with its own colour. */
    background: var(--surface-card);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-card, 12px);
    cursor: pointer;
    transition: border-color .14s ease, transform .14s ease;
}

.v2-ro-fork-card:hover {
    border-color: var(--accent-request-order);
    transform: translateY(-1px);
}

.v2-ro-fork-card:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

.v2-ro-fork-icon {
    grid-area: icon;
    display: block;
    color: var(--text-strong);
}

.v2-ro-fork-title {
    grid-area: title;
    font-size: 1.0625rem;
    font-weight: 700;
    color: var(--text-strong);
    line-height: 1.3;
}

/* Two lines in the frame, and left to wrap rather than clamped: this is the
   sentence that tells the two branches apart, so it is the last thing that
   should be truncated. */
.v2-ro-fork-blurb {
    grid-area: blurb;
    font-size: 0.8125rem;
    color: var(--text-muted);
    line-height: 1.4;
}

.v2-ro-fork-card .v2-ro-range-go { grid-area: go; margin: 0; }

.v2-ro-fork-card:hover .v2-ro-range-go {
    background: var(--accent-request-order);
    color: var(--text-on-brand);
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-fork-card { transition: none; }
    .v2-ro-fork-card:hover { transform: none; }
}

/* Narrow: one above the other rather than two half-width cards whose blurbs
   wrap to four lines each. */
@media (max-width: 720px) {
    .v2-ro-product-grid.is-fork { grid-template-columns: minmax(0, 1fr); }
}

/* ── THE RANGE CHOOSER ─────────────────────────────────────────────────────
   Built to the card Daniel sent on 17-Sep: the range name, a quiet pill saying
   how many products sit behind it, and an arrow on the right.

   The whole card is the <button>. A card with a small arrow button inside it
   gives two targets for one action and makes the arrow look like the only live
   part, when the useful target is the 200px card.

   Lives in the same container as the products, so one grid serves both steps
   and there is no second scroll region to keep in step — but on wider tracks,
   because a range card carries a name, a count and an arrow where a product
   card carries a clamped name and a button. */
/* No track override for ranges any more: both steps share 15rem above, which is
   what stops the grid re-flowing as you move between them. The class stays as a
   state hook — the two card types are still different shapes inside the same
   tracks. */

/* SHORTER. Daniel, 17-Sep: "is there a need for the range card height to be
   that tall". There was not — 7rem plus a 16px gap between two short rows left
   a band of nothing in the middle of every card, and with nine or twelve ranges
   on screen that is a screenful of padding. The card is now as tall as its two
   rows need. */
.v2-ro-range-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 12px);
    padding: var(--space-3, 12px) var(--space-4, 16px);
    text-align: left;
    background: var(--surface-card);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-card, 12px);
    cursor: pointer;
    transition: border-color .14s ease, transform .14s ease;
}

.v2-ro-range-card:hover {
    border-color: var(--accent-request-order);
    transform: translateY(-1px);
}

.v2-ro-range-card:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

.v2-ro-range-name {
    font-size: 0.9375rem;
    font-weight: 700;
    color: var(--text-strong);
    line-height: 1.3;
}

.v2-ro-range-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
}

/* ── NO PILL AROUND THE COUNT ──────────────────────────────────────────────
   The frame draws one — Card/Products, a filled box inside Product_Card_Dark —
   but Daniel overrules it, twice: "i am not a huge fan of the inner container in
   the continer", then "the produc card is ok. its the range ones i dont like".
   Figma is the rule until Daniel is.

   It reads better without: a box inside a box, holding two words and a number,
   was drawing a border around the least interesting thing on the card. The
   count is a fact — the ARROW is the control — so it is plain text and the
   colour does the separating.

   It was also wrong in dark for a reason worth recording: the pill used
   --surface-sunken, which is Flint 600 in dark and therefore IDENTICAL to
   --surface-card. A container painted the colour of the thing behind it is not
   a container. The token that steps correctly in both themes is
   --surface-inset — Pebble 200 in light, Flint 500 in dark — which is exactly
   the #3A4A50 the frame specifies. That is what the arrow below now uses. */
.v2-ro-range-count {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2, 8px);
    color: var(--text-muted);
    font-size: 0.75rem;
}

/* Meadow on the number alone: it is the one value that differs card to card,
   and colouring it lets the row be scanned for "which range is big". */
.v2-ro-range-n {
    color: var(--intent-success-text);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

/* ── THE INNER SHADING, MADE TO ACTUALLY READ ──────────────────────────────
   Daniel, 17-Sep: "im not seeing an inner shading?"

   --surface-inset is the right token and matches the frame — Pebble 200 in
   light, Flint 500 in dark — but Flint 500 against a Flint 600 card is a twelve
   point step, which is correct on paper and nearly invisible on a screen at
   arm's length.

   A border fixes it without picking a louder fill: an edge reads at any
   contrast where a fill of the same difference does not, and it holds up in
   both themes without a second token guessing which way to step. The fill
   stays, so the circle is shaded AND defined rather than one or the other. */
.v2-ro-range-go {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    margin-left: auto;
    border-radius: var(--radius-pill, 999px);
    background: var(--surface-inset);
    border: 1px solid var(--border-subtle);
    color: var(--text-muted);
    transition: background-color .14s ease, color .14s ease, border-color .14s ease;
}

.v2-ro-range-card:hover .v2-ro-range-go {
    background: var(--accent-request-order);
    color: var(--text-on-brand);
}

/* THE HEADING IS THE BREADCRUMB. The brand segment is a button only while a
   range is open; where you already are is never pressable. */
/* The heading becomes a row so the segments sit on one line and wrap together
   on a narrow panel rather than the crumb dropping below its own separator. */
.v2-ro-crumbs {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    flex-wrap: wrap;
}

.v2-ro-crumb-sep { color: var(--text-muted); font-weight: 400; }

.v2-ro-crumb {
    padding: 0;
    background: none;
    border: 0;
    /* font: inherit so the crumb is the HEADING's size — it is part of the
       title's sentence, not a control parked next to it. Only the colour says
       it can be pressed. */
    font: inherit;
    color: var(--accent-request-order);
    cursor: pointer;
}

.v2-ro-crumb:hover { text-decoration: underline; }

.v2-ro-crumb:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
    border-radius: var(--radius-sm, 6px);
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-range-card,
    .v2-ro-range-go { transition: none; }
    .v2-ro-range-card:hover { transform: none; }
}

/* ── A RANGE HEADING INSIDE THE PRODUCT GRID ───────────────────────────────
   The grid is a CSS grid of cards, so a heading has to be told to take the whole
   row — otherwise it sits in the first cell and the range's own products wrap
   around it, which reads as a card with no Add button.

   grid-column: 1 / -1 spans whatever the column count resolves to at this width,
   rather than naming a number that would be wrong the moment the panel widens. */
.v2-ro-range-head {
    grid-column: 1 / -1;
    margin: var(--space-4, 16px) 0 0;
    font-size: 0.8125rem;
    font-weight: 700;
    letter-spacing: .02em;
    color: var(--text-strong);
}

/* No leading gap on the first one — it sits directly under the controls. */
.v2-ro-range-head:first-child { margin-top: 0; }

/* The pack, in the picker. Quiet: it is a fact about the product, not a
   control, and it sits between the name and the axes that are. */
.v2-ro-var-pack {
    margin: 0 0 var(--space-3, 12px);
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* Pack size and type, and how many variants sit behind the card. The pack is
   the load-bearing half: it changes what a quantity of 1 MEANS, so it belongs
   beside the control that sets one. */
.v2-ro-product-facts {
    margin: 0;
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* Made-to-measure is a different FLOW, not a different product — measurement
   forms, a schema-or-form gate, option lines that are not separate items. A
   nurse needs that before the Add, not after it, so it is a badge rather than a
   footnote. Warning tones because it is a caution, not an error. */
.v2-ro-product-mtm {
    align-self: flex-start;
    padding: 2px var(--space-2, 8px);
    border-radius: var(--radius-pill, 999px);
    background: var(--intent-warning-subtle);
    color: var(--intent-warning-text);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: .02em;
}

/* ── THE VARIANT PICKER ────────────────────────────────────────────────────
   Chips, not the dropdowns classic uses. A dropdown hides every option until it
   is opened, so choosing two axes is open-read-choose twice — and it cannot show
   that Large/Long does not exist until after Large/Long has been chosen. Laid
   out flat, the whole axis reads at a glance and an impossible combination is
   disabled before it is clicked. Most garments are two axes of three or four
   values: one screen, no scrolling, no reopening. */
.v2-ro-var-axes {
    display: flex;
    flex-direction: column;
    gap: var(--space-4, 16px);
}

.v2-ro-var-axis {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
}

.v2-ro-var-axis-label {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: .02em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* ── AN AXIS TITLE CENTRES OVER ITS BOX ────────────────────────────────────
   Daniel, 20-Sep: "lets centralise the drop down titles over the boxes? So
   Size should be middle above the drop down box?"

   It follows the quantity, which centres for the same reason: a caption
   belongs over the thing it names, and a short uppercase word pinned to the
   left corner of a full-width box reads as the start of a form row rather than
   as that box's title.

   SCOPED TO .v2-ro-var-axis, not to the label class. That class is also worn
   by the OPTIONS heading over a two-column list, by the schema field's caption
   above its helper line, and by the velcro modal's Top/Bottom — all of which
   head left-aligned content and would be stranded mid-air by a blanket rule.
   The axis rows are the only place a title sits over a single centred control. */
.v2-ro-var-axis > .v2-ro-var-axis-label {
    display: block;
    text-align: center;
}

/* ── ONE SELECT PER AXIS ───────────────────────────────────────────────────
   The chips are gone. They read well for three sizes and badly for fifteen
   colours — which is most of this catalogue — where they wrapped to four rows
   and turned three axes into a wall. A dropdown costs one line whatever it
   holds, which is the property that actually matters here.

   ── ONE WIDTH, WHATEVER THE DIALOG HOLDS ──────────────────────────────────
   Daniel, 20-Sep: "i hate the resizing we added. what i think i want is them
   all to have a unified size. the size they are when across both columns,
   should be the same for even just 1 variant select."

   He is right, and it is the better answer than the one before it. Sizing each
   box to its own longest value made four boxes four different widths — Size
   short, Batik shorter, Border long — which reads as damage rather than as
   fit. And the previous full-width version had the same fault from the other
   end: one axis got a box twice the width of the same axis on a product that
   happens to have two.

   So the track is the unit. Every select fills its column, the grid always has
   two columns, and the dialog always asks for the width to hold them. A lone
   Size box is then exactly the Size box a four-axis product shows. Nothing
   resizes because nothing is being fitted to anything. */
.v2-ro-var-select,
.v2-ro-var-axis > .v2-combo {
    width: 100%;
}

/* ── THE CLIP LIFTS WHILE A COMBO IS OPEN ──────────────────────────────────
   Daniel, 20-Sep, on select.js changing under this page: "lets get that added
   and done as should be an easish win."

   The win is the combo itself — it now honours disabled and hidden on an
   option, which is exactly what the unavailable-variant handling needs, and it
   brings v2's own list styling instead of the platform's.

   What made it unusable here is written down three times already: a
   .v2-combo-list is an ELEMENT, and any overflow:hidden/auto ancestor eats it.
   This modal body scrolls, so it was one. The documented fix is to make the
   clip CONDITIONAL rather than to remove it — the body still scrolls while a
   long option list is being read, and only stops for as long as a dropdown is
   actually open.

   Keyed on the combo's own aria-expanded, which select.js maintains, so
   nothing new has to be kept in step. */
.v2-modal-body:has(.v2-combo [aria-expanded="true"]) {
    overflow: visible;
}

/* The address lookup is the same shape of problem and takes the same fix: it
   overlays now, and an overlay inside an overflow:auto box is clipped at that
   box's edge. Conditional again — the body still scrolls the moment the results
   close, which for this one is the common state. */
.v2-modal-body:has([data-v2-ro-cf-results]:not([hidden])),
.v2-modal-body:has([data-v2-ro-cf-saved]:not([hidden])) {
    overflow: visible;
}

/* ── AND THE CONTENT KEEPS ITS WIDTH ACROSS THE SWITCH ─────────────────────
   Daniel, 20-Sep: "they are still flexing when i open and close the drop down,
   i believe its the scroll bar but that should sit inside the drop down not
   request extra space."

   He had the culprit. scrollbar-gutter above reserves 12px each side WHILE this
   is a scroll container, and every rule here stops it being one — so the
   gutters are handed back the instant a list opens and the content grows by
   24px. Centred, which is what the last fix bought, but wider, and the boxes
   flexed by exactly that.

   So the gutters are paid for by padding while they are not being reserved. The
   content box is then identical in both states and nothing moves at all.

   12px is not a guess: .v2-modal-body is named in the ::-webkit-scrollbar rule
   in components.css that sets it, and Firefox's 'thin' is the nearest match —
   the note there records the same pairing. If that width changes, this changes
   with it. */
.v2-modal-body:has(.v2-combo [aria-expanded="true"]),
.v2-modal-body:has([data-v2-ro-cf-results]:not([hidden])),
.v2-modal-body:has([data-v2-ro-cf-saved]:not([hidden])) {
    padding-inline: 12px;
}

.v2-ro-var-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
}

.v2-ro-var-chip {
    padding: var(--space-2, 8px) var(--space-3, 12px);
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-pill, 999px);
    background: transparent;
    color: var(--text-body);
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color .14s ease, border-color .14s ease, color .14s ease;
}

.v2-ro-var-chip:hover:not(:disabled) {
    border-color: var(--accent-request-order);
    color: var(--text-strong);
}

.v2-ro-var-chip.is-on {
    background: var(--accent-request-order);
    border-color: var(--accent-request-order);
    color: var(--text-on-brand);
}

/* SHOWN, NOT HIDDEN. A nurse hunting for Large needs to see that Large exists
   and is unavailable with what else is chosen — not to watch it disappear and
   wonder whether she imagined it. Kept above the 3:1 non-text contrast floor
   rather than faded to nothing, because it still has to be readable to do that
   job. */
.v2-ro-var-chip:disabled {
    opacity: .45;
    cursor: not-allowed;
}

/* A LOCKED DEFAULT IS NOT AN UNAVAILABLE ONE. Both are disabled, and the fade
   above would say the same thing about each — but one is "you cannot have this"
   and the other is "you already have this, always". So the default keeps its
   full contrast and its fill; only the cursor tells you it will not toggle. */
.v2-ro-var-chip.is-locked {
    opacity: 1;
    cursor: default;
}

.v2-ro-var-chip:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

/* ── AN OPTION IS A ROW, BECAUSE IT CARRIES A NUMBER ───────────────────────
   Chips were right while an option was a yes/no. Once it has a quantity it
   needs a row: a tick and a name on the left, the count on the right, aligned
   down the list so a configured garment can be read as a column of numbers. */
/* ── TWO COLUMNS ONCE THERE ARE ENOUGH TO WARRANT IT ───────────────────────
   Daniel, 18-Sep: "maybe 2 columns for ease for the larger option bases."

   auto-fill with a 13rem floor rather than a hard two: a garment with three
   options gets one column and stays compact, one with ten gets two and stops
   being a scroll. The dialog decides from its own width, so the same rule holds
   on a phone without a media query. */
.v2-ro-opt-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
    gap: 4px var(--space-3, 12px);
    align-content: start;
}

/* ── THE ROW DOES NOT RESIZE WHEN IT IS TICKED ─────────────────────────────
   Daniel, 18-Sep: "not sure it visually looks good when you go from deactive to
   active the jump in size moves the quant too much?"

   It did: a greyed "×6" is about 22px and a stepper is about 78px, so ticking
   an option shoved its own row and — in a two-column grid — every row beside
   it. The list appeared to twitch each time a box was checked.

   A grid instead of a flex row fixes it at the cause: the third track is sized
   once, to the widest thing that can occupy it, so the preview and the stepper
   land in the same footprint and nothing moves but the control itself. */
.v2-ro-opt-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 5.25rem;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: 4px 6px;
    border-radius: var(--radius-button, 8px);
    min-width: 0;
}

/* ── ONE SLOT, ALWAYS THE SAME SIZE ───────────────────────────────────────
   Daniel, 20-Sep: "When an option is chosen, the quant grows large and the
   mvement is quite janky."

   Ticking swapped a two-character "×1" for a three-control stepper, and the
   row resized around it — which in a two-column grid nudges the neighbour and
   everything below. The jank was the layout re-solving itself.

   So the slot is in every row from the start, right-aligned in the fixed
   track and tall enough for the stepper whatever is in it. The contents swap;
   nothing moves. min-height rather than height, so a wrapped reason can still
   grow the row it belongs to. */
.v2-ro-opt-slot {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    justify-self: end;
    min-height: 28px;
}

/* Numbers line up down the column whether they are previews or steppers, which
   is what makes a garment-count change readable as one movement rather than
   eight. */
.v2-ro-opt-slot > .v2-ro-qty,
.v2-ro-opt-slot > .v2-ro-opt-preview,
.v2-ro-opt-slot > .v2-ro-opt-synced,
.v2-ro-opt-slot > .v2-ro-opt-velcro {
    justify-self: end;
}

/* A reason spans both tracks rather than squeezing into the quantity slot. */
.v2-ro-opt-row > .v2-ro-opt-why { grid-column: 1 / -1; }

.v2-ro-opt-row.is-on { background: var(--surface-inset, transparent); }

/* Shown, not hidden — a nurse hunting for an option needs to see it exists and
   is unavailable, not watch it disappear and wonder. Above the 3:1 floor so it
   stays readable enough to do that job. */
.v2-ro-opt-row.is-off { opacity: .55; }

/* The whole label is the target: a 15px checkbox is a poor one, and the name is
   what the nurse is actually aiming at. */
.v2-ro-opt-tick {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: 2px 0;
    border: 0;
    background: none;
    color: var(--text-body);
    font: inherit;
    font-size: 0.8125rem;
    text-align: left;
    cursor: pointer;
}

.v2-ro-opt-tick[aria-disabled="true"] { cursor: not-allowed; }

/* The box is drawn rather than native, so its ticked state can carry the accent
   in both themes without fighting the UA stylesheet. */
.v2-ro-opt-tick::before {
    content: "";
    flex: 0 0 auto;
    width: 15px;
    height: 15px;
    border: 1.5px solid var(--border-medium);
    border-radius: 4px;
}

.v2-ro-opt-row.is-on .v2-ro-opt-tick::before {
    background: var(--accent-request-order);
    border-color: var(--accent-request-order);
    /* The tick itself, drawn with a background image so it needs no extra
       element and inherits nothing that could fight it. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-size: 12px 12px;
    background-position: center;
    background-repeat: no-repeat;
}

.v2-ro-opt-tick:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
    border-radius: 4px;
}

.v2-ro-opt-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* What this option WOULD take, before it is ticked. Muted to the point of being
   clearly inert — it is a preview of a figure, not a control — but present, so
   changing the garment count visibly moves the whole column and the rule shows
   itself rather than having to be inferred from the first ticked one. */
.v2-ro-opt-preview {
    flex: 0 0 auto;
    font-size: 0.75rem;
    color: var(--text-muted);
    opacity: .55;
    font-variant-numeric: tabular-nums;
}

/* A default's count: shown, not settable. There is no decision to make here, so
   it gets no control to imply there is. */
.v2-ro-opt-synced {
    flex: 0 0 auto;
    font-size: 0.75rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* The velcro count is a RESULT. It reads as a control because it opens the
   helper, but never as a stepper — nudging it would contradict the measurements
   without changing them. */
.v2-ro-opt-velcro {
    flex: 0 0 auto;
    padding: 2px 8px;
    border: 1px dashed var(--accent-request-order);
    border-radius: var(--radius-pill, 999px);
    background: none;
    color: var(--accent-request-order);
    font: inherit;
    font-size: 0.75rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}

.v2-ro-opt-why {
    flex: 1 1 100%;
    padding-left: 23px;
    font-size: 0.6875rem;
    color: var(--intent-warning-text);
}

/* ── HOW MANY GARMENTS ─────────────────────────────────────────────────────
   The number everything else on this screen follows. */
.v2-ro-mtm-qty-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    margin: var(--space-1, 4px) 0 var(--space-4, 16px);
}

/* ── AN UNTICKED OPTION IS A WHAT-IF ───────────────────────────────────────
   Daniel, 20-Sep: "i feel inactive options should be greyed out until ticked,
   otherwise the quant shown on unticked options is misleading."

   The preview count exists because he asked for it on 18-Sep — "so they can
   see the dynamic switching of quants too" — and it earns its place: a nurse
   changing the garment count watches the whole column move and reads the rule
   off it. But at full strength that column looked like eight things being
   ordered.

   Dimming the ROW rather than removing the number keeps the rule visible and
   says what the number is: what this option WOULD take, not what is on the
   order. The ticked rows then stand out as the ones that count, which is the
   fastest possible read of "what have I actually chosen".

   Not .is-off's .55 — that means unavailable and has to stay distinguishable
   from merely unchosen. */
.v2-ro-opt-row:not(.is-on):not(.is-off) { opacity: .72; }

.v2-ro-opt-row:not(.is-on):not(.is-off):hover,
.v2-ro-opt-row:not(.is-on):not(.is-off):focus-within { opacity: 1; }

/* ── A COUNT STANDS UNDER THE THING IT COUNTS ──────────────────────────────
   Daniel, 20-Sep: "The quant box on RTW is left alligned, should be central",
   and on the made-to-measure one, "the quant box is just randomly positioned
   ... just centralise the name, with quant below and supplied as below that".

   Both were the same mistake in two costumes. Laid out as a label-and-control
   ROW, the stepper drifted to wherever the row's justification left it and the
   pack note trailed off the end — which is the "randomly positioned" he saw,
   and it is random: nothing in that arrangement said what the number belonged
   to.

   Stacked and centred, the stepper sits directly under the product it counts
   and the pack note directly under the stepper it qualifies. Each line
   explains the one above it, so the eye reads downward and nothing needs a
   caption to say what it is — which is why the made-to-measure label could go
   entirely. */
.v2-ro-mtm-topqty,
.v2-ro-var-qty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2, 8px);
}

/* Restated, because `display: flex` above is an author declaration and beats
   the browser's own [hidden] { display: none } — the garment count belongs to
   the made-to-measure modal and the quantity to ready-to-wear, so each is
   hidden for the other and would otherwise show on both. */
.v2-ro-mtm-topqty[hidden],
.v2-ro-var-qty[hidden] { display: none; }

/* The label survives on ready-to-wear because axes sit above it there, so the
   stepper needs saying what it counts. Centred with everything else. */
.v2-ro-var-qty .v2-ro-var-axis-label {
    margin: 0;
    text-align: center;
}

.v2-ro-mtm-topqty .v2-ro-mtm-qty-row,
.v2-ro-var-qty .v2-ro-mtm-qty-row {
    justify-content: center;
    margin: 0;
}

/* The pack note goes BELOW the stepper rather than beside it. On the
   made-to-measure side it is a child of the row, so it is pushed onto its own
   line there; on ready-to-wear it is already a sibling. */
.v2-ro-mtm-topqty .v2-ro-mtm-pack {
    flex: 1 0 100%;
    text-align: center;
}

.v2-ro-mtm-topqty .v2-ro-mtm-qty-row { flex-wrap: wrap; }

.v2-ro-var-qty .v2-ro-qty-pack {
    margin: 0;
    text-align: center;
}

/* ── AND THE NAME ABOVE IT ─────────────────────────────────────────────────
   Centred on BOTH now. Daniel, 21-Sep: "Centralise the product name in RTW
   variant modal".

   I had this on the made-to-measure modal only, reasoning that ready-to-wear
   put the name above left-aligned fields it would not line up with. That stopped
   being true the same day: the axis titles centre over their boxes and the
   quantity centres under them, so the name was the one thing on the dialog
   still hugging the left edge — which is exactly what made it look wrong rather
   than deliberate.

   One rule for the whole dialog, so the two modes cannot drift into two
   different headings for the same product. */
.v2-modal[data-v2-modal="ro-variant"] [data-v2-ro-var-name] {
    text-align: center;
}

/* The garment count sits with the product it counts, above the options it
   drives — see the note in the panel. No bottom margin: the modal body's own
   gap follows it. */
.v2-ro-mtm-topqty {
    margin: var(--space-3, 12px) 0 0;
}

.v2-ro-mtm-topqty .v2-ro-mtm-qty-row {
    margin-bottom: 0;
}

.v2-ro-mtm-pack {
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* ── THE VELCRO HELPER ─────────────────────────────────────────────────────
   Takes the dialog while it is open, rather than stacking under the options:
   two questions on screen at once is how a nurse loses track of which she is
   answering. */
.v2-ro-velcro-fields {
    display: flex;
    gap: var(--space-3, 12px);
    margin: var(--space-3, 12px) 0;
}

.v2-ro-velcro-fields label {
    flex: 1 1 0;
    min-width: 0;
    display: grid;
    gap: 4px;
}

/* The ANSWER, not the arithmetic. classic hides the working deliberately — the
   nurse's job is the measurement. */
.v2-ro-velcro-result {
    margin: 0;
    min-height: 1.25rem;
    font-size: 0.8125rem;
    font-weight: 600;
}

.v2-ro-velcro-result.is-ok  { color: var(--accent-request-order); }
.v2-ro-velcro-result.is-bad { color: var(--intent-warning-text); }

.v2-ro-velcro-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2, 8px);
    margin-top: var(--space-4, 16px);
}

/* ── THE SCHEMA, AND WHAT IT COVERED ───────────────────────────────────────
   Two numbers on one row with a multiplication sign between them, because that
   is what they mean and what gets sent: "48291 x 2". Laid out so the sign reads
   as the operator it is rather than as decoration between two unrelated fields. */
.v2-ro-mtm-schema {
    margin: var(--space-5, 20px) 0 0;
    padding: var(--space-4, 16px) 0 0;
    border-top: 1px solid var(--border-subtle, var(--border-medium));
}

.v2-ro-mtm-hint {
    margin: var(--space-1, 4px) 0 var(--space-3, 12px);
    font-size: 0.75rem;
    color: var(--text-muted);
}

.v2-ro-mtm-schema-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
}

/* The schema takes the room; the count needs four characters and no more, so it
   does not flex and the row cannot be mistaken for two equal fields. */
.v2-ro-mtm-schema-row > [data-v2-ro-schema-no] {
    flex: 1 1 auto;
    min-width: 0;
}

/* ── A NUMBER BETWEEN TWO BUTTONS, NOT A FIELD ─────────────────────────────
   Daniel, 18-Sep: "also note the quant is containered?"

   It was wearing .v2-field, which is the full input chrome — border, fill,
   focus ring — so a stepper read as three separate controls in a row rather
   than one. The basket's stepper has always been a bare number between its two
   buttons, and this is the same control, so it looks the same.

   Still an <input>: it stays typeable and keyboard-reachable, and a real focus
   ring appears when it is actually focused. Only the resting chrome goes. */
.v2-ro-mtm-qty {
    flex: 0 0 2.75rem;
    width: 2.75rem;
    padding: 0;
    border: 0;
    background: none;
    min-height: 0;
    color: var(--text-strong);
    font-weight: 600;
    text-align: center;
    font-variant-numeric: tabular-nums;
    /* The spinners are what make it look like a form field, and the two buttons
       either side already do that job. */
    -moz-appearance: textfield;
}

.v2-ro-mtm-qty::-webkit-outer-spin-button,
.v2-ro-mtm-qty::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.v2-ro-mtm-qty:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
    border-radius: 4px;
}

.v2-ro-mtm-times {
    flex: 0 0 auto;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text-muted);
}

/* ── THE ANSWER, NOT ANOTHER QUESTION ──────────────────────────────────────
   Daniel, 18-Sep: "cetralise the Pip code and have the lead below it?"

   Centred, because everything above it is a control the nurse operates and this
   is what those controls produced — centring is what separates a result from a
   field. The code leads; the lead time sits under it and keeps its own tone, so
   a long one still reads amber or red exactly as it does on the card. */
.v2-ro-var-resolved {
    display: grid;
    justify-items: center;
    gap: 2px;
    margin: var(--space-4, 16px) 0 0;
    padding: var(--space-3, 12px) 0;
    border-top: 1px solid var(--border-subtle, var(--border-medium));
    border-bottom: 1px solid var(--border-subtle, var(--border-medium));
    text-align: center;
}

/* Blue. Daniel, 18-Sep: "Pip code shoul be coloured too (blue)". It is the one
   thing on this dialog a nurse copies out of it, so it takes the accent that
   marks everything else on this panel worth acting on. */
.v2-ro-var-pip {
    font-size: 0.9375rem;
    font-weight: 700;
    color: var(--accent-request-order);
    font-variant-numeric: tabular-nums;
}

/* The pack note under the quantity it qualifies, centred on it and in the same
   muted tone the basket uses for the same fact. */
.v2-ro-qty-pack {
    margin: 4px 0 0;
    max-width: 12rem;
    text-align: center;
    font-size: 0.75rem;
    color: var(--text-muted);
}

.v2-ro-qty-pack:empty { display: none; }

.v2-ro-var-lead {
    font-size: 0.75rem;
    font-weight: 600;
}

/* ── AXES SIDE BY SIDE ─────────────────────────────────────────────────────
   Size and Model are two short answers and were taking a full row each, which
   pushed the result and the quantity below the fold on a two-axis product —
   the commonest shape in the catalogue. Same auto-fill rule as the options: one
   column while there is one axis, two once there are two. */
/* TWO COLUMNS ALWAYS, not auto-fit. auto-fit collapses the empty track, so a
   single axis stretched across the whole dialog and the same select was one
   width on one product and twice that on another — the resizing Daniel asked
   to be rid of. Two fixed tracks make the COLUMN the unit, so a lone Size box
   is the Size box every other product shows. */
.v2-ro-var-axes {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3, 12px);
    align-items: start;
}

/* On a phone the dialog is barely wider than one track, so two would put a
   pair of half-legible selects where one readable one belongs. */
@media (max-width: 600px) {
    .v2-ro-var-axes { grid-template-columns: minmax(0, 1fr); }
}

/* ── AND THE PANEL HAS TO BE WIDE ENOUGH TO HAVE TWO ───────────────────────
   Daniel, 20-Sep: "why is this stacked for drop downs and not side by side?"

   .v2-modal-panel is width:fit-content and this is an auto-fit grid, so the
   grid divides whatever width the panel has and the panel takes whatever the
   grid asks for. Left alone they settle on one column — the same knot the
   confirm dialog hit, and no minmax() tuning escapes it.

   Which is why it looked arbitrary rather than broken: a long product name
   made the panel wide enough for two, a short one did not. "Duomed Soft class
   2 (18-24mmHg) below knee closed toe" got two columns; "Haddenham Comfiwave
   (15-21mmhg) below knee" got one. The layout was being decided by the length
   of a name.

   So a dialog with more than one axis asks for a width instead of inheriting
   one. --modal-max moves with it, because it is a CEILING and would otherwise
   clamp the width back to narrow's 420. 480 leaves 432 inside the panel's
   padding, which two 11rem columns and their gap fit with room to spare.

   One axis still sizes to its content, because a lone select has nothing to
   sit beside and a half-empty dialog is worse than a small one. */
.v2-modal[data-v2-modal="ro-variant"][data-v2-ro-axes="1"] .v2-modal-panel,
.v2-modal[data-v2-modal="ro-variant"][data-v2-ro-axes="2"] .v2-modal-panel {
    --modal-max: 480px;
    width: min(92vw, 480px);
}

/* ── ONE AXIS SITS IN THE MIDDLE, NOT IN THE LEFT TRACK ────────────────────
   Daniel, 20-Sep: "drop down still showing on the left not central?"

   Two fixed tracks is what gives every select the same width, but with one
   axis the grid puts it in track one and leaves track two empty — so the box
   was the right size in the wrong place.

   It spans both tracks and takes back exactly one: 50% of the grid less half
   the gap IS a track, expressed without needing to know what a track measures.
   So the dialog width can change and the box stays the size it is beside a
   second axis, which was the whole point of fixing the width.

   The label spans the full width and centres its text, so the title centres
   over the box rather than over the track it would otherwise share. */
.v2-modal[data-v2-ro-axes="1"] .v2-ro-var-axes > .v2-ro-var-axis {
    grid-column: 1 / -1;
}

.v2-modal[data-v2-ro-axes="1"] .v2-ro-var-axis > .v2-ro-var-select,
.v2-modal[data-v2-ro-axes="1"] .v2-ro-var-axis > .v2-combo {
    width: calc(50% - (var(--space-3, 12px) / 2));
    margin-inline: auto;
}

/* Below the breakpoint the grid is one track, so there is nothing to halve —
   the box fills it as every other select does. */
@media (max-width: 600px) {
    .v2-modal[data-v2-ro-axes="1"] .v2-ro-var-axis > .v2-ro-var-select,
    .v2-modal[data-v2-ro-axes="1"] .v2-ro-var-axis > .v2-combo {
        width: 100%;
    }
}

/* The made-to-measure list is one block inside that grid and manages its own
   columns, so it spans rather than becoming a column itself. */
.v2-ro-var-axes > .v2-ro-var-axis:has(.v2-ro-opt-list) {
    grid-column: 1 / -1;
}

.v2-ro-var-note {
    margin: var(--space-3, 12px) 0 0;
    font-size: 0.8125rem;
    color: var(--intent-warning-text);
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-var-chip { transition: none; }
}

/* ── THE CARD SAYS WHAT THE BASKET HOLDS ───────────────────────────────────
   Daniel, 16-Sep: "also reflected on the product card on the catalogue to show
   whats in basket."

   A border rather than a fill, for the reason this panel keeps relearning: the
   tile's fill already differs from its container in both themes, and a second
   fill would have to know which. An accent edge works on either. */
.v2-ro-product[data-v2-ro-in-basket] {
    border-color: var(--accent-request-order);
}

/* The card's floor: Add anchored left, the basket marker right. It replaces
   margin-top: auto on the button — the row is what sits on the bottom now, so
   the button keeps one position whether or not the marker is showing. */
/* The button alone on the card's floor, pushed right — "move the select
   variant, add to basket button to the bottom right". It is the only thing on
   the card that does anything, so it gets the row to itself. */
/* Inside .v2-ro-product-bottom now, which owns the margin-top: auto that holds
   the pair at the foot of the card. The foot keeps only its own job: never
   shrink, so the facts beside it are what gives way. */
.v2-ro-product-foot {
    flex: 0 0 auto;
    display: flex;
    justify-content: flex-end;
}

/* Up with the variant count, not on the button's row: "have the added to
   basket appear under the variant number (if relevant)". They are the same kind
   of fact about the product, and keeping them together stops the marker from
   ever pushing the control around. */
/* ── AND ITS LINE IS RESERVED, EMPTY OR NOT ────────────────────────────────
   Daniel, 22-Sep: "can we have the variant count start where it ends up after
   a variant is added? ... the added text would appear in the space afforded to
   it and nothing else would shift."

   So the marker is always in the layout and only its visibility changes. It
   used to be [hidden], which took the line out entirely — so the count sat a
   line lower on every untouched card and jumped up the moment something was
   added. The row is two lines tall everywhere now and the marker fills a space
   that was already its own.

   The height is declared rather than left to the line box, because an empty
   inline element has none: 0.6875rem at 1.45 is 15.95px, so 1rem is the same
   line it occupies when it has something to say. If the font size moves, this
   moves with it. */
.v2-ro-product-inbasket {
    min-height: 1rem;
    font-size: 0.6875rem;
    line-height: 1.45;
    font-weight: 600;
    color: var(--accent-request-order);
    visibility: hidden;
}

.v2-ro-product-inbasket.is-on { visibility: visible; }

/* ── the two commit controls ───────────────────────────────────────────────
   Stacked, not side by side: a 300px column cannot hold two labelled buttons in
   a row without one of them wrapping, and "Submit order request" is not a label
   to abbreviate. */
/* ── THE PART THAT DOES NOT SCROLL ─────────────────────────────────────────
   Daniel, 17-Sep: "The submit buttons should be the locking part, i think the
   notes and files should actually be its own container?"

   These now live OUTSIDE .v2-ro-aside-scroll and last in the column, so they sit
   on the panel's foot whatever the basket is doing above them. It is the
   original "submit should stay locked to bottom" honoured properly: the first
   attempt pinned the whole Order details card, because Submit was welded to it,
   and that is what forced something to be squashed to make room. Only the two
   buttons are pinned now, and nothing is compressed to achieve it.

   flex: 0 0 auto was already right and is what made the separation a markup
   change rather than a layout one. */
.v2-ro-actions {
    display: flex;
    /* SIDE BY SIDE — "lets put them next to each other. [Icon Submit ] [ icon
       schedule ]". They are alternatives rather than a sequence, and stacked
       they read as a first choice with a second underneath it. flex: 1 1 0 on
       each gives them one width between them, so neither looks like the
       consolation. */
    flex-direction: row;
    gap: var(--space-2, 8px);
    /* Clear of the panel's edge — "the bottoms need some room from bottom of
       screen". This is on top of the 20px the column already carries. */
    padding-bottom: var(--space-3, 12px);
    flex: 0 0 auto;
}

/* Both buttons still carry width: 100% from when they were stacked, and
   .v2-ro-schedule carries flex: 0 0 auto from the same era. flex-basis: 0 is
   what actually sizes a flex item so those are already inert, but they are
   stated away rather than left to be reasoned about — the two-class selector
   beats both. */
.v2-ro-actions > .v2-button {
    flex: 1 1 0;
    width: auto;
    min-width: 0;
}

.v2-ro-submit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2, 8px);
    width: 100%;
    min-height: 40px;
    border: 0;
    /* A rounded rectangle like every other button (Daniel, 13-Oct: "the
       submit and schedule buttons are rounded pills when other buttons are
       rounded rectangles?"). */
    border-radius: var(--radius-button, 12px);
    background: var(--accent-request-order);
    color: var(--text-on-brand);
    font-weight: 600;
    cursor: pointer;
}

.v2-ro-submit:disabled { opacity: .5; cursor: not-allowed; }

.v2-ro-submit:focus-visible {
    outline: 2px solid var(--text-strong);
    outline-offset: 2px;
}

.v2-ro-qty-btn {
    flex: none;
    width: 22px;
    height: 22px;
    display: grid;
    place-items: center;
    padding: 0;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-button, 8px);
    background: none;
    color: var(--text-body);
    font-size: 0.875rem;
    line-height: 1;
    cursor: pointer;
}

.v2-ro-qty-btn:hover { border-color: var(--border-medium); color: var(--text-strong); }

.v2-ro-qty-btn:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 1px;
}

/* Tabular so the row does not shuffle sideways as 9 becomes 10. */
.v2-ro-line-qty {
    flex: none;
    min-width: 1.4rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: var(--text-strong);
}

.v2-ro-basket-items:empty { display: none; }

/* ── the attachment box ─────────────────────────────────────────────────────
   A <label> wrapping a visually-hidden file input, so the whole dashed area is
   the hit target and the keyboard gets the input's own focus ring. No click()
   forwarding and nothing to keep in step. */
.v2-ro-drop {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1, 4px);
    padding: var(--space-3, 12px);
    border: 1px dashed var(--border-medium);
    border-radius: var(--radius-card, 12px);
    cursor: pointer;
    text-align: center;
    transition: border-color .16s ease, color .16s ease;
}

.v2-ro-drop:hover { border-color: var(--accent-request-order); }

/* :focus-within, because the input inside it is what actually takes focus. */
.v2-ro-drop:focus-within {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

.v2-ro-drop-lead {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2, 8px);
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text-strong);
}

.v2-ro-drop-help {
    font-size: 0.6875rem;
    color: var(--text-muted);
}

.v2-ro-drop-warn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 4px);
    font-size: 0.6875rem;
    color: var(--text-muted);
}

/* The attached files, named as they will actually be sent. */
.v2-ro-attachments {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 4px);
}

.v2-ro-attachments:empty { display: none; }

.v2-ro-attachment {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: var(--space-1, 4px) var(--space-2, 8px);
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card-raised);
    font-size: 0.75rem;
    color: var(--text-body);
    min-width: 0;
}

/* ── EPS barcodes, one pill each (Jake, 27-Oct) ──────────────────────────
   The attachment row above, laid out as wrapping pills over the box the
   barcodes are typed into. Pill radius from the scale; the barcode keeps its
   full width (no ellipsis — a clipped barcode is a wrong barcode) and uses
   tabular figures so 18-character codes line up. NOT IN THE FRAMES — built
   from the panel's own parts, to be confirmed. */
.v2-ro-eps-pills {
    flex-direction: row;
    flex-wrap: wrap;
    margin-bottom: var(--space-2, 8px);
}
.v2-ro-eps-pill {
    border-radius: var(--radius-pill);
    padding-left: var(--space-3, 12px);
}
.v2-ro-eps-code {
    flex: 0 1 auto;
    overflow: visible;
    font-variant-numeric: tabular-nums;
}
/* Touch widths: the remove control is a 44px target, as on basket lines. */
@media (max-width: 1024px) {
    .v2-ro-eps-pill .v2-ro-icon-btn { width: 44px; height: 44px; }
}

/* A long filename must not push the remove control out of a 300px column. */
.v2-ro-attachment-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 3.5rem, down from 5.5. The floor was taller than the two rows the field is
   now sized to, so the box was set by the floor and the placeholder sat in the
   top half of a half-empty box — "the clinical notes section has dead space
   below the plaeholder text". Still resizable by hand, and it is the whole card
   that is being trimmed here rather than this one control. */
.v2-ro-notes { min-height: 3.5rem; resize: vertical; }

/* Notes an edit cannot change (15-Oct): staff-only, or already in BrightPearl.
   Read-only rather than disabled, so the text stays selectable and copyable. */
.v2-ro-notes[readonly] { resize: none; cursor: default; color: var(--text-muted); }
.v2-ro-notes-lock {
    margin: var(--space-1, 4px) 0 0;
    font-size: 0.75rem;
    line-height: 1.4;
    color: var(--text-muted);
}

/* THE ONE THING ON THIS SCREEN THAT COMMITS, and the frame paints it green
   rather than the panel's accent for exactly that reason — the accent is the
   colour of this SECTION, used on every affordance in it, so it cannot also
   mean "this is the end of the job". */
.v2-ro-schedule {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2, 8px);
    width: 100%;
    min-height: 40px;
    border: 0;
    border-radius: var(--radius-button, 12px);   /* as Submit, 13-Oct */
    /* Meadow, with --text-on-accent rather than --text-on-brand: Seafoam on
       Meadow measures 2.1:1, which is the trap already written down against the
       Search button and .v2-choice-option. Same pairing as those. */
    background: var(--intent-success);
    color: var(--text-on-accent);
    font-weight: 600;
    cursor: pointer;
}

.v2-ro-schedule:disabled { opacity: .5; cursor: not-allowed; }

.v2-ro-schedule:focus-visible {
    outline: 2px solid var(--text-strong);
    outline-offset: 2px;
}

/* Room under the grid so the last row is never behind the button. */
/* ── the staff answers: a form while asking, pills once answered ─────────
   Daniel, 14-Sep: "those last 2 questions need pill placement so not to affect
   the title". Two labelled selects are five rows in a head control built for
   two, and the title gets pushed.

   Same markup, two shapes, chosen by WHERE the card is — the pattern the rest
   of this card already uses. Answering is a job and gets a form; the answer is
   a fact and gets a pill. */
/* Answered, they live INSIDE the subject card's fact list, so they are laid out
   as one more row of it — same column, same rhythm as the patient and the
   delivery address, no rule above them dividing the card in two. The separator
   and the padding were there for when this block sat on its own in the head
   beside the title; it does not any more. */
/* align-items: start, and it is not cosmetic. This was `center`, left over from
   the pill row the head used to show, and .v2-ro-selection's grid inherited it —
   so the two questions were centred against EACH OTHER rather than sharing a top
   edge. One of them carries help text and the other does not, so their boxes are
   different heights, and centring staggered the labels and the selects by about
   20px: "Can you look at the offset staff question layout too?" */
.v2-ro-staff {
    display: flex;
    flex-wrap: wrap;
    align-items: start;
    gap: var(--space-2, 8px);
    min-width: 0;
}
/* Not while editing an order (Jake, 16-Oct): an edit keeps the order's own
   attribution and source, so the two staff questions are neither asked nor
   shown — request-order-catalogue.js sets data-v2-ro-edit-open as it opens one. */
.v2-panel[data-v2-ro-edit-open] .v2-ro-staff,
.v2-panel[data-v2-ro-editing] .v2-ro-staff { display: none !important; }

/* In the head the labels are carried by the values themselves — "Liz Earle" and
   "Phone" need no caption — so they go visually while staying for a screen
   reader, which has no layout to be pushed. */
.v2-ro-staff .v2-label,
.v2-ro-staff .v2-field-help {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.v2-ro-staff .v2-ro-field { flex: 0 1 auto; min-width: 0; }

/* NO SHAPE OF THEIR OWN. Daniel, 16-Sep: "could we also get the drop downs for
   the staff questions to match that of the patient info ones?"

   They were pill-sized here and square-ish in the selection card, both of which
   were dressing them for a head they no longer go to — answered staff questions
   are hidden now. Left plain, they are .v2-select, which is exactly what the
   clinic and patient controls beside them are. */

/* ── and the form shape, while they are still being asked ─────────────── */
.v2-ro-selection .v2-ro-staff {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
    /* Stated rather than inherited: the two questions share a top edge, whatever
       the difference in their heights. */
    align-items: start;
    gap: var(--space-4, 16px);
    padding-top: var(--space-4, 16px);
}

.v2-ro-selection .v2-ro-staff .v2-label,
.v2-ro-selection .v2-ro-staff .v2-field-help {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    white-space: normal;
}


/* The staff pair, while it is the thing being asked. It lands in the same slot
   the clinic and patient card just left, so it inherits .v2-ro-selection's
   centring and width — same container, which is what was asked for. */
.v2-ro-selection .v2-ro-staff {
    width: 100%;
    padding: var(--space-6, 32px);
    /* Same lift as the subject card beside it — it occupies the slot that card
       just left, so it has to wear the same step. */
    background: var(--surface-card-raised);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-card, 12px);
    border-top: 1px solid var(--border-subtle);   /* not the divider it is in the head */
}

/* The catalogue no longer has to keep its last row clear of a button that is no
   longer over it. */
.v2-ro-catalogue { padding-bottom: var(--space-4, 16px); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE WIDE MODE — the outer gaps give way, the nav slides left with them
   Daniel, 14-Sep, drawn rather than described:

       before   gap   nav   panel        gap
                |---| |--| |----|        |---|
       after    gap   nav   panel        gap
                |-|   |--| |--------|    |-|

   The nav KEEPS its width and stays on screen — "dont lose the nav bar. just
   have that slide to the left." It is not the nav that gives way, it is the
   empty margin either side of the row.

   And that needs exactly one declaration, because .v2-section is
   `230px | 40px | minmax(0, 930px)` CENTRED in a full-width row: the outer gaps
   ARE the centring surplus. Raise the panel's cap and the row grows, the surplus
   shrinks at both edges, and the nav slides left because the row it sits in now
   starts further left. The slide is a consequence of the widening, not a second
   thing to animate — so the two can never fall out of step.

   1400 rather than 1fr: the gaps shrink but do not vanish, which is what the
   sketch shows and what stops the panel running into the viewport edge.

   Set by request-order.js on .v2-shell, cleared when the order closes or the
   shell goes home. On the SHELL because the grid that moves is the panel's
   parent — a panel cannot widen itself.
   ═══════════════════════════════════════════════════════════════════════════ */
/* ── IT MOVES BOTH WAYS, AND THE WAY BACK IS THE SAME MOVE ─────────────────
   Daniel, 15-Sep: "the way it smoothly expands on activation, a navigation away
   should trigger an equally smooth transition back to regular width… it seems
   instant now? i hate that."

   So the transition stays on the BASE, where both directions read it. I had
   briefly moved it onto the wide rule, which made the collapse instant — a
   transition is read from the state being moved TO, so landing on a base with
   no transition snaps. That was the wrong reading of the flash: the flash was
   never the animation, it was wide mode not being cleared on a SIDEWAYS
   navigation at all (see the note in request-order.js). Fix that, and the
   collapse is something a nurse watches happen rather than something that
   catches up with them later. */
@media (min-width: 1025px) {
    .v2-shell[data-v2-view="section"] .v2-section {
        /* 700ms. It went 340 → 480 → 700 across two asks ("could the transition
           be slowed slightly", then "and slower please"). The movement is the
           width of a nav bar and it is the page's one deliberate flourish, so it
           can afford to be seen rather than merely noticed.

           Both states declare grid-template-columns with the SAME TRACK COUNT —
           .v2-section's base is `230px minmax(0, 930px)` — which is what makes
           this interpolate at all rather than snap. Two tracks to two tracks. */
        transition: grid-template-columns .7s cubic-bezier(.4, 0, .2, 1);
    }

    .v2-shell[data-v2-wide][data-v2-view="section"] .v2-section {
        grid-template-columns: 230px minmax(0, 1400px);
    }

    /* ── AND WIDER AGAIN WHEN THE FILTER RAIL OPENS ────────────────────────
       1400 + the rail's 200 + the workspace's 16px gap. The catalogue keeps
       every pixel it had; the rail is paid for by the page, which is what was
       asked for — "further expands the full page to allow the space. seeing the
       nav bbuttons slide further left and the panel further right so all remain
       centralised".

       Centring needs no help: .v2-section is justify-content: center, so a wider
       panel column pushes both edges outward symmetrically. The nav moving left
       IS the row getting wider. Same transition as wide mode, so opening filters
       reads as the same gesture as opening the catalogue did. */
    .v2-shell[data-v2-wide-filters][data-v2-view="section"] .v2-section {
        grid-template-columns: 230px minmax(0, 1616px);
    }
}

/* The layout change still has to happen under reduced motion — it just stops
   being a movement. Same selector as the rule it overrides, so the specificities
   match and the later block wins; a media query on its own does not out-rank
   anything (see the note in components.css). */
@media (prefers-reduced-motion: reduce) {
    .v2-shell[data-v2-view="section"] .v2-section { transition: none; }
}

/* ── THE TITLE DOES NOT MOVE WHEN THE CARD ARRIVES ────────────────────────
   .v2-panel-head--control is align-items: end, which is right where the control
   is a button or a toggle sitting on the title's baseline. Here the control is a
   two-row card, so aligning at the BOTTOM pulled the title down to meet it the
   moment a patient was chosen (Daniel, 14-Sep: "when i add a patient the title
   still drops a touch it shouldnt").

   Scoped to this panel rather than changed on the shared rule: every other
   panel's control really does want to sit on the baseline. */
.v2-panel[data-v2-panel="request-order"] .v2-panel-head--control {
    align-items: start;
}

/* ── THE BRAND ROW: BARE ARTWORK ──────────────────────────────────────────
   Daniel, 14-Sep: "i liked the image size, but no grey surrounding? should be
   no. or basically no gap betwee. brand name to show on hover etc. modern. but
   no container the image sits in".

   So there is no tile. Each export already carries its own ground — Haddenham
   white, Juzo teal, medi pink, Sigvaris navy — and a plate behind that was only
   ever drawing a second edge around an edge, then padding the logo away from it.

   The row is a uniform HEIGHT and each logo keeps its own width. That is what
   makes a mixed set look deliberate rather than boxed: nothing is letterboxed,
   nothing is cropped, and the differing widths are the artwork's own.

   The name is always in the DOM and revealed on hover, in space that is always
   reserved — so nothing shifts when it appears. */
/* ── SEVEN, ON ONE ROW, ALWAYS ──────────────────────────────────────────────
   Daniel, 15-Sep: "The brand images are flexing, should be one row always with
   the search bar above them."

   flex with space-between could not promise that. A flex row wraps the moment
   the tiles outgrow it, and the tiles are sized by their own artwork — so the
   row broke 5 + 2 at exactly the width where it mattered.

   A seven-column grid promises it structurally: the count is in the template, so
   there is no width at which it becomes eight or six. Each cell is an equal
   share and the logo sits centred inside it, which is also what finally kills
   the trailing-gap and widening-container problems the flex versions kept
   trading between — the CELLS divide the row, the artwork just sits in them.

   object-fit: contain, never cover. cover is what cropped every logo into
   ADDENHA and IGVARI GROU; contain letterboxes inside the cell instead, and
   since there is no container drawn the letterboxing is invisible. */
.v2-ro-brand-strip {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    align-items: center;
    justify-items: center;
    gap: var(--space-3, 12px);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* The cell centres whatever the tile turns out to be. The tile hugs its artwork
   rather than filling the cell — see the note below on why that is what brings
   the corners back. */
.v2-ro-brand {
    display: grid;
    place-items: center;
    min-width: 0;
    width: 100%;
}

/* ── ONE BOX, SEVEN LOGOS ──────────────────────────────────────────────────
   Daniel, 16-Sep: "the image cards are all slightly different sizes, should be
   uniform in size."

   They were, and it was width-driven sizing doing it: with the image at
   width: 100% of an equal grid cell and height: auto, every logo resolved to a
   DIFFERENT height according to its own aspect ratio. Equal widths, ragged
   heights — which reads as seven cards that nearly match, the worst of both.

   A FIXED HEIGHT on the tile makes the height the constant instead. Each logo
   is exactly 78px tall and as wide as its artwork needs, centred in its cell.
   Height is the axis the eye reads along a row, so that is the one to hold. */
/* ── AND THE TILE HUGS THE ARTWORK, WHICH IS WHY THE CORNERS COME BACK ─────
   Daniel, 16-Sep: "Brand images are still squared, not rounded as they were
   before unifying the sizing."

   The clip was right and it had nothing to clip. With the tile filling its cell
   and the image at width: auto / max-width: 100%, a wide logo gets clamped to
   the cell and object-fit: contain then letterboxes it — so the artwork is
   painted SMALLER than the box it sits in, the radius rounds transparent bands
   either side of it, and the logo's own square corners show through untouched.

   width: auto on the tile makes the box the artwork's size, so the clip lands on
   the artwork itself. Height stays fixed, so the row is still uniform — this
   only changes what the rounding is applied TO, not how big anything is. */
.v2-ro-brand-tile {
    display: grid;
    place-items: center;
    /* ── A UNIFORM BOX, WHICH IS WHAT CLASSIC DOES ─────────────────────────
       Daniel, 16-Sep: "youre wrong. classic as unified them and so v2 can too.
       i simply need the images centralising top to bottom."

       He is right and I argued myself into a corner. I had concluded that
       uniform tiles and bare artwork could not both be had, and classic has been
       doing it all along — .brand-bar-item is flex: 1 / min-width: 130px /
       height: 120px with the image at width and height 100% and object-fit:
       contain. A fixed box, the logo centred inside it on both axes. That is
       all it takes.

       What I got wrong was treating the artwork's own edges as the thing that
       had to be uniform. They do not: the BOX is uniform, and contain centres
       whatever shape the logo happens to be within it. Letterboxing is not a
       failure here, it is how seven different ratios sit in one row without any
       of them being cropped or stretched. */
    /* ── THE BOX TAKES THE ARTWORK'S SHAPE, AND THAT ENDS THE STRETCH ──────
       Daniel, 16-Sep, with classic and v2 side by side: "classic looks good, v2
       looks bad." Every v2 logo was pulled wide — Haddenham's oval flattened,
       medi smeared.

       The cause was this box, not the artwork. A 1fr cell is ~137px and the
       height was pinned at 54, so the box sat at ~2.54 while the artwork is
       2.185 — and object-fit: fill hands the BOX's ratio to the image, so every
       logo was stretched ~16% horizontally. classic never showed this because it
       uses contain and simply refuses to distort.

       Guessing the height was always going to lose: the width is one seventh of
       a row that changes with the panel, so any fixed height is only correct at
       one panel width. Every file is now 472x216 (commit 6a11077d), so instead
       of picking a height, DERIVE it — the cell gives the width, aspect-ratio
       gives the height, and the box matches the artwork at every width.

       Bigger than the old 54px too, since the tile now uses its whole cell
       rather than sitting inside it, which is most of what made classic's row
       read as more confident than ours. */
    width: 100%;
    aspect-ratio: 472 / 216;
    padding: 0;
    /* ── THE TILE HAS A FILL, AND THAT IS WHAT MAKES IT UNIFORM ────────────
       Daniel, 16-Sep: "can we make the curves smoother if possible? we have lost
       the uniform widths?"

       Those two only reconcile with a fill. Without one the eye reads the
       ARTWORK'S edges, so a logo that fits by height leaves its box narrower
       than its neighbour's and the widths look ragged however identical the
       boxes are — and the radius rounds box corners the artwork does not reach,
       which is the squared-off look from earlier today.

       classic has had this the whole time: .brand-bar-item carries
       background: var(--glass-bg). The tile is the thing you see, so it is
       uniform and its corners are the ones being rounded.

       NOT THE GREY SURROUND that was rejected on 14-Sep. This is
       --surface-card-raised, the same step the product tiles and the selection
       cards take — white in light, Flint 500 in dark — so the row reads as part
       of the catalogue rather than as seven boxes dropped onto it. */
    /* ── AND THE DECLARATION HAD DRIFTED OFF THAT NOTE ─────────────────────
       It said --surface-card-raised and read `background: none`, so for a week
       the tile had no ground at all. It went unnoticed because most brand
       artwork ships with a white field baked in and paints its own.

       A TRANSPARENT PNG HAS NOTHING TO PAINT. Daniel, 22-Sep: "need to account
       for no background png images? they should in the least have a theme
       appropriate background" — and theme-tracking rather than a fixed light
       plate, because "we need to account for a clinician using both."

       So the plate follows the theme, which also settles what the file cannot
       do: one PNG serves both themes, so a ground baked into it is theme-blind
       by construction. The theme is only known here. What the composer owes in
       return is a WARNING — a dark mark on the dark plate is 1.9:1, and the
       admin has to be told that before she saves it, not after a clinician
       switches theme. That measurement is in portal_admin/sections/brands. */
    background: var(--surface-card-raised);
    border: 0;
    cursor: pointer;
    transition: transform .16s ease;
    /* ROUNDED, AND ON THE TILE AS WELL AS THE IMAGE. Daniel, 16-Sep: "keep the
       rounded edges for the brand cards too."

       The artwork is a rectangle with its own baked-in background and square
       corners, so the curve has to be a CLIP rather than a border — the radius
       on the img alone only rounds if nothing else paints over it, and with the
       tile now a fixed-height box the safe thing is to clip at the box. */
    /* Smoother, as asked — --radius-card rather than --radius-button. On a 72px
       tile 20px reads as a soft square rather than a rounded rectangle, which is
       what the frame's tiles look like. */
    /* A ROUNDED RECTANGLE, 12px, the buttons' radius (Daniel, 13-Oct: "more
       of a rounded rectangle shape"); 20px read as a pill at this height. */
    border-radius: var(--radius-button, 12px);
    overflow: hidden;
}

/* The unpicked ones go quiet once a brand is chosen — "the brand images fade
   ther inactive ones". Keyed off the strip so the default state, with nothing
   picked, leaves all seven equally bright rather than dimming six of them
   against a tile that has not been chosen yet. */
.v2-ro-brand-strip[data-v2-ro-picked] .v2-ro-brand-tile {
    opacity: .35;
    transition: opacity .18s ease, transform .16s ease;
}

.v2-ro-brand-strip[data-v2-ro-picked] .v2-ro-brand-tile.is-picked,
.v2-ro-brand-strip[data-v2-ro-picked] .v2-ro-brand-tile:hover,
.v2-ro-brand-strip[data-v2-ro-picked] .v2-ro-brand-tile:focus-visible {
    opacity: 1;
}

.v2-ro-brand-tile img {
    display: block;
    /* HEIGHT IS THE CONSTANT, width follows the artwork. The reverse — width
       100%, height auto — is what made the row ragged: seven different aspect
       ratios in seven equal-width cells can only produce seven heights.

       max-width caps a very wide logo at its cell so the row can never be
       pushed out of shape, and object-fit: contain means that cap letterboxes
       rather than crops. cover is what turned these into ADDENHA and IGVARI
       GROU, and it is not coming back. */
    /* Fill the box on both axes and let contain do the fitting — classic's own
       rule, and the reason its row is even. width: auto was mine, and it made
       the tile the artwork's shape, which is exactly what stopped the row being
       uniform. */
    width: 100%;
    height: 100%;
    /* ── FILL, NOT CONTAIN ─────────────────────────────────────────────────
       Daniel, 16-Sep: "i dont want the images in containers. defeats what im
       trying to do. static height and width, use the largest image in the logic
       we had before and balloon the others so they fit."

       contain was the whole problem, and it is worth being precise about why.
       It fits the artwork INSIDE the box, so a logo whose ratio differs from the
       box's leaves transparent margin — which is what made the widths look
       ragged with no container, and what left the box's rounded corners hanging
       over empty space while the artwork kept its square ones.

       fill scales the artwork to the box on both axes. Every tile is then the
       same size, the artwork reaches all four corners, and the radius clips the
       artwork itself — uniform, rounded, and no container, which is the three
       things asked for at once.

       THE COST WAS ASPECT, and it was not mild — it is exactly what Daniel saw
       next to classic. The fix this comment predicted has now happened: every
       export shares one ratio (472x216), and the box above is pinned to it, so
       fill and contain do the same thing.

       So it is contain, not fill. With the ratios matched the artwork still
       reaches all four corners exactly as fill did — Daniel, 16-Sep, "the images
       to sit inside them filled" — but a brand image uploaded through Settings
       is NOT normalised (the uploader is a bare move_uploaded_file), and when
       one of those turns up contain letterboxes it instead of distorting it.
       A band is a blemish; a stretched logo is somebody's trademark drawn
       wrong. */
    object-fit: contain;
}

/* The lift is the only affordance — there is no border left to brighten. */
.v2-ro-brand-tile:hover { transform: translateY(-2px); }

.v2-ro-brand-tile:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 3px;
    border-radius: var(--radius-button, 8px);
}

/* The name: reserved always, shown on hover and on keyboard focus — a caption
   that only a mouse can summon is one a keyboard user never gets. */
/* ── THE CAPTION IS THE GAP ────────────────────────────────────────────────
   Daniel, 16-Sep: "its because of the name text below the image thats causing
   the gap. can we push the name slightly closer to the image to lower the gap
   space?"

   He is right, and it explains why trimming the strip's padding kept moving the
   distance less than expected: the name is hidden with OPACITY, so it holds its
   full height under every tile whether it is showing or not. The row is
   therefore always 78px of logo plus a caption's worth of space, and the strip's
   padding only ever adjusted what came after that.

   Closed up rather than removed: it still has to have somewhere to appear, and
   taking it out of the layout would make the row jump on every hover. 0 margin
   and a tighter line-height give back most of it while leaving the caption its
   place. */
.v2-ro-brand-name {
    display: block;
    margin-top: 0;
    font-size: 0.6875rem;
    line-height: 1.1;
    text-align: center;
    color: var(--text-muted);
    opacity: 0;
    transition: opacity .16s ease;
}

.v2-ro-brand-tile:hover + .v2-ro-brand-name,
.v2-ro-brand-tile:focus-visible + .v2-ro-brand-name { opacity: 1; }

/* A brand with no artwork has nothing to be bare, so it keeps a surface. */
.v2-ro-brand-tile.is-wordmark {
    display: grid;
    place-items: center;
    /* The same box as a tile with artwork — it inherits width and aspect-ratio
       from the rule above, so a brand with no logo keeps its place in the row
       instead of standing at its own size. Only the radius differs, because a
       text tile is the one case with no artwork for the card curve to clip. */
    padding: var(--space-2, 8px);
    background: var(--surface-card);
    border-radius: var(--radius-button, 8px);
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-strong);
    text-align: center;
}

/* Narrow: wrap and pack from the left, so a part-filled last row does not
   spread its two logos to the edges. */
/* ── THE STRIP BECOMES ONE SWIPEABLE ROW BELOW 1025 ──────────────────────────
   Daniel, 22-Sep: "i think brand images shold remain 1 row only, and try to fit
   4 in (the top 4 from that clinicians selections or defaults) maybe even a side
   swipe to show the remaining 3. fade for the scroll."

   Seven 1fr columns is ~45px a tile on a 402px screen, which is a row of stamps.
   Four across the same width is ~93px, which is a logo you can read and a target
   you can hit. The other three stay one swipe away rather than being dropped —
   the strip is the clinician's own seven, in their own order, and silently
   showing four of them would quietly undo that choice.

   1025 because that is this shell's one frontier — the basket, the filter rail
   and the steps all turn there, and Daniel confirmed it: "1025 is the break
   point then you have the containment as screen gets smaller." A second
   frontier near it is how you get a band where half the layout has changed.

   SNAP TO THE TILE, so a swipe never parks half a logo at the edge. */
@media (max-width: 1279px) {
    .v2-ro-brand-strip {
        display: flex;
        /* The desktop grid is explicitly stood down. Leaving the template in
           place while switching to flex leaves a rule that does nothing but
           will read as live to whoever edits this next. */
        grid-template-columns: none;
        gap: var(--space-2, 8px);
        overflow-x: auto;
        /* PINNED, not left to default. Setting overflow-x alone computes
           overflow-y to auto, and the tile's hover lift is a transform that
           would then summon a vertical scrollbar on a row that cannot scroll
           vertically. */
        overflow-y: hidden;
        scroll-snap-type: x mandatory;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
    }

    .v2-ro-brand-strip::-webkit-scrollbar { display: none; }

    /* Four across whatever the width: the track is the row minus its three
       gaps, split four ways. */
    .v2-ro-brand {
        flex: 0 0 calc((100% - (var(--space-2, 8px) * 3)) / 4);
        scroll-snap-align: start;
    }

    /* ── THE SAME SHAPE AS DESKTOP, NOT THE SAME RADIUS ─────────────────────
       Daniel, 1-Oct: "in mobile view the brand images are slightly cropped ...
       Sigvaris is a good example. in desktop its perfect". 20px is right on a
       ~137x63 desktop tile; on this ~93x43 one it is nearly half the height, so
       the tile became a pill and clipped the logo's corners. As a percentage of
       the tile (20/137 across, 20/63 down) the curve scales with it: the
       desktop shape at every width. */
    .v2-ro-brand-tile { border-radius: 14.6% / 31.9%; }

    /* ── AND L&R SITS LEVEL ──────────────────────────────────────────────────
       "L&R medical brand sits higher (im assuming because its name spans 2
       rows?)" — yes. The name under each tile holds its space while hidden, and
       "Lohmann & Rauscher" wraps in a ~93px cell, so that one cell was taller
       and the row (align-items: center from the desktop grid) centred it. Tops
       aligned, and the name kept to one line, so every cell is the same height.
       It only shows on hover or keyboard focus, never on a tap. */
    .v2-ro-brand-strip { align-items: flex-start; }
    .v2-ro-brand-name {
        width: 100%;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* ── THE FADE SAYS THERE IS MORE, AND NEVER MOVES ANYTHING ───────────────
       A MASK, not a gradient overlay, because an overlay would need a stacking
       context and a background colour to sit on — and the strip sits on the
       catalogue's surface, which is two different colours across the themes.
       A mask fades whatever is actually behind it and needs to know neither.

       READ-ONLY, and that is the whole point. data-fade is set from
       scrollLeft/scrollWidth/clientWidth and changes no dimension, no padding
       and no track. The filter rail already paid for the alternative: its floor
       was tied to an at-end state, which changed whether it WAS at the end, and
       the scrollbar oscillated forever. Nothing here can feed back, because
       nothing here has a size. */
    .v2-ro-brand-strip[data-fade="end"] {
        -webkit-mask-image: linear-gradient(to right, #000 82%, transparent 100%);
                mask-image: linear-gradient(to right, #000 82%, transparent 100%);
    }

    .v2-ro-brand-strip[data-fade="start"] {
        -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 18%);
                mask-image: linear-gradient(to right, transparent 0%, #000 18%);
    }

    .v2-ro-brand-strip[data-fade="both"] {
        -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 18%, #000 82%, transparent 100%);
                mask-image: linear-gradient(to right, transparent 0%, #000 18%, #000 82%, transparent 100%);
    }
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-brand-tile,
    .v2-ro-brand-name { transition: none; }
    .v2-ro-brand-tile:hover { transform: none; }
}

/* The 640px wrap rule that stood here is gone with the row it described. It
   packed a part-filled second row from the left — and below 1025 there is no
   second row now, the strip is a single snapping scroller. It survived as a
   no-op on a fixed-fr grid, which is why nobody noticed it saying something
   untrue; on the flex scroller it would have started applying again, still
   harmlessly, still describing a layout that had gone. */

@media (prefers-reduced-motion: reduce) {
    .v2-ro-brand-tile { transition: none; }
    .v2-ro-brand-tile:hover { transform: none; }
}

/* The row fades in once its images have decoded — see revealStrip() in
   request-order-catalogue.js. Without the fade the settled row still APPEARS
   abruptly, which reads as the same jump it was meant to remove. */
.v2-ro-brands:not([hidden]) {
    animation: v2-ro-brands-in .42s cubic-bezier(.4, 0, .2, 1) both;
}

/* 420ms and a short rise, not a 220ms opacity blink — at that length against a
   700ms slide the fade was over before anyone could see it had happened
   (Daniel: "no fade"). The row should arrive, not appear. */
@keyframes v2-ro-brands-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-brands:not([hidden]) { animation: none; }
}

/* ── SMART SCROLL ─────────────────────────────────────────────────────────
   Daniel, 14-Sep: "the whole container which includes the search bar and
   products to move up and over the title on scroll. stopping with the search bar
   at the top. then the products scroll fade under the search bar so its always
   visible."

   THE BODY SCROLLS, AS IT DOES EVERYWHERE ELSE. My previous attempt made the
   PANEL the scroller so the head would be inside the scroll — and it broke three
   things the shell hangs off the body being the scroll region: the styled
   scrollbar, the head's fade, and the panel's own rounded clipping. "lost the
   custom scroll bar again… the fade is broken… the panel edges are squared not
   rounded". Reverted.

   The head gives way instead of being scrolled past. Collapsing the title and
   the subject card as the content rises reads as the catalogue moving up OVER
   them — which is the ask — and it does it without taking the scroll away from
   the element the rest of the shell is built around.

   And it dissolves the old objection to a sticky search. That objection was that
   sticky has to out-rank .v2-panel-head and then sits on the head's fade band.
   Here the head has COLLAPSED by the time the search pins, so there is no band
   left to sit on and nothing to out-rank.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   THE CATALOGUE RISES WITH THE SCROLL — IT IS NOT A FOLD ANY MORE
   ═══════════════════════════════════════════════════════════════════════════
   Daniel, 16-Sep: "id actually like it to only be the catalogue itself that
   scrolls up so Column 1 if you like", and "need to fix the judder at the start
   … it seems to judder as it activates then the scroll is seamless but i want it
   clean and seemless to start with."

   THE JUDDER WAS THE THRESHOLD. The old version waited for 24px, then ran a
   300ms animation that changed the head's LAYOUT — and changing the layout above
   a scroll container resizes it mid-gesture, so the content you are dragging
   moves under your finger. Two things fighting: your scroll, and an animation
   reacting to it.

   So there is no threshold and no animation. The lift is a plain function of
   scrollTop — one pixel up per pixel scrolled until it reaches its stop — which
   means it tracks the gesture exactly and there is no moment at which it
   "activates". Same lesson as the validation bed: a transition running over a
   value the browser is also changing reads as a jump, so drive the value
   directly instead.

   AND IT IS A TRANSFORM, NOT A REFLOW. translateY is composited; nothing above
   the scroll container is re-laid-out, so the scroll stays smooth. The negative
   bottom margin is what keeps the card reaching the panel floor: in a stretch
   context it makes the used height track + lift, and the transform then puts the
   bottom edge back where it started.

   ONLY COLUMN 1 MOVES. The catalogue is exactly as wide as its own column, so
   rising over the head covers the TITLE and leaves the patient card — which sits
   above the basket column — exactly where it was. That is the answer to whether
   the patient info should travel with the fold: it does not, because it is not
   in the way. */
/* The card no longer lifts. With the body scrolling it does not need to: it
   grows, and what moves is the scroll. What the lift was FOR — the search
   coming up to stand in for the title — is the "top is unique" half, and that
   is now carried by the search's own sticky rule rather than by moving the
   whole card and hoping its contents survive the trip.

   align-self: stretch, against the workspace's align-items: start. The two are
   not in conflict — they are two different jobs. The ASIDE must not stretch, or
   the basket becomes as tall as four hundred products and Schedule order goes
   below the fold; the CATALOGUE must, or it shrink-wraps to a brand strip and
   leaves the bottom half of a full-height panel empty. Start for the column that
   should stay short, stretch for the one that is the workspace.

   A floor, not a ceiling: at rest it fills the panel, and with results it grows
   past it and the body scrolls — "if product display is large then the panel
   should expand". */
/* ── THE COLUMN RISES AS ONE ───────────────────────────────────────────────
   "shouldnt be lost should go up to the top of panel and lock visible with
   products then scrolling through fade behind."

   The whole catalogue column moves — controls and products together — so the
   search and the brand strip arrive at the panel's top and stay there, with the
   products continuing to scroll underneath them. Nothing is relocated, nothing
   is duplicated, nothing hides and comes back.

   THE TRANSFORM IS SAFE NOW because nothing inside is sticky. Every previous
   attempt had a sticky bar in here, and a transform on a sticky element's
   ancestor replaces the frame of reference it resolves against — which is why
   the bar kept leaving with the card instead of staying put. Take the sticky
   away and the transform is just paint.

   The negative bottom margin is what stops the column coming up short: in a
   stretch context it makes the used height track + lift, and the transform then
   returns the bottom edge to where it started. */
.v2-ro-catalogue {
    position: relative;
    z-index: 3;                 /* over .v2-panel-head's 2, to cover the title */
    align-self: stretch;
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* NOTHING MOVES THE COLUMN ITSELF. The lift used to be a transform plus a
       negative bottom margin on this box, and the margin was a runaway: this is
       a grid item, so making it taller made the ROW taller, which made the
       stretched aside taller, which let the flex-grown basket claim more, which
       made the row taller again. "the basket seems infinite" — it was, and the
       products scroll died with it because its container kept being resized
       mid-gesture: "i go to scroll down and it does than stops".

       The rise happens INSIDE the column now, between the two children. The
       outer box never changes size, so the grid row is stable and there is
       nothing to feed back. */
}

/* Nothing fades the title any more — it is simply not rendered once the
   catalogue is open. See the display: none rule above. */

/* The dropdown clip that used to stand down here is GONE WITH THE FOLD. It
   existed only because collapsing the head needed overflow: hidden on its
   children, which then ate the patient combo's list. Nothing clips the head any
   more, so there is nothing to make an exception for — the fourth payment on
   that trap was avoided by removing the cause rather than adding another rule. */

/* ── THE SEARCH DOES NOT PIN, BECAUSE IT NEVER MOVES ───────────────────────
   It was sticky, with negative margins and an opaque fill, because the whole
   panel body scrolled and the bar had to hold its place against it. It does not
   any more: the products have their own scroll region below it and the search
   sits outside that region, so staying put is simply what it does.

   That deletes three problems rather than solving them. No z-index to out-rank
   the head. No opaque fill painting a square over the card's rounded top —
   "Product container is squared, not rounded like every one else"; the card was
   rounded all along and this was covering it. And no bar that can drift out of
   step with the thing it is meant to be pinned to. */

/* ── ONE SCROLL, AND IT IS THE PANEL BODY'S ────────────────────────────────
   Daniel, 16-Sep: "in theory the panel should expand down off page and scroll
   bar remains on page to allow to scroll through? same logic as my orders, my
   patients etc." — and, on those panels, "only the bottom of the those pages
   are relevant. the top is unique moreso".

   So the bottom is theirs and there is nothing bespoke about it. .v2-panel-body
   is already flex: 1 1 auto / min-height: 0 / overflow-y: auto, with the styled
   scrollbar, the head fade and the panel's rounded clipping all hanging off it.
   The catalogue simply grows and the body scrolls it — the products run past the
   bottom edge and the panel clips them, which is what rolling off screen is.

   THE SECOND SCROLL REGION IS GONE. A fixed-height card with its own scrollbar
   inside a body that also scrolls was two bars for one list: it is why the bar
   "slightly escaped the container", and why the search and the brand strip were
   trapped in a scroll neither belongs to. Removing it is what makes the bottom
   behave like every other panel. */

/* ── THE FADES, BOTH ENDS, FROM ONE MASK ───────────────────────────────────
   A mask dissolves the CONTENT. The alternative — a gradient panel laid over the
   top — has to be painted the exact colour of whatever is behind it, and this
   card sits on --surface-sunken while the product cards inside it do not: the
   "a fill has to know what it is sitting on" trap that has already cost three
   fixes here.

   TOP: proportional to the lift, so at rest the first row is crisp and the fade
   arrives as the search bar comes up over it. Part of the gesture, not a state
   that switches on.

   BOTTOM: always. Daniel, 16-Sep: "when expanded, the products container should
   flow off the page in the same way my orders and my patients does. like the
   contents are spilling out of the page and scrolling brings them in to view."
   A hard edge with a gap under it reads as a list that has ended; a dissolve
   reads as one that continues, which is the honest signal since it does.

   Done with a mask rather than by letting the card overhang the panel, because
   overhanging would need the PANEL to clip — and the panel holds the patient
   card, whose dropdown cannot escape a clipping ancestor. That is the same trap
   again, and this avoids it entirely. */
/* NO MASK ON THE SCROLL REGION. It had one at both ends and both were wrong.

   The bottom fade was answering the wrong question: "the bottom of page fades
   though? where iut should in fact run off page?" A dissolve says the list is
   ending softly; running off the edge says there is more and you should keep
   going. The second is both truer and what was asked for.

   The top fade belongs to the SEARCH BAR, not to the region — it has to travel
   with the bar it sits under, and a mask on the region cannot. It is back on
   .v2-ro-search::after below. */

/* ── THE PADDING MOVES OFF THE CARD AND ONTO ITS CONTENTS ──────────────────
   Daniel, 16-Sep: "There is also a horizontal scroll bar - should never happen
   on v2", "the search bar is still lost on scroll", "the gap at the top of the
   panel where the search bar would / should be is too big".

   One cause under all three. The search carried negative margins to reach the
   card's edges — correct while it was a child of the CARD, and wrong the moment
   it moved inside the scroll region: a negative left/right margin makes the
   content wider than the box it is in, which IS the horizontal scrollbar, and a
   negative TOP margin puts it above the scroll origin, where the region clips it
   — so the bar was not travelling away, it was being cut off.

   With no padding on the card, each child pads itself and the search spans the
   full width without reaching for anything. No negative margins anywhere, and
   the top gap is the bar's own padding rather than the card's plus the bar's. */
.v2-ro-catalogue { padding: 0; }

/* The brand strip travels with the catalogue and goes quiet on the way.
   Daniel, 16-Sep: "when a manufacturer is selected. faded out the rest and those
   slide up too with the catalogue?" The sliding is the scroll doing its job now
   that the strip is in the scrolling body; the fading is this, tied to the same
   0..1 as the bar's rise so the two read as one movement rather than a list that
   happens to be scrolling while something else animates. */
/* A tighter gap under the tiles. Daniel, 16-Sep: "can we bring the fade point
   slightly closer to the brand image cards? so less gap between them and the
   products?" The strip and the grid are one list of things to pick from, so the
   space between them should read as a step rather than a division.

   THE 4 WAS NEVER THE WHOLE GAP, which is why reducing it twice barely moved
   anything: .v2-ro-catalogue is a flex column with gap: 16px, and that sits
   between the controls and the products on top of this padding. The catalogue's
   gap is zeroed below — its children all carry their own padding now, so it was
   only ever adding a second helping. */
.v2-ro-brands {
    padding: 0 var(--space-4, 16px) var(--space-1, 4px);
}

.v2-ro-catalogue { gap: 0; }
/* No padding here — .v2-ro-products carries it, and two boxes both insetting
   the grid is how a column ends up narrower than its neighbours. */

/* Sticky to the top of the region it lives in, so it is held by the same element
   that moves the products past it. Opaque, or the cards show through as they
   pass. Top corners are the card's own — an opaque square over a 20px radius is
   what made the card look square before. */
/* ── AND THE TOP IS THE UNIQUE HALF ────────────────────────────────────────
   Daniel, 16-Sep: "it should never go but slide up the panel to replace the
   title", and of My Orders / My Patients, "only the bottom of the those pages
   are relevant. the top is unique moreso".

   Two behaviours, one element. Sticky holds it at the top of the panel body so
   it can never be scrolled away — that is the "never go" half, and it is
   structural rather than timed. The transform then carries it the rest of the
   way, up through the head's height, so it comes to rest exactly on the title's
   line — the "replace the title" half.

   THE TRANSFORM IS ON THE BAR ITSELF, not on an ancestor. That distinction is
   the whole reason this works now and did not before: a transform on a parent
   creates a new containing block and takes sticky's frame of reference with it,
   so the bar went along for the ride and out of view. Applied to the sticky
   element, layout — and therefore stickiness — is settled first and the paint
   simply happens higher up.

   Driven straight off scrollTop with no transition, so it tracks the gesture
   exactly and there is no threshold to judder over. */
/* ── NOT STICKY. IT DOES NOT NEED TO BE ────────────────────────────────────
   The search and the brand strip live in .v2-ro-controls, a plain flex child at
   the top of a fixed-height column, with only the products scrolling below them.
   Something that is not inside a scroll cannot be scrolled away, so there is
   nothing here to pin, hide or recover.

   That also retires the bug that survived three attempts: sticky resolves
   against its scroll ancestor's frame of reference, and the lift is a transform
   on the column — which replaces that frame and carried the bar off with it
   every time. No sticky, no conflict, and the transform is free to move the
   whole column as one. */
/* THE CONTROLS ARE WHAT RISES, and the products close the gap behind them.
   Two declarations, one movement, and neither touches the column's outer box —
   which is what makes it safe where moving the whole card was not.

   z-index so they paint over the panel head they are rising into, and an opaque
   background so the title does not show through them on the way. */
.v2-ro-controls {
    flex: 0 0 auto;
    position: relative;
    z-index: 3;
    background: var(--surface-card);
    border-radius: var(--radius-card) var(--radius-card) 0 0;
}

.v2-ro-search {
    margin: 0;
    padding: var(--space-4, 16px);
}

/* ── THE PRODUCTS, AND THE ONE SCROLLBAR ───────────────────────────────────
   "Scroll bar is in wrong place. should be dedicated to product scrolling."
   This is the only thing in the panel that scrolls, so it is the only thing with
   a bar, and the bar sits against the product list rather than at the panel's
   edge. v2-scroll is the shell's styled-scrollbar class, so it is the design's
   bar and not the OS one.

   THE FADE IS ON THIS BOX, which is also what keeps it out of the basket column.
   "ensure th afde is cutton through the right side column which it currently
   is" — it was, because it was drawn by the search bar, and the search bar spans
   the catalogue card while the gradient bled past it. A mask on the scroll
   region can only ever affect that region: it dissolves the products themselves
   as they pass under the controls, and stops exactly where the column does.

   Proportional to the lift, so at rest the first row is crisp and the fade
   arrives as the controls come up over the title. */
/* ── THE BAR OCCUPIES SPACE, IT DOES NOT TAKE IT ───────────────────────────
   Daniel, 17-Sep: "i have a glitch on the scroll bar showing and hiding rapidly
   - also on such, it too is pushing the catalogue contents. all scroll bars
   should fill space, not push intot it".

   Both symptoms are one cause, and the flicker is the tell. A scrollbar takes
   its width out of the CONTENT box, so: the bar appears, the grid narrows, an
   auto-fill row re-flows, the content gets shorter, the bar is no longer needed,
   it disappears, the grid widens, the row re-flows back, the content gets taller
   — and round again. A feedback loop, not a rendering bug, which is why it
   oscillates rather than settling.

   scrollbar-gutter: stable reserves the width whether or not the bar is showing.
   The content width then never changes, so there is nothing to feed back: no
   shift when it appears, and no loop for it to get caught in. Same fix as the
   basket column earlier today, same reason. */
.v2-ro-products {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    scrollbar-gutter: stable;
    /* 16px, and it stays 16px in every state — see the note below. */
    padding: 0 var(--space-4, 16px) var(--space-4, 16px);
}

/* ── WHY THERE IS NO EXTRA PADDING HERE ────────────────────────────────────
   Two earlier attempts added bottom padding to this scroller when the list was
   spilling, to put a gap under the last row. Both are gone, and the reason is
   worth keeping.

   ANY space inside a scroller is part of scrollHeight. Adding it makes a list
   that only just fitted overflow — which is precisely how Haddenham gained a
   scrollbar it should never have had, from a 4px increase. And when spilling
   only releases 12px of margin (20 -> 8), there is nothing spare to spend here
   anyway: every pixel added back is a pixel of overflow just cleared.

   The gap at the end of a long list is the scroller's own 16px plus the
   container's remaining 8px. That reads as a finish without costing the fit.

   What Daniel actually asked for — "no padding of outer panel as desired at
   bottom of scroll", the container pulling BACK from the panel floor on
   reaching the end — cannot be built. It is a height change keyed on scroll
   position, and a height change moves the scroll position. No dead band helps,
   because the measurement moves by exactly what the rule changes. */

/* The fade under the controls, once there is something passing beneath them.
   Keyed on the scrolled attribute rather than a lift value, because there is no
   lift any more — the catalogue starts where it belongs and stays there. At rest
   the first row is crisp, which is what it should be when nothing has moved. */
.v2-panel[data-v2-panel="request-order"][data-v2-ro-scrolled] .v2-ro-products {
    /* 16px rather than 24: the fade starts where the products do, so a shorter
       ramp brings the point it resolves closer to the tiles above it. */
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 16px);
            mask-image: linear-gradient(to bottom, transparent 0, #000 16px);
}

/* Nothing to stand down under reduced motion any more: the lift has no
   transition to begin with. It tracks the scroll, and a scroll is the user's own
   movement rather than an animation played at them. */

/* ── BACK TO TOP ────────────────────────────────────────────────────────────
   Daniel, 14-Sep: "a back to top button should appear on scroll off".

   Same signal as the folding head. The catalogue now runs 20vh past the fold on
   purpose, so a long brand is a long way down and the way back had to be one
   click rather than a flick.

   A FILL WOULD BE INVISIBLE HERE, so it is carried by a BORDER. It floats over
   .v2-panel-body, which is --surface-sunken, and in dark --surface-card and
   --surface-sunken are the same Flint 600 — a card fill alone would read as no
   button at all. The border separates it in both themes; the fill only has to
   stop the products showing through. (Third time this has caught me: a FILL has
   to know what it is sitting on, a BORDER does not.)

   Quiet on purpose. The basket is the accent and the only loud thing down here;
   this is a convenience beside it, not a second call to action. */
.v2-ro-top {
    position: fixed;
    right: clamp(var(--space-4, 16px), 3vw, var(--space-8, 40px));
    /* It had the basket's floating button stacked under it; the basket is a
       column now, so this gets the corner to itself. */
    bottom: clamp(var(--space-4, 16px), 4vh, var(--space-8, 40px));
    z-index: 4;
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-pill, 999px);
    background: var(--surface-card);
    color: var(--text-body);
    cursor: pointer;
    box-shadow: 0 10px 24px -12px rgb(23 30 32 / 40%);

    /* Away by default. visibility, not display, so it can fade — and unlike
       opacity alone it also drops out of the tab order while it is gone. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(8px);
    transition: opacity .2s ease, transform .2s ease, visibility 0s linear .2s;
}

.v2-panel[data-v2-panel="request-order"][data-v2-ro-scrolled] .v2-ro-top {
    opacity: 1;
    visibility: visible;
    transform: none;
    /* No delay on the way in, or the button arrives late to its own fade. */
    transition: opacity .2s ease, transform .2s ease, visibility 0s;
}

.v2-ro-top:hover {
    border-color: var(--accent-request-order);
    color: var(--accent-request-order);
}

.v2-ro-top:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-top { transition: opacity .01s linear, visibility 0s; }
}


/* ── THE HEADING CARRIES ITS OWN EDIT ──────────────────────────────────────
   Daniel, 20-Sep, twice: "i feel the pen isnt in the right spot against the
   text here?"

   It was not, and baseline alignment is why. The title carries 8px of bottom
   margin, and a flex item with no text of its own — this button holds one SVG
   — takes its baseline from its bottom MARGIN EDGE. So the browser lined the
   pen's bottom up with the title's bottom-plus-margin and the glyph sat exactly
   that 8px high. Baseline was the right instinct for an icon beside words and
   the wrong tool for an icon that has no words in it.

   Centred instead, with the margin moved off the title and onto the ROW. The
   pen then sits on the middle of the line it belongs to, and the space below
   the heading is unchanged because the row now owns it. */
.v2-ro-brands-head {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    margin-bottom: var(--space-2, 8px);
}

.v2-ro-brands-head > .v2-ro-section-title { margin-bottom: 0; }

.v2-ro-brands-edit[hidden] { display: none; }

/* ── THE PEN, NOT A BUTTON WITH A PEN IN IT ────────────────────────────────
   Daniel, 20-Sep: "the icon could be better. it doesnt appear to be central to
   the container and also maybe just have the pen itself in blue?"

   It was not central, and the container is why. .v2-ro-icon-btn draws a 30px
   box because its other users sit on CARDS, where a bare glyph reads as
   decoration and needs an edge to look pressable. This one sits beside a
   heading, where the heading is already the thing it belongs to — so the box
   was borrowing a solution to a problem this instance does not have, and the
   pencil's own bounding box is a hair low and left inside a 24 viewBox, which a
   border draws attention to and nothing else would.

   Taking the box away removes the centring question rather than tuning it, and
   the accent says pressable on its own. The focus ring comes back explicitly,
   because the border was carrying that job too. */
.v2-ro-brands-edit {
    width: auto;
    height: auto;
    padding: 2px;
    border: 0;
    background: none;
    color: var(--accent-request-order);
}

.v2-ro-brands-edit:hover {
    background: none;
    border: 0;
    color: var(--accent-request-order);
    opacity: .75;
}

.v2-ro-brands-edit:focus-visible {
    outline: 2px solid var(--accent-request-order);
    outline-offset: 2px;
    border-radius: var(--radius-button-sm, 6px);
}

/* ── CHOOSING THE SEVEN ────────────────────────────────────────────────────
   Two columns doing two jobs: the strip as it will be on the left, everything
   available on the right. Stacked below the breakpoint, slots first, because
   the slots are the answer and the list is how you change it. */
.v2-ro-strip-cols {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-5, 20px);
    align-items: start;
}

@media (max-width: 640px) {
    .v2-ro-strip-cols { grid-template-columns: minmax(0, 1fr); }
}

.v2-ro-strip-side {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
    min-width: 0;
}

.v2-ro-strip-slots {
    display: grid;
    gap: var(--space-2, 8px);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Every slot is the same height whether or not it holds a brand, so the column
   does not resize as brands are added and removed — the same reason the option
   rows in the picker got a fixed slot. */
.v2-ro-strip-slot {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    min-height: 38px;
    padding: 4px 8px;
    border: 1px solid var(--border-subtle, var(--border-medium));
    border-radius: var(--radius-button, 8px);
    background: var(--surface-card);
}

/* The slot NUMBER, because "the order too slot 1-7" is the point of this
   column — without it the list is just seven brands in a box. */
.v2-ro-strip-slot-n {
    flex: none;
    width: 1.25rem;
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.v2-ro-strip-slot-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.8125rem;
    color: var(--text-strong);
}

.v2-ro-strip-empty {
    flex: 1 1 auto;
    font-size: 0.75rem;
    font-style: italic;
    color: var(--text-muted);
}

.v2-ro-strip-act {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-button-sm, 6px);
    background: none;
    color: var(--text-muted);
    cursor: pointer;
}

.v2-ro-strip-act:hover:not(:disabled) {
    background: var(--surface-inset);
    color: var(--text-strong);
}

/* Dimmed rather than removed, so the row keeps its shape at the ends of the
   list and the buttons stay where the hand learned they were. */
.v2-ro-strip-act:disabled { opacity: .3; cursor: default; }

.v2-ro-strip-all {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
    max-height: 16rem;
    overflow-y: auto;
    padding: 2px;
}

.v2-ro-strip-pick {
    padding: 5px var(--space-3, 12px);
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-pill, 999px);
    background: transparent;
    color: var(--text-body);
    font: inherit;
    font-size: 0.75rem;
    cursor: pointer;
}

.v2-ro-strip-pick:hover:not(:disabled) { border-color: var(--accent-request-order); }

/* On the strip. Filled, because this is the same "chosen" the interval chips
   use and a second treatment for one idea is how two controls stop agreeing. */
.v2-ro-strip-pick.is-on {
    border-color: var(--accent-request-order);
    background: var(--accent-request-order);
    color: var(--text-on-brand);
}

.v2-ro-strip-pick:disabled { opacity: .45; cursor: not-allowed; }


/* ── CENTRED, AND SIZED TO THEIR WORDS ─────────────────────────────────────
   Daniel, 20-Sep: "action buttons at the bottom should be central."

   .v2-modal-actions shares the row between its buttons, which is right where
   they are the two halves of one decision — Cancel and Place order on an order
   worth a page of checking. This dialog is a preference: Cancel and Save are
   small, ordinary words, and two banners across a 900px modal gave them a
   weight the choice does not have. */
.v2-modal[data-v2-modal="ro-strip"] .v2-modal-actions {
    justify-content: center;
}

.v2-modal[data-v2-modal="ro-strip"] .v2-modal-actions > .v2-button {
    flex: 0 0 auto;
    min-width: 140px;
}

/* ── AND THE WAY BACK IS A CONTROL, NOT A FOOTNOTE ─────────────────────────
   Daniel: "back to defaults clearer". It was a text link under the slots,
   which on a dialog full of pressable things read as the one thing that was
   not. It is what a clinician reaches for when she has made a mess of the
   order and wants out of it, so it should look like something to press —
   bordered and centred under the column it undoes, and quiet enough not to
   compete with Save. */
.v2-ro-strip-side > [data-v2-ro-strip-reset] {
    align-self: center;
    margin-top: var(--space-2, 8px);
    padding: 6px var(--space-3, 12px);
    border: 1px solid var(--border-medium);
    border-radius: var(--radius-button, 8px);
    background: none;
    color: var(--text-body);
    font: inherit;
    font-size: 0.75rem;
    cursor: pointer;
}

.v2-ro-strip-side > [data-v2-ro-strip-reset]:hover {
    border-color: var(--accent-request-order);
    color: var(--text-strong);
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE ORDER AS A BOTTOM SHEET — below 1025px
   ═══════════════════════════════════════════════════════════════════════════

   classic's own arrangement, and the one this panel was designed around: its
   three checkout steps live inside #mobile-basket-drawer, a layer that slides
   over a catalogue which never goes away (new-order.php:7430+). Browsing is the
   resting state and the order is a layer on top of it — which is exactly right
   for a job Daniel described as iterative: add a garment, check the basket,
   realise you need the liner, go back.

   WHAT IT REPLACES. Below 1025px the workspace is one column, so the aside was
   stacked UNDER the catalogue. The catalogue's row is bounded to the workspace
   height, so the basket — and Submit with it — sat a full screen below four
   hundred products, reachable only by scrolling past everything already chosen.
   It was not broken so much as unreachable.

   THE SHEET IS OUT OF FLOW, which is what gives the catalogue the whole
   workspace back. Nothing about the desktop column changes: every rule that
   makes it a column is inside @media (min-width: 1025px) already.

   1024px, the same frontier the filter rail uses. Below it a 360px column is
   most of the screen, and the rail is hidden for the same reason.
   ─────────────────────────────────────────────────────────────────────────── */

/* Desktop: neither of these exists. They are declared here rather than inside
   the media query so the hiding is the default and the sheet has to ask. */
.v2-ro-basket-bar,
.v2-ro-sheet-scrim,
.v2-ro-sheet-head { display: none; }

@media (max-width: 1279px) {

    /* ── A RIGHT-HAND STEP, NOT A BOTTOM SHEET ──────────────────────────────
       It was a bottom sheet, built 21-Sep, and it fixed reachability — but the
       Figma mobile frames show Basket as a full screen with a Back button, and
       Daniel settled the model on 22-Sep: "filter should be drawer left and
       basket drawer right with step through logic."

       So it arrives from the RIGHT, opposite the filter step, and covers the
       screen. The direction is what tells you which of the two you are in
       without reading anything; the coverage is what stops a half-visible
       catalogue inviting a mis-tap.

       Full height rather than 88dvh now: with a Back button at the top there is
       a way out that does not depend on leaving a strip of the page showing,
       and the basket needs every pixel it can get once there are six lines and
       an MTM block in it. */
    .v2-ro-aside {
        /* The panel's visible box, as the filter step — see the note there. */
        position: fixed;
        top: var(--v2-ro-box-top, 0px);
        bottom: var(--v2-ro-box-bottom, 0px);
        right: var(--v2-ro-box-right, 0px);
        left: auto;
        width: var(--v2-ro-box-w, 100%);
        max-width: var(--v2-ro-box-w, 100%);
        height: auto;
        z-index: 70;

        /* dvh, not vh, and the reason has not changed with the direction: on
           iOS the URL bar makes vh taller than the screen, so a step sized in
           vh puts its own actions under the chrome — and those actions are
           Submit and Schedule. dvh is the visible box. */
        max-height: none;

        gap: 0;
        padding: 0 var(--space-4, 16px)
                 calc(var(--space-4, 16px) + env(safe-area-inset-bottom, 0px));
        background: var(--surface-page);
        /* The panel's corners where they are on screen, square where they are
           not — the box decides, as for the filter step. */
        border-radius: var(--v2-ro-box-rt, 0px) var(--v2-ro-box-rt, 0px)
                       var(--v2-ro-box-rb, 0px) var(--v2-ro-box-rb, 0px);
        box-shadow: -8px 0 32px rgb(0 0 0 / 22%);

        /* In from the right, a short way, as it fades — the filter step's
           reasoning, mirrored. */
        opacity: 0;
        transform: translateX(24px);
        /* 101% rather than 100%: a subpixel rounding at some zoom levels leaves
           a hairline showing along the edge, which reads as a rendering fault
           rather than a closed drawer. Now on the X axis with the shadow that
           made it necessary. */

        /* VISIBILITY, NOT JUST TRANSFORM. A sheet that is merely translated off
           screen is still in the tab order, so tabbing from the catalogue walks
           into a basket nobody can see. visibility takes it out and comes back
           instantly on open; the delay on the way out is the transform's, so it
           disappears after it has finished sliding rather than during. */
        visibility: hidden;
        transition: transform .28s cubic-bezier(.4, 0, .2, 1),
                    opacity .2s ease,
                    visibility 0s linear .28s;
    }

    /* ── THE DIALOGS INSIDE IT ARE NOT PART OF THE SHEET ───────────────────
       They live inside <aside> in the markup and they open into the TOP LAYER,
       which escapes the sheet's clipping and stacking — but not its inherited
       styles, and visibility inherits. So a closed sheet would make an open
       dialog invisible while still blocking the page behind it.

       That is not hypothetical: the repeat prompt opens on arrival, before the
       sheet has been touched, and the placed dialog stays up after a submit.
       Both would be a black screen with nothing on it. */
    .v2-ro-aside .v2-modal { visibility: visible; }

    .v2-panel[data-v2-ro-basket-open] .v2-ro-aside {
        opacity: 1;
        transform: none;
        visibility: visible;
        transition: transform .28s cubic-bezier(.4, 0, .2, 1),
                    opacity .2s ease,
                    visibility 0s;
    }

    /* The cards keep their own rhythm inside the scrolling part; the sheet's
       padding is the outer frame. */
    .v2-ro-aside-scroll {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
        padding-top: var(--space-2, 8px);
    }

    /* ── THE HEAD: BACK ON THE LEFT, THE TITLE ON THE RIGHT ────────────────
       Daniel, 2-Oct: "on basket move your order to right side and have the
       back button on the left matching filter". It was a grip, a title and a
       ✕ — the grip from when this was a bottom sheet, and a ✕ where the filter
       step has Back. Both steps leave the same way now, with the same control
       in the same corner; the title sits at the far side, which is the side
       the order arrived from. */
    .v2-ro-sheet-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-3, 12px);
        padding: var(--space-4, 16px) 0 var(--space-2, 8px);
        flex: 0 0 auto;
    }

    /* The filter step's title, so the two heads are one design. */
    .v2-ro-sheet-title {
        margin: 0;
        font-size: 1.375rem;
        font-weight: 700;
        color: var(--text-strong);
        text-align: right;
    }

    /* ── SUBMIT STAYS ON THE SHEET, NOT IN IT ──────────────────────────────
       The actions are already outside .v2-ro-aside-scroll and already
       flex: 0 0 auto, so they sit on the sheet's bottom edge whatever the order
       holds. All that is needed here is the rule that separates them from a
       list scrolled up against them, and the safe area under a home indicator —
       Submit is the one control on this screen it must never sit beneath. */
    .v2-ro-actions {
        padding-top: var(--space-3, 12px);
        padding-bottom: calc(var(--space-2, 8px) + env(safe-area-inset-bottom, 0px));
        border-top: 1px solid var(--border-subtle);
    }

    /* ── THE BAR ───────────────────────────────────────────────────────────
       The only way back to the order on a phone, so it is always there and it
       says what is in there. Full width along the bottom edge, above the sheet
       in the stacking order is wrong — it sits BELOW it, so the sheet covers it
       when open rather than floating over its own contents. */
    /* ── A BASKET THAT STAYS WHERE THE THUMB IS ─────────────────────────────
       Daniel, 2-Oct: "The basket is along the bottom and that can get lost on
       scroll on iphone. i suggest moving to a basket icon instead? that pins
       to the screen location? (bottom left)".

       The full-width bar sat on the bottom edge, which is exactly where
       Safari's toolbar comes and goes as the page scrolls — so it was covered
       and uncovered under the finger. A round button in the corner, clear of
       that edge by the safe area and 16px, stays put. Bottom LEFT, his corner;
       back-to-top keeps the right.

       The accent fill and the inverted count are the vocabulary the desktop's
       floating basket used (14-Sep), so this is a returning shape rather than
       a new one. 56px: a thumb target with room to spare. The words are still
       there for a screen reader — the label is hidden visually, not removed. */
    .v2-ro-basket-bar {
        display: grid;
        place-items: center;
        position: fixed;
        left: calc(var(--space-4, 16px) + env(safe-area-inset-left, 0px));
        bottom: calc(var(--space-4, 16px) + env(safe-area-inset-bottom, 0px));
        z-index: 50;
        width: 56px;
        height: 56px;
        padding: 0;
        border: 0;
        border-radius: 999px;
        background: var(--accent-request-order);
        color: var(--text-on-brand);
        font: inherit;
        cursor: pointer;
        box-shadow: 0 10px 24px -10px rgb(23 30 32 / 55%),
                    0 2px 6px rgb(23 30 32 / 18%);
        transition: transform .2s ease, opacity .2s ease, visibility 0s;
    }

    .v2-ro-basket-bar:focus-visible {
        outline: 2px solid var(--text-strong);
        outline-offset: 3px;
    }

    .v2-ro-basket-bar-icon { display: block; }

    /* Away while EITHER step is up: it opens what is already open, and over
       the filter step it would sit on the last rows of the list. */
    .v2-panel[data-v2-ro-basket-open] .v2-ro-basket-bar,
    .v2-panel[data-v2-ro-filters-open] .v2-ro-basket-bar {
        opacity: 0;
        transform: scale(.85);
        visibility: hidden;
        transition: transform .2s ease, opacity .2s ease, visibility 0s linear .2s;
    }

    /* The count on the button's shoulder, inverted so the number is the loudest
       thing there. The ring is the page colour, which is what separates it from
       the button in both themes. */
    .v2-ro-basket-bar-count {
        position: absolute;
        top: -4px;
        right: -4px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 22px;
        height: 22px;
        padding: 0 6px;
        box-sizing: border-box;
        border-radius: 999px;
        background: var(--text-on-brand);
        color: var(--accent-request-order);
        box-shadow: 0 0 0 2px var(--surface-page);
        font-size: 0.75rem;
        font-weight: 700;
        line-height: 1;
        font-variant-numeric: tabular-nums;
    }

    /* An author display beats the browser's [hidden] rule, and this file has
       been caught by that four times — see .v2-ro-mtm-topqty. */
    .v2-ro-basket-bar-count[hidden] { display: none; }

    .v2-ro-basket-bar-label {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* ── THE WORKSPACE GIVES THE BAR ITS HEIGHT ────────────────────────────
       The bar is fixed, so it covers whatever is under it — which is the last
       row of products and, worse, the catalogue's own scrollbar end. Reserved
       from the WORKSPACE rather than padded onto the catalogue card, so the
       card's bottom edge sits above the bar and its internal scroll still ends
       where the card does. */
    /* The button and its 16px, so the last card can always be scrolled clear
       of it. */
    .v2-ro-workspace {
        padding-bottom: calc(88px + env(safe-area-inset-bottom, 0px));
    }

    .v2-ro-sheet-scrim {
        display: block;
        position: fixed;
        top: var(--v2-ro-box-top, 0px);
        bottom: var(--v2-ro-box-bottom, 0px);
        left: var(--v2-ro-box-left, 0px);
        right: var(--v2-ro-box-right, 0px);
        border-radius: var(--v2-ro-box-rt, 0px) var(--v2-ro-box-rt, 0px)
                       var(--v2-ro-box-rb, 0px) var(--v2-ro-box-rb, 0px);
        z-index: 60;
        background: rgb(11 22 26 / 46%);
        opacity: 0;
        pointer-events: none;
        transition: opacity .32s ease;
    }

    /* EITHER step dims what is behind it, so one scrim serves both. Selector
       order does not matter here — the two states are mutually exclusive below
       this width, enforced in request-order-catalogue.js, which is the whole
       point of "youll not need filter when in basket vice versa". */
    .v2-panel[data-v2-ro-basket-open] .v2-ro-sheet-scrim,
    .v2-panel[data-v2-ro-filters-open] .v2-ro-sheet-scrim {
        opacity: 1;
        pointer-events: auto;
    }

    /* ── ON A TABLET THEY ARE DRAWERS AGAIN ─────────────────────────────────
       Daniel, 22-Sep: "im testing on phone, i feel if we can nail mobile,
       tablet will naturally follow." This is that following, and it is why the
       step was built as a transform on a fixed layer rather than a full-screen
       view: capping the width is the only change needed, and nothing about the
       logic, the Back control or the exclusivity moves.

       It also gives the scrim something to do. At full width there is nothing
       of the page left to tap, so "tap outside to close" is not an exit on a
       phone — Back and Escape are. From 641 up there is a margin again, and the
       scrim becomes the third way out it was always meant to be.

       640, which is this stylesheet's existing narrow frontier, rather than a
       new number invented for drawers. */
    @media (min-width: 641px) {
        /* The edge that meets the panel's own edge keeps its corners; the edge
           in the middle of the panel is square, as a drawer's is. */
        .v2-ro-aside {
            max-width: 420px;
            border-radius: 0 var(--v2-ro-box-rt, 0px) var(--v2-ro-box-rb, 0px) 0;
        }
        .v2-ro-filters {
            max-width: 360px;
            border-radius: var(--v2-ro-box-rt, 0px) 0 0 var(--v2-ro-box-rb, 0px);
        }
    }
}

@media (prefers-reduced-motion: reduce) {
    .v2-ro-aside,
    .v2-ro-basket-bar,
    .v2-ro-sheet-scrim { transition: none; }
}

/* ── "ADDED TO THE BASKET — SIZE II | BLACK" ───────────────────────────────
   The variant dialog stays open after an Add (Daniel, 22-Sep), so this line is
   the only thing that says it worked.

   GREEN, on his instruction the same day, and it is the right call on its own
   terms: --intent-success-text is what "No delays expected" already uses in the
   basket two feet away, and the note directly above this one in the same dialog
   is for a combination that does not exist. A success and a refusal wearing the
   same colour is a dialog that has to be read to be understood.

   ── IT NEVER RESIZES THE MODAL ─────────────────────────────────────────────
   "the gap is a little too much and flexes the modal a bit. lets reduce that."

   Both halves, and the second is the one worth stating as a rule: the line is
   ALWAYS in the layout and only its visibility changes. A message that makes
   the dialog jump to announce itself moves the Add button out from under the
   cursor at the exact moment a nurse is adding a second colour — which is the
   whole reason the dialog now stays open.

   Same lesson as the NHS allowance note, which was a four-line paragraph that
   grew the confirm dialog and dragged the address card with it. A message
   reserves its space or it does not get to appear.

   min-height rather than a fixed one, so a long variant label that wraps is
   allowed to — it would be the one case where a jump is the honest answer. */
.v2-ro-var-added {
    /* 4px, and the rest of what was there was never its own: it used to sit as
       a child of .v2-modal-panel, whose 20px row gap stacked on top of this
       margin. It lives under the quantity block now, so the whole distance is
       the one declared here. */
    margin: 4px 0 0;
    min-height: 1.15rem;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--intent-success-text);
    text-align: center;
    visibility: hidden;
}

.v2-ro-var-added.is-on { visibility: visible; }

/* ── THE FACTS STACK; THE BUTTON SITS BESIDE THEM ──────────────────────────
   Daniel, 22-Sep, with three pictures — how it was, how I changed it, and how
   it should be: "it fills the space - doesnt push the text down."

   The facts stack, as they always did: "3 variants" and "1 variant added" are
   two lines about one product and reading them as a pair is the point. What
   changes is that the button stops waiting for them to finish. It shares their
   row, hard right, so the marker appearing FILLS space the card already had
   rather than adding a row underneath it — which is what was moving the control
   on one card while its neighbours stood still.

   My first attempt put the two facts side by side. That held the height and
   lost the reading: they stopped looking like two facts about the same thing
   and started looking like a row of chips. This keeps both.

   flex-end, so the button lands at the foot of the column whether it is one
   line tall or two, rather than floating against the middle of it. */
.v2-ro-product-bottom {
    margin-top: auto;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-2, 8px);
    min-width: 0;
}

.v2-ro-product-facts-col {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* The facts give way, never the control: a long marker ellipses inside its
   share instead of squeezing the button or wrapping to a line that would put
   the height back. */
.v2-ro-product-facts-col > * {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── THE CHOOSER, WHILE IT IS IN ITS OWN DIALOG ────────────────────────────
   The confirm dialog's picker is MOVED here rather than copied (see the note in
   _request-order-aside.php), so it arrives wearing the margin it needs when it
   sits under a card in a column. In a dialog of its own it IS the body, and a
   trailing 12px would read as the panel being badly padded on one side only. */
[data-v2-ro-addr-slot] > .v2-ro-cf-picker {
    margin: 0;
    /* No second border either: the dialog panel is already the surface and the
       card edge, so the picker's own would be a box inside an identical box. */
    padding: 0;
    border: 0;
    background: none;
}

/* ── WHERE SAVED ADDRESSES ARE MANAGED ─────────────────────────────────────
   The last line of the saved-address list. It names where the job is done and
   deliberately does not offer to do it — Daniel, 22-Sep, on his own proposal
   for a button: it "would open up an issue with changing patient details just
   before submitting an order that then relies on the API firing fast for the
   sync up. maybe a helper text only then."

   Muted and unclickable, because a link here is an invitation to leave and
   leaving mid-order is the thing being avoided. */
.v2-ro-cf-saved-note {
    margin: 0;
    padding: var(--space-2, 8px) var(--space-3, 12px);
    border-top: 1px solid var(--border-subtle, var(--border-medium));
    font-size: 0.6875rem;
    line-height: 1.4;
    color: var(--text-muted);
}

/* ══ THE QUERIES LIST HEAD READS LIKE MY ORDERS' ══════════════════════════════
   Daniel, 1-Oct: "the Title isnt as high like the My patients, My orders is ...
   also has no Sub text?"

   This head is --split because it can carry a way back (arriving from an order),
   and --split gives the back control row 1 and the title row 2 whether or not
   the control is there. From the dashboard or the nav there is nowhere to go
   back to, the control is [hidden] (server-side, so this holds from the first
   paint) and row 1 was an empty row plus a row-gap above the title.

   And --split puts .v2-panel-sub in the RIGHT column, because on Order details
   the sub is the order ref. Here it is an ordinary subtitle, so it goes back
   under the title on the left, as on My Orders.

     with a way back      ‹ Back
                          Title                    view toggle
                          Subtitle
                          search + status (full width)
     without              Title                    view toggle
                          Subtitle
                          search + status (full width)

   Scoped to this panel: Order details keeps its ref on the right. */
.v2-panel[data-v2-panel="order-queries"] .v2-panel-head--split .v2-panel-sub {
    grid-column: 1 / -1;   /* the full width: column 1 is narrow here, to line up with the body */
    grid-row: 3;
    justify-self: start;
    align-self: start;
    display: block;
    text-align: left;
    /* A subtitle's size and its 12px from the title at every width, as on My
       Orders. --split shrinks its sub to 12px on touch (it is a ref there) and
       its row gap to 8px; the margin makes up the difference without touching
       the gap the back control shares, which Daniel wants uniform. */
    font-size: var(--type-heading-size);
    /* AS CLOSE TO THE TITLE AS EVERY OTHER PANEL'S (Daniel, 13-Oct: "bring
       the sub header up closer to the title to match all other pages"). The
       plain head puts its sub 12px - 14px = 2px ABOVE the title box's foot
       (.v2-panel-title + .v2-panel-sub); this sat a full 12px below it, 14px
       lower. The same box gap here, whatever the head's row gap is. */
    margin-top: calc(-2px - var(--v2-head-gap));
}
@media (max-width: 640px) {
    /* The phone's plain head: 12px - 12px = 0. */
    .v2-panel[data-v2-panel="order-queries"] .v2-panel-head--split .v2-panel-sub { margin-top: calc(0px - var(--v2-head-gap)); }
}
.v2-panel[data-v2-panel="order-queries"] .v2-panel-head--split [data-v2-head-below] { grid-row: 4; }

.v2-panel[data-v2-panel="order-queries"] .v2-panel-head--split:has(.v2-panel-back[hidden]) .v2-panel-title,
.v2-panel[data-v2-panel="order-queries"] .v2-panel-head--split:has(.v2-panel-back[hidden]) .v2-panel-head-aside { grid-row: 1; }
.v2-panel[data-v2-panel="order-queries"] .v2-panel-head--split:has(.v2-panel-back[hidden]) .v2-panel-sub { grid-row: 2; }
.v2-panel[data-v2-panel="order-queries"] .v2-panel-head--split:has(.v2-panel-back[hidden]) [data-v2-head-below] { grid-row: 3; }
/* INSIDE A THREAD THE SUBTITLE IS THE ORDER LINE, "Patient | SO#...", and it
   sits opposite the back control, above New query and Assigned to — Daniel,
   5-Oct: "would sit above the New Query / Allocation". It was display:none
   here from 1-Oct, on the reasoning that the subtitle describes the list; that
   was true of the list's own line and wrong for this one, so a thread on
   desktop lost the only thing saying which order and patient it was about.

   The list's line still never shows in a thread: only the order line carries
   [data-v2-q-ref] (queries.js setSub). Above 900px only: at 900 and below the
   head is hidden in a thread and .v2-q-thread-order carries the same line. */
.v2-panel[data-v2-panel="order-queries"][data-view="thread"] .v2-panel-sub:not([data-v2-q-ref]) { display: none; }
.v2-panel[data-v2-panel="order-queries"][data-view="thread"] .v2-panel-head--split .v2-panel-sub[data-v2-q-ref] {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    align-self: center;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    text-align: right;
    margin-top: 0;
}
/* With no back control there is no row 1 to share, so the line takes row 1
   alone and the title and the controls move down beneath it. */
.v2-panel[data-v2-panel="order-queries"][data-view="thread"] .v2-panel-head--split:has(.v2-panel-back[hidden]):has(.v2-panel-sub[data-v2-q-ref]) .v2-panel-title,
.v2-panel[data-v2-panel="order-queries"][data-view="thread"] .v2-panel-head--split:has(.v2-panel-back[hidden]):has(.v2-panel-sub[data-v2-q-ref]) .v2-panel-head-aside { grid-row: 2; }

/* THE LIST TAKES THE ORDINARY COLUMNS. The narrow first column (min(32%, 300px),
   above) exists so a THREAD's New query and Assigned to line up with the cards
   under them. On the list the head holds a title and the view toggle, and the
   narrow column wrapped "Order queries" onto two lines. :not(thread) rather
   than [data-view="list"] so it holds before queries.js has written the view.
   The row gap goes back to the head's own 12px too: the thread's 20px is the
   body grid's, and it put the subtitle 8px further from the title than on
   My Orders. */
.v2-panel[data-v2-panel="order-queries"]:not([data-view="thread"]) .v2-panel-head--split {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: var(--v2-head-gap);
}

/* AT 900 AND BELOW THE LIST HEAD STACKS, as My Orders' does (.v2-panel-head--control
   goes to one column at this width): title, subtitle, then the view toggle under
   them, then search. Left beside the title, a phone's title (28px) is shorter
   than the toggle (34px), so the toggle set the row's height and pushed the
   subtitle 6px further down than on My Orders. */
@media (max-width: 900px) {
    .v2-panel[data-v2-panel="order-queries"]:not([data-view="thread"]) .v2-panel-head--split {
        grid-template-columns: minmax(0, 1fr);
    }
    .v2-panel[data-v2-panel="order-queries"]:not([data-view="thread"]) .v2-panel-head--split .v2-panel-head-aside {
        grid-column: 1;
        grid-row: 4;
        justify-self: start;
        margin-top: calc(var(--space-3) - var(--v2-head-gap));   /* 12px under the subtitle, as My Orders */
    }
    .v2-panel[data-v2-panel="order-queries"]:not([data-view="thread"]) .v2-panel-head--split [data-v2-head-below] { grid-row: 5; }

    .v2-panel[data-v2-panel="order-queries"]:not([data-view="thread"]) .v2-panel-head--split:has(.v2-panel-back[hidden]) .v2-panel-head-aside { grid-row: 3; }
    .v2-panel[data-v2-panel="order-queries"]:not([data-view="thread"]) .v2-panel-head--split:has(.v2-panel-back[hidden]) [data-v2-head-below] { grid-row: 4; }
}

/* ── MTM OPTION SKUs (Jake, classic df3e20d4) ─────────────────────────────
   "JLES1 | Adhesive Border". The code in the muted tone so the name still
   leads, and the bar as a quiet separator rather than punctuation. */
.v2-ro-line-option-sku { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.v2-ro-line-option-sep { color: var(--text-muted); margin: 0 6px; }

/* ── THE SCHEMA CONFIRM (Jake, classic 2997119d) ──────────────────────────
   Takes the dialog while it shows, as the velcro helper does. The list is the
   order being confirmed: the garment, then each option, one row each. */
.v2-ro-schema-confirm[hidden] { display: none; }
.v2-ro-schema-confirm-title {
    margin: 0;
    font-size: var(--type-heading-size);
    font-weight: 700;
    color: var(--text-strong);
    overflow-wrap: anywhere;
}
.v2-ro-schema-confirm-text {
    margin: var(--space-2, 8px) 0 0;
    font-size: 0.875rem;
    line-height: 1.5;
    color: var(--text-body);
}
.v2-ro-schema-confirm .v2-ro-schema-confirm-list {
    list-style: none;
    margin: var(--space-4, 16px) 0 0;
    padding: var(--space-3, 12px) var(--space-4, 16px);
    border: 1px solid var(--border-subtle, var(--border-medium));
    border-radius: var(--radius-button, 12px);
}
.v2-ro-schema-confirm-list li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3, 12px);
    padding: 3px 0;
    font-size: 0.8125rem;
    min-width: 0;
}
/* The garment is the first row and the subject; the options sit under it. */
.v2-ro-schema-confirm-list li:first-child { font-weight: 600; }
.v2-ro-schema-confirm-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
    color: var(--text-body);
}
.v2-ro-schema-confirm-qty {
    flex: 0 0 auto;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
/* PHONES: the two answers share the row at full width, 44px tall. */
@media (max-width: 640px) {
    .v2-ro-schema-confirm .v2-ro-velcro-actions > .v2-button { flex: 1 1 0; min-height: 44px; }
}/* The footer and the ✕ set their own display, which beats [hidden]; while the
   schema confirm shows they must actually go, or there are four ways out. */
.v2-modal[data-v2-modal="ro-variant"] .v2-modal-actions[hidden],
.v2-modal[data-v2-modal="ro-variant"] .v2-modal-close[hidden] { display: none; }

/* NOTHING MOVES SIDEWAYS IN CHOOSE OPTIONS (Daniel, 13-Oct: "the fields
   still seem to shift sometimes ... they should always be hidden on these
   modals. should be no shift left to right at all on selections"). Opening a
   dropdown lifts the body's clip, which stops it being a scroll container, and
   its reserved gutter (stable both-edges, the shared modal rule) went with it:
   the fields widened 12px each side and settled back on close. No bar and no
   gutter here, in either state, so the width never changes. It still scrolls
   by wheel, touch and keyboard. */
.v2-modal[data-v2-modal="ro-variant"] .v2-modal-body {
    scrollbar-gutter: auto;
    scrollbar-width: none;
}
.v2-modal[data-v2-modal="ro-variant"] .v2-modal-body::-webkit-scrollbar { display: none; width: 0; height: 0; }
/* And so no stand-in for it either. The shared rule above pays the reserved
   gutter back as 12px of padding while a dropdown is open, for a body that
   loses its gutter then; this one never has a gutter, so that padding was
   the shift itself (Daniel, 13-Oct, with the two screenshots: fields 20px in
   closed, 32px open). Nothing either side, in both states. */
.v2-modal[data-v2-modal="ro-variant"] .v2-modal-body:has(.v2-combo [aria-expanded="true"]) { padding-inline: 0; }

/* CANCEL AND ADD SPLIT THE ROW 50/50 (Daniel, 13-Oct: in the made-to-measure
   options "the cancel, add buttons are not centralised - either centralise
   or span width of modal and split 50/50"). The shared 260px cap left the
   pair sitting left in the wider made-to-measure modal. */
.v2-modal[data-v2-modal="ro-variant"] .v2-modal-actions > .v2-button { max-width: none; }

/* ── THE SCHEMA CONFIRM, ONE SHAPE (Daniel, 1-Oct) ────────────────────────
   "would be nicer to have a more rigid modal ... both came up very different
   widths". The picker is 420 or 760 by its option count (sizePicker); while the
   confirm shows it is one width, Jake's classic 520, so every schema question
   looks the same. Below 600px the panel already fills the screen. */
.v2-modal[data-v2-modal="ro-variant"][data-v2-ro-confirming] .v2-modal-panel {
    width: min(92vw, 520px);
    max-width: min(92vw, 520px);
}
.v2-modal[data-v2-modal="ro-variant"] [data-v2-ro-var-name][hidden] { display: none; }

/* The garment sits apart from its options: "slightly separate the main garment
   from the options in the box". A hairline and a little air, not a heading. */
.v2-ro-schema-confirm .v2-ro-schema-confirm-list li:first-child {
    padding-bottom: var(--space-2, 8px);
    margin-bottom: var(--space-2, 8px);
    border-bottom: 1px solid var(--border-subtle, var(--border-medium));
}

/* Back to Dashboard takes its own row under the two next steps, full width. */
.v2-ro-placed-actions > .v2-ro-placed-away { flex-basis: 100%; }

/* ════════════════════════════════════════════════════════════════════════════
   REQUEST ORDER BELOW 1025 — THE 2-OCT SANITY PASS
   ════════════════════════════════════════════════════════════════════════════
   Daniel, 2-Oct, on the mobile/tablet review: "we do want good UX aswell as UI
   so lets adjust accordingly on mobile. but on spec."

   AT THE END OF THE FILE ON PURPOSE. Every rule here adjusts one declared
   earlier at the same specificity, and a later rule is the only kind that
   wins without a heavier selector — the trap this file has paid for more than
   once (the filter step lost to its own base rule for a fortnight). Nothing
   above 1024 changes unless a rule here says "all widths". */

/* ── THE TAP IS 44px; THE LOOK IS UNCHANGED ─────────────────────────────────
   The mobile frames use 44px throughout (1300:4964, 1300:4992), and this
   shell already holds that floor for its own controls. These were the ones
   below it — the quantity steppers at 22, every dialog's ✕ at 32, the brand
   strip's pencil at about 20.

   An invisible ::after centred on each control is the hit area, so nothing
   grows on screen and nothing around it moves. It is clipped by any ancestor
   that clips, which only ever makes it smaller — never wrong. */
@media (max-width: 1279px) {
    .v2-ro-qty-btn,
    .v2-modal-close,
    .v2-ro-hint-btn,
    .v2-ro-strip-act,
    .v2-ro-cal-nav,
    .v2-ro-brands-edit,
    .v2-ro-crumb,
    .v2-ro-cf-change,
    .v2-ro-form-pick,
    .v2-ro-onpage-open { position: relative; }

    .v2-ro-qty-btn::after,
    .v2-modal-close::after,
    .v2-ro-hint-btn::after,
    .v2-ro-strip-act::after,
    .v2-ro-cal-nav::after,
    .v2-ro-brands-edit::after,
    .v2-ro-crumb::after,
    .v2-ro-cf-change::after,
    .v2-ro-form-pick::after {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        width: max(100%, 44px);
        height: max(100%, 44px);
        transform: translate(-50%, -50%);
    }

    /* The steppers' neighbours are a number or a field. 12px between them keeps
       the − and + hit areas off the field either side of it, so a tap on the
       quantity box lands in the box. */
    .v2-ro-mtm-qty-row { gap: var(--space-3, 12px); }

    /* ── THE FILTER STEP IS A LIST, AND A LIST ON A PHONE IS 44px A ROW ───
       The rows were ~25px: desktop's dense rail, carried into a full screen
       a thumb has to hit. Taller rows, same type. */
    .v2-ro-filter-row,
    .v2-ro-filter-summary,
    .v2-ro-filter-brand {
        min-height: 44px;
        box-sizing: border-box;
    }
    .v2-ro-filter-brand {
        display: flex;
        align-items: center;
    }
    .v2-ro-filter-clear { min-height: 44px; }

    /* "Keep filters open" is a desktop preference — it decides whether the
       rail is there when the panel arrives. Below 1025 the filters are a step
       you go into and come out of, so there is nothing for it to keep open
       (request-order-catalogue.js no longer applies it here either). */
    .v2-ro-filter-pin { display: none; }

    /* The toggle beside the search, at the 44px the search beside it is. */
    .v2-ro-filter-toggle {
        width: 44px;
        height: 44px;
    }

    /* The variant dialog's made-to-measure option rows. */
    .v2-ro-opt-tick { min-height: 44px; }

    /* ── THE RESULTS FILTER: AN ICON ON THE LINE, OPENING TO ITS LEFT ─────
       Daniel, 2-Oct: "move the results search bar to an icon to the right of
       the 'results for "Juzo"' on click expands to the left but stays inline?
       losing this wrap".

       One line, always: the title takes what is left and ends in an ellipsis
       rather than pushing the field to a second row. Closed, the field has no
       width and no paint, and is out of the tab order (visibility); open, it
       grows leftwards from the button, because the button does not move. The
       button reads ✕ while open — clear, and close. */
    .v2-ro-results-head {
        flex-wrap: nowrap;
        align-items: center;
    }

    /* The title gives way first, but never to nothing: "Results for…" still
       says what the line is. */
    .v2-ro-results-head > .v2-ro-section-title {
        flex: 1 1 auto;
        min-width: 6.5rem;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .v2-ro-onpage {
        flex: 0 1 auto;
        min-width: 0;
        gap: var(--space-1, 4px);
    }

    /* The field is the part that shrinks inside the filter's half of the
       line, so the ✕ never leaves the edge of the grid. */
    .v2-ro-onpage-field {
        flex: 1 1 auto;
        min-width: 0;
        max-width: 0;
        width: 0;
        min-height: 44px;
        height: 44px;
        padding-left: 0;
        padding-right: 0;
        border-width: 0;
        opacity: 0;
        visibility: hidden;
        transition: width .24s cubic-bezier(.4, 0, .2, 1),
                    max-width .24s cubic-bezier(.4, 0, .2, 1),
                    padding .24s cubic-bezier(.4, 0, .2, 1),
                    opacity .16s ease,
                    visibility 0s linear .24s;
    }

    .v2-ro-onpage[data-v2-ro-onpage-open] .v2-ro-onpage-field {
        width: 15rem;
        max-width: 15rem;
        padding-left: 30px;
        padding-right: var(--space-3, 12px);
        border-width: 1px;
        opacity: 1;
        visibility: visible;
        transition: width .24s cubic-bezier(.4, 0, .2, 1),
                    max-width .24s cubic-bezier(.4, 0, .2, 1),
                    padding .24s cubic-bezier(.4, 0, .2, 1),
                    opacity .16s ease .04s,
                    visibility 0s;
    }

    /* Room for the count inside the field while there is one. */
    .v2-ro-onpage[data-v2-ro-onpage-open]:has(.v2-ro-onpage-count:not(:empty)) .v2-ro-onpage-field {
        padding-right: 4.75rem;
    }

    /* The button is the clear on a phone; the browser's own ✕ would be a second
       one a centimetre away. */
    .v2-ro-onpage-field::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }

    .v2-ro-onpage-icon { opacity: 0; transition: opacity .16s ease; }
    .v2-ro-onpage[data-v2-ro-onpage-open] .v2-ro-onpage-icon { opacity: 1; }

    /* The count sits inside the field's right end, so it never needs a line of
       its own. Pinned dark like the magnifier: the field is white in both
       themes. */
    .v2-ro-onpage-count {
        position: absolute;
        right: calc(44px + var(--space-1, 4px) + var(--space-3, 12px));
        top: 50%;
        transform: translateY(-50%);
        color: #6B7B82;
        pointer-events: none;
    }
    .v2-ro-onpage:not([data-v2-ro-onpage-open]) .v2-ro-onpage-count { display: none; }

    .v2-ro-onpage-open {
        flex: none;
        display: grid;
        place-items: center;
        width: 44px;
        height: 44px;
        padding: 0;
        border: 0;
        border-radius: var(--radius-button, 8px);
        background: none;
        color: var(--text-body);
        cursor: pointer;
    }
    .v2-ro-onpage-open:hover { background: var(--surface-sunken); color: var(--text-strong); }
    .v2-ro-onpage-open:focus-visible {
        outline: 2px solid var(--focus-ring, var(--colour-harbour-500));
        outline-offset: 2px;
    }
    .v2-ro-onpage-open-clear { display: none; }
    .v2-ro-onpage[data-v2-ro-onpage-open] .v2-ro-onpage-open-find { display: none; }
    .v2-ro-onpage[data-v2-ro-onpage-open] .v2-ro-onpage-open-clear { display: block; }

    /* ── BACK TO TOP, WHEN THE PAGE IS WHAT SCROLLS ───────────────────────
       Below 1025 the panel grows with its products and the page scrolls, so
       the panel body's own scroll position — which is what showed this — never
       moves. request-order-catalogue.js sets data-v2-ro-page-scrolled from the
       page instead. Clear of a home indicator, in the right-hand corner; the
       basket has the left. */
    .v2-panel[data-v2-panel="request-order"][data-v2-ro-page-scrolled] .v2-ro-top {
        opacity: 1;
        visibility: visible;
        transform: none;
        transition: opacity .2s ease, transform .2s ease, visibility 0s;
    }
    .v2-ro-top {
        right: calc(var(--space-4, 16px) + env(safe-area-inset-right, 0px));
        bottom: calc(var(--space-4, 16px) + 6px + env(safe-area-inset-bottom, 0px));
    }
    /* The steps meet the bottom of their box. The 20px buffer the columns
       keep on desktop (A BUFFER UNTIL THERE IS A REASON) is a margin, and on a
       fixed step a bottom margin is a strip of the page showing under Submit. */
    .v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] .v2-ro-aside,
    .v2-panel[data-v2-panel="request-order"][data-v2-ro-stage="work"] .v2-ro-filters {
        margin: 0;
    }

    /* Gone while a step is up: it scrolls a catalogue that is not on screen. */
    .v2-panel[data-v2-ro-basket-open] .v2-ro-top,
    .v2-panel[data-v2-ro-filters-open] .v2-ro-top {
        opacity: 0;
        visibility: hidden;
    }
}

/* ── THE PAGE STAYS STILL WHILE A STEP IS UP ───────────────────────────────
   Set on <html> by request-order-catalogue.js while the filter or basket step
   is open below 1025. A finger on a step's head used to scroll the catalogue
   behind it, which then moved the step's own box. The gutter is kept so a
   window with a classic scrollbar does not shift sideways as it goes. */
html[data-v2-ro-lock],
html[data-v2-ro-lock] body {
    overflow: hidden;
}
html[data-v2-ro-lock] { scrollbar-gutter: stable; }

.v2-ro-filter-groups,
.v2-ro-aside-scroll { overscroll-behavior: contain; }

/* ── THE STAFF QUESTIONS: ANSWERS ONLY, AND THEY FIT (all widths) ──────────
   12rem tracks need 192px each, and on a 320px phone the card had about 164
   to give, so the second question ran off the side. min() lets a track be the
   whole row when the row is narrower than one. */
.v2-ro-selection .v2-ro-staff {
    grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
}

/* ── THE SCHEMA CONFIRM NEVER RUNS OFF THE SCREEN (all widths) ─────────────
   Daniel, 2-Oct: "make it fixed so doesnt go offscreen. with the escape
   always visble and in good design."

   While it shows, the dialog refuses Escape and hides its ✕, so Go back is the
   way out — and on a landscape phone a garment with a dozen options pushed
   Go back below the screen. Now the heading, the sentence and the two buttons
   stay put and only the list scrolls, inside its own rounded box, so both
   answers are always on screen however long the order. */
.v2-ro-schema-confirm:not([hidden]) {
    display: flex;
    flex-direction: column;
    flex: 0 1 auto;
    min-height: 0;
}
.v2-ro-schema-confirm .v2-ro-schema-confirm-list {
    flex: 0 1 auto;
    min-height: 4.5rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.v2-ro-schema-confirm .v2-ro-velcro-actions { flex: none; }

/* ── THE SCHEDULE PRESETS, TWO BY TWO ON A PHONE ───────────────────────────
   Four equal tracks at 375px are about 74px each, and "12 months" needs 88.
   Two rows of two give every label its room; the ellipsis the old note
   promised is there now as well, for a longer label one day. */
.v2-ro-cal-quick-btn {
    overflow: hidden;
    text-overflow: ellipsis;
}
@media (max-width: 640px) {
    .v2-ro-cal-quick { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .v2-ro-cal-quick-btn { min-height: 44px; }
}

/* ── ON A PHONE, A DIALOG IS THE SCREEN'S WIDTH ────────────────────────────
   The phone rule (max-width 600, .v2-modal-panel) makes every dialog full
   width — but three of Request Order's set their own width with heavier
   selectors and kept 92vw, a few pixels short of the edges for no reason.
   Restated here at their own weight: "make them full width ofcourse". */
@media (max-width: 600px) {
    .v2-modal[data-v2-modal="ro-confirm"] .v2-modal-panel,
    .v2-modal[data-v2-modal="ro-variant"][data-v2-ro-axes="1"] .v2-modal-panel,
    .v2-modal[data-v2-modal="ro-variant"][data-v2-ro-axes="2"] .v2-modal-panel,
    .v2-modal[data-v2-modal="ro-variant"][data-v2-ro-confirming] .v2-modal-panel {
        width: 100%;
        max-width: 100%;
    }
}

/* Under 360px the field is too short to hold the count and the words being
   typed; the words win. The grid still shows what was filtered. */
@media (max-width: 359px) {
    .v2-ro-onpage-count { display: none; }
    .v2-ro-onpage[data-v2-ro-onpage-open]:has(.v2-ro-onpage-count:not(:empty)) .v2-ro-onpage-field {
        padding-right: var(--space-3, 12px);
    }
}

/* While the schema confirm shows, the picker's body is empty — every part of it
   has stepped aside — but it still took its row and the panel's gap either
   side, which on a landscape phone was the height of three list rows. The
   confirm "takes the dialog", so the body goes with everything else in it. */
.v2-modal[data-v2-modal="ro-variant"][data-v2-ro-confirming] .v2-modal-body { display: none; }


/* ════════════════════════════════════════════════════════════════════════════
   REQUEST ORDER BELOW 1025 — DANIEL'S 2-OCT ROUND TWO, ON A REAL iPHONE
   ════════════════════════════════════════════════════════════════════════════ */

/* Empty on desktop, and a flex row with a gap would still spend it. */
.v2-ro-subject-slot:empty { display: none; }

@media (max-width: 1279px) {

    /* ── THE PANEL REACHES THE BOTTOM OF THE SCREEN, WHATEVER IT HOLDS ─────
       "the panel should always span the page downward, not cut if the panel
       itself is short (like it is when you havent selected anything yet)".
       Short, it ended halfway down with wallpaper and a floating basket
       underneath. --v2-ro-floor is the panel's distance from the top of the
       page, measured by request-order-catalogue.js; lvh, the tallest the
       screen gets, so it still meets the bottom with Safari's bars tucked
       away. Its bottom edge is then always off the screen, so it is square —
       the shell's rule for a panel that runs off the page. */
    /* Phones and tablets only: from 1025 the shell caps the panel to the
       screen and its body scrolls, so there is no page to reach the floor of. */
    @media (max-width: 1024px) {
        .v2-panel[data-v2-panel="request-order"] {
            min-height: calc(100lvh - var(--v2-ro-floor, 0px));
            border-bottom-left-radius: 0;
            border-bottom-right-radius: 0;
        }
    }

    /* ── THE BASKET AND BACK-TO-TOP FLOAT WHERE SAFARI CANNOT REACH ────────
       "I can seem to slightly scroll the page causing the basket in the
       bottom left to get lost behind a footer on iphone? (also should be
       bottom right) - it should float always visible".

       Placed from the TOP of the SMALL viewport — the screen with Safari's
       toolbar fully out — rather than from the bottom of a viewport that
       grows and shrinks under the toolbar as the page moves. So it sits
       above the toolbar in both states. Bottom RIGHT, his corner; back to
       top stacks above it on the same edge, centred on it. */
    .v2-ro-basket-bar {
        left: auto;
        right: calc(var(--space-4, 16px) + env(safe-area-inset-right, 0px));
        bottom: auto;
        top: calc(100svh - 56px - var(--space-6, 24px) - env(safe-area-inset-bottom, 0px));
    }

    .v2-ro-top {
        right: calc(var(--space-4, 16px) + 6px + env(safe-area-inset-right, 0px));
        bottom: auto;
        top: calc(100svh - 56px - var(--space-6, 24px) - var(--space-3, 12px) - 44px
                  - env(safe-area-inset-bottom, 0px));
    }

    /* ── THE BREADCRUMB GIVES WAY ON ITS OWN LINE ──────────────────────────
       "on the search expanding, it should [not] cause the breadcrumb to
       wrap. just go to breadcrumb ...". The crumbs are a wrapping flex row,
       so the title's ellipsis never reached them. One line of inline pieces
       instead, cut with an ellipsis. The padding and the matching negative
       margin are room for the crumbs' 44px tap areas inside the clip. */
    .v2-ro-results-head > .v2-ro-crumbs {
        display: block;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        padding-block: 12px;
        margin-block: -12px;
    }
    .v2-ro-results-head > .v2-ro-crumbs > * + * { margin-left: var(--space-2, 8px); }

    /* ── THE CHIPS: WHO, AND WHERE IT IS GOING ─────────────────────────────
       The subject card's two rows, drawn as the search field's shape — white,
       44px, round — while they sit above it. Closed: icon and one line, the
       control hidden. Open: the whole row's width, the whole value, and the
       control back (Change for the patient, the pencil for the address). */
    .v2-ro-subject-slot { margin-bottom: var(--space-3, 12px); }

    .v2-ro-subject-slot .v2-ro-subject-wrap {
        width: auto;
        max-width: none;
        justify-self: stretch;
    }

    .v2-ro-subject-slot .v2-ro-subject {
        padding: 0;
        background: none;
        border: 0;
        border-radius: 0;
    }

    .v2-ro-subject-slot .v2-ro-resolved {
        flex-direction: row;
        flex-wrap: wrap;
        gap: var(--space-2, 8px);
    }

    .v2-ro-subject-slot .v2-ro-row {
        display: flex;
        align-items: center;
        gap: var(--space-2, 8px);
        flex: 1 1 0;
        min-width: 0;
        min-height: 44px;
        box-sizing: border-box;
        padding: 0 var(--space-4, 16px);
        border: 1px solid var(--border-subtle);
        border-radius: 999px;
        background: var(--surface-page);
    }

    .v2-ro-subject-slot .v2-ro-row > :first-child {
        flex: 1 1 auto;
        min-width: 0;
        min-height: 42px;
        display: flex;
        align-items: center;
        cursor: pointer;
        border-radius: inherit;
    }
    .v2-ro-subject-slot .v2-ro-row > :first-child:focus-visible {
        outline: 2px solid var(--focus-ring, var(--colour-harbour-500));
        outline-offset: 2px;
    }

    .v2-ro-subject-slot .v2-ro-facts .v2-ro-item { flex: 1 1 auto; min-width: 0; }

    .v2-ro-subject-slot .v2-ro-row:not(.is-open) .v2-ro-item-value {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        overflow-wrap: normal;
    }
    .v2-ro-subject-slot .v2-ro-row:not(.is-open) .v2-ro-item-nhs { display: none; }
    .v2-ro-subject-slot .v2-ro-row:not(.is-open) > .v2-ro-icon-btn { display: none; }

    .v2-ro-subject-slot .v2-ro-row.is-open {
        order: -1;
        flex-basis: 100%;
        padding: var(--space-1, 4px) var(--space-1, 4px) var(--space-1, 4px) var(--space-4, 16px);
        border-radius: var(--radius-card, 12px);
    }

    /* ── A TAPPED ROW DOES NOT STAY GREY ───────────────────────────────────
       iOS keeps :hover on whatever was last tapped, so a brand picked in the
       filter step kept its hover shade after the step had done its job. */
    @media (hover: none) {
        .v2-ro-filter-brand:hover {
            background: none;
            color: var(--text-body);
        }
    }
}

@media (max-width: 1279px) {
    /* The chips share the search row's inset, so the two lines start and end
       on the same edges; the search row's own top padding is the gap. */
    .v2-ro-subject-slot {
        margin-bottom: 0;
        padding: var(--space-4, 16px) var(--space-4, 16px) 0;
    }

    /* The facts wrap on desktop (name above NHS when the column is tight);
       a chip is one line that gives way with an ellipsis instead. */
    .v2-ro-subject-slot .v2-ro-facts {
        flex: 1 1 auto;
        flex-wrap: nowrap;
        min-width: 0;
        overflow: hidden;
        justify-content: flex-start;
    }
    .v2-ro-subject-slot .v2-ro-row > :first-child { justify-content: flex-start; }
    .v2-ro-subject-slot .v2-ro-item { min-width: 0; overflow: hidden; }
    .v2-ro-subject-slot .v2-ro-row:not(.is-open) .v2-ro-item-value { flex: 1 1 auto; }
}
@media (max-width: 1279px) {
    /* The card stacks its facts in a column with the items centred, which in
       a chip centred an over-long name on both sides of the clip. A chip's
       facts are a row. */
    .v2-ro-subject-slot .v2-ro-facts {
        flex-direction: row;
        align-items: center;
    }
}

/* ── LONG OPTION NAMES ON TOUCH ──────────────────────────────────────────────
   They carry the full name as a tooltip now (Daniel, 2-Oct), but a phone has
   no hover to show one, so there the name wraps instead of being cut. */
@media (hover: none) {
    .v2-ro-opt-name {
        white-space: normal;
        overflow-wrap: anywhere;
    }
}

/* ── THE CHIPS ARE A DIAL, NOT A WRAP (Daniel, 2-Oct) ───────────────────────
   "they are 50/50 on rest great, but when clicked they push the other to
   another row. it should just push it out into just an icon, clicking the
   other than collapses the last. so its like a dial type animation, and
   clicking off puts them back to 50/50. this way you click one to open, then
   its action button to trigger. or it will fold back."

   One row, always. At rest both share it. Open, the open one takes the row
   and the other folds to its icon in a 44px circle; tapping that icon turns
   the dial the other way. The value of a folded chip is still in the DOM —
   only its width and paint go — so the swap animates rather than jumps.
   request-order.js puts has-open on the row and closes it on a tap anywhere
   else or Escape. Overrides the wrap-to-full-width version above it. */
@media (max-width: 1279px) {
    .v2-ro-subject-slot .v2-ro-resolved { flex-wrap: nowrap; }

    .v2-ro-subject-slot .v2-ro-row {
        transition: flex-grow .32s cubic-bezier(.4, 0, .2, 1),
                    flex-basis .32s cubic-bezier(.4, 0, .2, 1),
                    padding .32s cubic-bezier(.4, 0, .2, 1),
                    border-radius .32s cubic-bezier(.4, 0, .2, 1);
    }

    .v2-ro-subject-slot .v2-ro-row.is-open {
        order: 0;
        flex: 1 1 0;
        flex-basis: 0;
        border-radius: var(--radius-card, 12px);
    }

    .v2-ro-subject-slot .v2-ro-resolved.has-open > .v2-ro-row:not(.is-open) {
        flex: 0 0 44px;
        padding: 0;
        justify-content: center;
    }

    .v2-ro-subject-slot .v2-ro-resolved.has-open > .v2-ro-row:not(.is-open) > :first-child {
        justify-content: center;
    }

    /* Folded to the icon: the words go, the icon stays centred. */
    .v2-ro-subject-slot .v2-ro-resolved.has-open > .v2-ro-row:not(.is-open) .v2-ro-item {
        justify-content: center;
        gap: 0;
    }
    .v2-ro-subject-slot .v2-ro-resolved.has-open > .v2-ro-row:not(.is-open) .v2-ro-item-value,
    .v2-ro-subject-slot .v2-ro-resolved.has-open > .v2-ro-row:not(.is-open) .v2-ro-item-nhs {
        flex: 0 0 0;
        width: 0;
        min-width: 0;
        padding: 0;
        border: 0;
        opacity: 0;
    }

    /* The open one shows everything: the whole value, wrapped, and the NHS
       number beside the name. */
    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item-value {
        white-space: normal;
        overflow-wrap: anywhere;
    }

    @media (prefers-reduced-motion: reduce) {
        .v2-ro-subject-slot .v2-ro-row { transition: none; }
    }
}
@media (max-width: 1279px) {
    /* A folded chip stays a 44px circle beside a taller open one, centred on
       its first line rather than stretched to its height. */
    .v2-ro-subject-slot .v2-ro-resolved { align-items: flex-start; }
    .v2-ro-subject-slot .v2-ro-resolved.has-open > .v2-ro-row:not(.is-open) {
        height: 44px;
        border-radius: 999px;
    }

    /* Open, the NHS number never truncates — the card's rule: "half an NHS
       number is a different patient". A long name takes its own line and the
       number moves under it instead of being cut. */
    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item {
        flex-wrap: wrap;
        row-gap: 2px;
        padding-block: var(--space-2, 8px);
    }
    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item-nhs {
        flex: 0 0 auto;
        overflow: visible;
        text-overflow: clip;
    }
}
@media (max-width: 1279px) {
    /* Icon left, name over the number — not a wrapping row, which put the icon
       on a line of its own. */
    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        column-gap: var(--space-2, 8px);
        row-gap: 2px;
        align-items: center;
    }
    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item-icon { grid-row: 1 / span 2; }
    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item-value { grid-column: 2; }
    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item-nhs {
        grid-column: 2;
        justify-self: start;
    }
}

/* ── THE REPEAT'S STATE, BESIDE THE ORDER'S (2-Oct) ─────────────────────────
   v2_repeat_tag(): a second tag on the order card and the patient's order
   card, and on Order details beside "Order progress". The two sit as one group
   at the head's end; on a narrow card they stack rather than squeeze the name. */
.v2-record-tags {
    flex: 0 1 auto;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--space-1, 4px) var(--space-2, 8px);
    min-width: 0;
}
.v2-od-repeat-tag { margin-left: auto; align-self: center; }

/* "Repeat of …" / "Repeats as …" (Jake, 16-Oct): the repeat's two orders,
   linked both ways as classic's header badges are. A tag that is a button —
   it opens the other order in this panel — so it says so on hover, and on a
   phone its 24px plate carries a 44px tap area. */
.v2-od-repeat-link {
    position: relative;
    gap: 6px;
    align-self: center;
    font-family: inherit;
    cursor: pointer;
}
.v2-od-repeat-link svg { flex: none; }
.v2-od-repeat-link:hover { text-decoration: underline; text-underline-offset: 2px; }
.v2-od-repeat-link:focus-visible {
    outline: 2px solid var(--focus-ring, var(--colour-harbour-500));
    outline-offset: 2px;
}
@media (max-width: 640px) {
    .v2-od-repeat-link::after { content: ""; position: absolute; inset: -10px -4px; }
}
/* With a link beside the title and the repeat's status tag, the row can be
   wider than a phone: it wraps rather than overflowing. */
.v2-od-progress-head { flex-wrap: wrap; row-gap: var(--space-2, 8px); }

/* ── THE OPTIONS DIALOG, A LITTLE WIDER (Daniel, 2-Oct) ─────────────────────
   "Could make the options modal slightly wider also.... so more text shows
   naturally anyway? not a mad amount but a little." It sized to its content
   at about 600px, which cut most option names at 18 characters. 720, and two
   columns held — a wider auto-fill would have spent the room on a third
   column of names just as short. Not while the schema confirm holds it at
   520, and not on a phone, where it is the screen's width. */
@media (min-width: 601px) {
    .v2-modal[data-v2-modal="ro-variant"]:not([data-v2-ro-confirming]):has(.v2-ro-opt-list) .v2-modal-panel {
        width: min(92vw, 720px);
        max-width: min(92vw, 720px);
    }
    .v2-modal[data-v2-modal="ro-variant"] .v2-ro-opt-list {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* ── THE CHIPS HOLD THEIR HEIGHT (Daniel, 2-Oct) ────────────────────────────
   "The name / NHS number button in new order flexes to 2 rows, it shouldnt
   change height at all. just span in the relevant direction... and the icon
   for the inactive state should be the same as the expanded one (not a
   rectangle and a circle)."

   So opening is purely sideways: one line, 44px, the same pill shape as the
   folded chip beside it. The extra width shows more of the value; what still
   does not fit ends in an ellipsis — the NAME gives way, never the NHS number
   (the card's own rule). Undoes the two-line grid and the card radius above. */
@media (max-width: 1279px) {
    .v2-ro-subject-slot .v2-ro-resolved { align-items: center; }

    .v2-ro-subject-slot .v2-ro-row.is-open {
        height: 44px;
        border-radius: 999px;
        padding: 0 var(--space-1, 4px) 0 var(--space-4, 16px);
    }

    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item {
        display: flex;
        flex-wrap: nowrap;
        align-items: center;
        gap: var(--space-2, 8px);
        padding-block: 0;
    }
    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item-icon { grid-row: auto; }

    .v2-ro-subject-slot .v2-ro-row.is-open .v2-ro-item-value {
        flex: 1 1 auto;
        min-width: 0;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        overflow-wrap: normal;
    }

    /* The control inside an open chip is a circle in the pill's end. */
    .v2-ro-subject-slot .v2-ro-row.is-open > .v2-ro-icon-btn {
        width: 36px;
        height: 36px;
        border-radius: 999px;
        flex: none;
    }
}
@media (max-width: 1279px) {
    .v2-ro-subject-slot .v2-ro-row.is-open > .v2-ro-icon-btn {
        position: relative;
        width: 36px;
        height: 36px;
    }
    .v2-ro-subject-slot .v2-ro-row.is-open > .v2-ro-icon-btn::after {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        width: 44px;
        height: 44px;
        transform: translate(-50%, -50%);
    }
}
/* Two tags must not take the name's room: capped, so on a narrow card they
   stack at the head's end and the name keeps more than half the row. */
.v2-record-tags { max-width: 55%; }

/* ── A LIST ROW IS ONE LINE: NAME AND STATUS ────────────────────────────────
   Daniel, 2-Oct, on a list row where two tags had pushed the patient's name
   out of its column: "at this level should still just be Name with [status]
   pill". The row view holds the name and the status in a 220px column on one
   line by design (components.css, "An ORDER row"), and there is no room for a
   second tag there. The repeat's state stays on the grid card and in Order
   details. */
.v2-record-grid[data-view="list"] .v2-tag--repeat { display: none; }
.v2-record-grid[data-view="list"] .v2-record-tags {
    flex: none;
    max-width: none;
    flex-wrap: nowrap;
}

/* ── AND ON A DESKTOP WITH LESS THAN 1600 TO GIVE (Daniel, 5-Oct) ───────────
   From 1280 the panel is the desktop layout again, and with the rail pinned
   the catalogue is what pays. Two answers that need no mode switch:
   - the basket column comes in to 300px below 1600, where every pixel of
     catalogue counts;
   - the brand strip asks the CATALOGUE how wide it is, not the screen: under
     640px it is the swipe row of four the tablet uses, so seven tiles are never
     squeezed into a narrow column, at any screen width. A container query, so
     it follows the rail opening and closing on its own. */
@media (min-width: 1280px) and (max-width: 1599px) {
    .v2-panel[data-v2-panel="request-order"] { --v2-ro-aside-w: 300px; }
}

.v2-ro-catalogue { container: ro-cat / inline-size; }

@container ro-cat (max-width: 640px) {
    .v2-ro-brand-strip {
        display: flex;
        grid-template-columns: none;
        gap: var(--space-2, 8px);
        overflow-x: auto;
        overflow-y: hidden;
        scroll-snap-type: x mandatory;
        scrollbar-width: none;
        align-items: flex-start;
    }
    .v2-ro-brand-strip::-webkit-scrollbar { display: none; }
    .v2-ro-brand {
        flex: 0 0 calc((100% - (var(--space-2, 8px) * 3)) / 4);
        scroll-snap-align: start;
    }
    .v2-ro-brand-tile { border-radius: 14.6% / 31.9%; }
    .v2-ro-brand-name {
        width: 100%;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .v2-ro-brand-strip[data-fade="end"] {
        -webkit-mask-image: linear-gradient(to right, #000 82%, transparent 100%);
                mask-image: linear-gradient(to right, #000 82%, transparent 100%);
    }
    .v2-ro-brand-strip[data-fade="start"] {
        -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 18%);
                mask-image: linear-gradient(to right, transparent 0%, #000 18%);
    }
    .v2-ro-brand-strip[data-fade="both"] {
        -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 18%, #000 82%, transparent 100%);
                mask-image: linear-gradient(to right, transparent 0%, #000 18%, #000 82%, transparent 100%);
    }
}
