/* ==========================================================================
   Dark Water — PHONE LAYER
   --------------------------------------------------------------------------
   This file is the phone-native experience. It is NOT a fork of the site and
   it is NOT a pile of patches on top of styles.css; it is one deliberate
   layer that takes over below 768px and rebuilds the parts of the desktop
   design that are physically wrong on a handset.

   Why a separate file instead of more @media blocks inside styles.css:
   styles.css already carries 111 media blocks across ~18 ad-hoc widths
   (400/420/520/560/600/620/640/680/720/760/820/860/900/940/980/1180/1200/1560).
   Adding a nineteenth ad-hoc width to that pile is how the phone got broken in
   the first place — nobody could tell which of six overlapping blocks was
   winning. Everything phone-specific now lives here, at ONE breakpoint, and it
   loads after styles.css so it wins on cascade order rather than on
   !important. The desktop site is untouched: delete this link tag and the
   1440px design is byte-for-byte what it was.

   Breakpoint: 767.98px. Not 768px — a 767.5px logical viewport exists on
   some Android split-screen modes and a max-width:768px query would also
   catch an iPad mini in portrait, which wants the tablet layout, not this.

   The rules that govern every decision below:
     1. 44x44pt minimum for anything tappable (Apple HIG). Not 40. Not 38.
     2. 16px minimum on any focusable input, or iOS Safari zooms the page on
        focus and the user is left panning around a zoomed form.
     3. Nothing may exceed 100vw. Ever. One 401px element in a 393px viewport
        makes the ENTIRE page scroll sideways, and the user reads that as
        "the site is broken", not "one card is 8px wide".
     4. env(safe-area-inset-*) everywhere something is fixed or sticky, so the
        notch, the Dynamic Island and the home indicator never sit on content.
     5. No horizontal scrollers for primary content. A carousel on a desktop
        is a design device; on a phone it is content the user never finds,
        because they scroll vertically and the second card does not exist to
        them.
     6. Sophistication is kept. Gradients, glass, the mark, the typography and
        the motion all stay. What goes is the desktop plumbing: cursor
        interactions, hover-reveal, 3D scenes that cost 4MB on cellular, and
        two-column grids that were never going to collapse.
   ========================================================================== */

/* ==========================================================================
   1. TOUCH-DEVICE TRUTHS — any width, any device with no real pointer
   --------------------------------------------------------------------------
   These are not phone-width rules, they are pointer rules. A 1024px iPad has
   no cursor either, and telling it to "move your cursor" is just as wrong.
   ========================================================================== */
@media (hover:none), (pointer:coarse) {

  /* The desktop custom cursor and its follower ring have no meaning without a
     pointer, and on touch they render as a dot stuck in the top-left corner. */
  .cursor, .cursor-dot, .cursor-ring, .cursor__ring, #cursor { display:none !important; }
  html { cursor:auto !important; }

  /* Hover-only reveals must be visible by default on touch, because the
     hover state that would have revealed them can never fire. Anything that
     depends on :hover to become readable is, on a phone, invisible content. */
  .show-card .show-copy,
  .quad__tile .quad__copy,
  .work-tile__c,
  .pcard__body { opacity:1 !important; transform:none !important; }

  /* Kill hover lift/scale so a tap does not leave an element stuck in its
     hover state until the user taps somewhere else (the "sticky hover" bug). */
  .btn:hover, .show-card:hover, .quad__tile:hover, .card:hover,
  .menu-toggle:hover, .theme-toggle:hover, .ptile:hover, .mprod:hover {
    transform:none;
  }
}

/* ==========================================================================
   2. THE PHONE LAYER
   ========================================================================== */
@media (max-width:767.98px) {

  /* ------------------------------------------------------------------------
     2.1 Foundation
     ------------------------------------------------------------------------ */
  :root {
    /* 60 is the iOS navigation-bar height. The desktop 78px bar eats a fifth
       of a 852pt screen before any content exists. */
    --header-h:60px;
    --dw-gut:20px;                              /* one gutter, used everywhere */
    --dw-safe-l:env(safe-area-inset-left,0px);
    --dw-safe-r:env(safe-area-inset-right,0px);
    --dw-safe-t:env(safe-area-inset-top,0px);
    --dw-safe-b:env(safe-area-inset-bottom,0px);
  }

  html {
    /* Stops iOS from inflating type in landscape, which silently breaks every
       fixed-width calculation on the page. */
    -webkit-text-size-adjust:100%;
    text-size-adjust:100%;
  }

  html, body {
    max-width:100%;
    /* `clip` not `hidden`: overflow-x:hidden on a scroll container silently
       promotes overflow-y to `auto`, which kills position:sticky on the
       header and breaks scroll anchoring. `clip` does not. The `hidden`
       line stays as the fallback for anything older than iOS 16. */
    overflow-x:hidden;
    overflow-x:clip;
    overscroll-behavior-x:none;                 /* no rubber-band sideways */
  }

  /* The single most valuable line in this file. A grid or flex child defaults
     to min-width:auto, meaning it refuses to shrink below its content — one
     long unbroken word or one wide image and the whole column blows past the
     viewport. Every "text cut off mid-word beside a giant logo" defect on the
     old phone build traces back to this default. */
  main *, footer * { min-width:0; }

  /* Nothing may be wider than its box, whatever it is. */
  img, svg, video, canvas, iframe, embed, object, table, pre {
    max-width:100%;
  }
  img, video { height:auto; }

  /* Long URLs, emails and product SKUs are the classic overflow source in
     otherwise-fine paragraphs. */
  p, li, td, th, dd, h1, h2, h3, h4, blockquote { overflow-wrap:anywhere; }
  h1, h2, h3 { hyphens:none; }                  /* but never hyphenate headings */

  /* One gutter for the whole site, notch-aware in landscape. */
  .container {
    padding-inline:
      calc(var(--dw-gut) + var(--dw-safe-l))
      calc(var(--dw-gut) + var(--dw-safe-r));
    max-width:100%;
  }

  /* ------------------------------------------------------------------------
     2.2 Type scale — phone-native, not scaled-down desktop
     ------------------------------------------------------------------------
     The desktop scale tops out around 3.5rem. At 393px a 3.5rem headline is
     four words per line with a river down the middle. These caps are chosen
     so the longest real headline on the site ("Stop Managing Software. Start
     Running Your Business.") sets in three balanced lines.
     ------------------------------------------------------------------------ */
  h1, .h1 { font-size:clamp(2rem, 8.6vw, 2.55rem); line-height:1.08; letter-spacing:-.02em; }
  h2, .h2 { font-size:clamp(1.65rem, 7vw, 2.05rem); line-height:1.14; letter-spacing:-.015em; }
  h3      { font-size:clamp(1.18rem, 4.8vw, 1.4rem); line-height:1.24; }
  h4      { font-size:1.02rem; }
  .lead   { font-size:1.02rem; line-height:1.6; max-width:none; }
  body    { font-size:1rem; line-height:1.62; }

  /* text-wrap:balance on headings is the single cheapest way to stop the
     orphaned-last-word problem ("PARTNER" alone on line 3 of the hero). */
  h1, h2, h3, .section-head h2, .hero__mono-title { text-wrap:balance; }
  .lead, p.lead { text-wrap:pretty; }

  /* Centred desktop copy reads as unanchored on a narrow column. Left-align
     everything except the deliberate hero/section-head lockups. */
  .section > .container > *:not(.section-head):not(.center) { text-align:left; }

  /* ------------------------------------------------------------------------
     2.3 Vertical rhythm — the page-length problem
     ------------------------------------------------------------------------
     index.html measured 31,759px tall on a phone: thirty-seven full screens.
     Most of that is desktop section padding (clamp(70px,9vw,130px) top AND
     bottom, twenty-seven times) surviving into a 393px viewport where it
     buys nothing but thumb mileage.
     ------------------------------------------------------------------------ */
  .section          { padding-block:clamp(44px, 11vw, 64px); }
  .section--tight   { padding-block:clamp(34px, 8.5vw, 48px); }
  .section-head     { margin-bottom:20px; }
  .section-head h2  { margin-bottom:.5rem; }
  .mt-2 { margin-top:.8rem; } .mt-3 { margin-top:1.1rem; }

  /* Grids: one column, one gap value, no exceptions. */
  .grid, .cols-2, .cols-3, .cols-4,
  .quad__grid, .footer-grid, .steps, .stats,
  .pillars, .scene-grid, .ledger__grid, .wfeat, .osrep__grid {
    display:grid !important;
    grid-template-columns:1fr !important;
    gap:14px !important;
  }
  /* Small stat/KPI clusters read better two-up than one-up — they are short
     numbers, not paragraphs. */
  .stats, .proof__kpis, .dkpi-row {
    grid-template-columns:1fr 1fr !important;
    gap:10px !important;
  }

  /* ---- and inside each of those cells, the number goes ABOVE the label ----
     styles.css:1564 makes a proof KPI a baseline-aligned flex ROW — number,
     gap, label — which is right in a 300px sidebar and impossible in a 157px
     half-cell. Measured at 390px: the `b` holding "$842K" was allotted 40px
     while its own nowrap text is about 100px, so the number ran straight
     through "Revenue attributed". Same for "99.99%" over "Uptime, always" and
     "4.2s→0.8s" over "Load time, rebuilt" — all three KPIs unreadable.

     The `b` shrank because `main * { min-width:0 }` above removes the
     min-width:auto that normally stops a flex item collapsing under its
     content. That rule earns its place ten times over and is not the thing to
     change; the row is. Stacking also matches the stat card the portal uses,
     which is the vocabulary this page is being brought up to. */
  .proof__kpis > div {
    flex-direction:column; align-items:flex-start; gap:.15rem;
  }
  .proof__kpis b { flex:0 0 auto; }
  .proof__kpis span { line-height:1.35; }

  /* Three KPIs in two columns leaves the third alone beside a hole, which
     reads as a card that failed to load rather than as a set. An odd last
     cell takes the full width instead. Scoped to :nth-child(odd) so that a
     fourth KPI added later falls back to a clean 2x2 with no rule to undo. */
  .proof__kpis > div:nth-child(odd):last-child { grid-column:1 / -1; }

  /* ---- the three-up hero stat row -------------------------------------
     .hero__stats was in the two-up list above, which did nothing: it is
     display:flex in styles.css:1096, and grid-template-columns on a flex box
     is inert. So on a phone it stayed a wrapping flex row of three spans
     sized by their own labels. Measured on designs.html at 393px: "90+ / LIVE
     IN THE VAULT" 154px wide, "100% / FREE TO USE" 106px, and "8 /
     COLLECTIONS" pushed onto a second line at 112px — three numbers in a 2+1
     rag, each one centred over a label of a different width, with the 10px
     gap the dead rule did manage to set leaving the two labels almost
     touching. It reads like a wrap accident, not a stat row. index.html
     carries the same component with the same three-item shape.

     Three items is a row of three, so make it one: equal thirds, hairline
     rules between, number over label. minmax(0,1fr) caps each track — the
     labels are tracked .18em uppercase and "Live in the vault" is wider than
     a third of the screen, so it has to be allowed to wrap rather than push
     the track. Once it can wrap, the numbers and the labels have to be held
     on their own lines, which is what the subgrid does: every number shares
     the first track, every label the second, however many lines the label
     takes. Below 360px three columns stop being honest — the label wraps to
     three lines — so it steps down to two. */
  .hero__stats {
    display:grid !important;
    grid-template-columns:repeat(3, minmax(0,1fr)) !important;
    gap:0 !important;
    justify-content:stretch;
    margin-top:1.8rem;
  }
  .hero__stats > span {
    justify-items:center; text-align:center;
    padding:0 8px; gap:.25rem;
    border-left:1px solid var(--hairline);
  }
  .hero__stats > span:first-child,
  .hero__stats > span:nth-child(3n+1) { border-left:0; }
  .hero__stats b { font-size:1.55rem; line-height:1.1; }
  @supports (grid-template-rows:subgrid) {
    .hero__stats > span {
      grid-template-rows:subgrid; grid-row:span 2; align-content:start;
    }
  }
  @media (max-width:359.98px) {
    .hero__stats { grid-template-columns:repeat(2, minmax(0,1fr)) !important; }
    .hero__stats > span:nth-child(2n+1) { border-left:0; }
    .hero__stats > span:nth-child(n+3) { margin-top:14px; }
  }

  /* ---- two-up stats: the labels have to line up ------------------------
     Two-up only works if the two cells in a row read as one row. On
     about.html the second band is 98 / 100% / WCAG 2.2 / 1 wk, and "WCAG
     2.2" measured 142px of text inside a 140px content box — so it broke to
     two lines, its own cell grew from 41px to 83px, and its label dropped to
     y10646 while the label beside it stayed at y10605. Four numbers, three
     labels on one line and one 41px lower: the band stopped looking
     designed and started looking like it had reflowed by accident.

     Two changes, in the order they matter. First give the number room:
     16px of side padding on a 172px cell is desktop padding on a phone, and
     6px is plenty when the cell has no border or background to sit off. The
     type step that used to live at ≤400px is widened to the whole phone
     range and given a ceiling, because at 430px the old clamp floor of
     2.1rem put "WCAG 2.2" at ~186px inside 178px and it wrapped there too —
     the 400px breakpoint was hiding the same bug on a larger iPhone.

     The row-level alignment that keeps the labels on one line when a value
     still does wrap — at 320px "WCAG 2.2" has nowhere to go — is the subgrid
     rule in styles.css §5b, which applies at every width. */
  .stat { padding-inline:6px; }
  .stat b {
    font-size:clamp(1.5rem, 6.4vw, 1.75rem);
    line-height:1.15;
  }

  /* A 16/11 tile is a desktop shape. work.html carries fifteen .work-tile
     panels; at 353px wide that aspect makes each one 242px tall to hold about
     90px of label, so every tile opened with ~130px of empty gradient above its
     own caption and the page spent roughly 2.3 screens on nothing. These tiles
     have no photograph — styles.css gives them an inline linear-gradient and a
     radial sheen, and the .work-tile--photo variant is not used anywhere — so
     the empty area is not a picture waiting to load, it is just air. Letting the
     tile hug its caption turns fifteen empty boxes into fifteen label plates and
     keeps the gradient as a deliberate colour field rather than a void. Any
     future photo tile keeps its aspect, because a photo needs the room. */
  .work-tile:not(.work-tile--photo) {
    aspect-ratio:auto;
    padding:18px 18px 20px;
  }

  /* Flex rows of buttons stack full-width and become genuinely tappable. */
  .flex, .footer-cta__btns, .quad__btns, .hero__cta, .prc__cta {
    display:flex; flex-direction:column; align-items:stretch; gap:10px;
  }
  .flex > .btn, .footer-cta__btns > .btn, .quad__btns > .qbtn { width:100%; }

  /* ------------------------------------------------------------------------
     2.4 Tap targets
     ------------------------------------------------------------------------
     Audited failures this fixes: "Open menu 40x40", "Close 38x38", the 124x39
     nav pills, the 141x42 plan CTAs, the 159x39 "Compare every plan" link,
     "Back to top 42x42", and the 156x16 footer links.
     ------------------------------------------------------------------------ */
  .btn, button, [role="button"], .qbtn, .show-pill, .prc__tab, .osmx__cta {
    min-height:46px;
    /* Width matters as much as height and is the half people forget: the
       "Free" filter chip on designs.html measured 42x61 — tall enough, and
       still a miss. */
    min-width:46px;
    display:inline-flex; align-items:center; justify-content:center;
    padding-inline:1.15rem;
    font-size:.97rem;
    /* Removes the 300ms tap delay and the grey flash on iOS. */
    touch-action:manipulation;
    -webkit-tap-highlight-color:transparent;
  }
  .btn--lg { min-height:52px; font-size:1rem; }
  .btn--sm { min-height:44px; }

  /* The desktop rule at styles.css:2955 shrinks these to 40px on narrow
     screens — exactly backwards. A phone needs them BIGGER, not smaller. */
  .menu-toggle, .theme-toggle, .show-nav, .pmodal__x, #chatClose {
    width:44px !important; height:44px !important;
    display:grid !important; place-items:center;
    /* The `button` in the touch-target rule above hands every one of these
       icon buttons padding-inline:1.15rem. With box-sizing:border-box and a
       fixed 44px width that leaves a 7.2px content box, so a 24-grid SVG was
       being squeezed to a 7x22 sliver — the drawer's close control read as a
       scratch in an empty tile, and so did the theme toggle, the carousel
       arrows, the modal close and the chat close. An icon button is sized by
       its box, never by padding. */
    padding:0 !important;
    touch-action:manipulation;
  }

  /* Footer links were 16px tall — a 16px target next to five other 16px
     targets is a guaranteed mis-tap. */
  .site-footer a, .footer-bottom a {
    display:inline-block;
    min-height:44px; line-height:44px;
    padding-block:0;
  }
  .site-footer li { margin:0; }
  .social a { width:44px; height:44px; display:grid; place-items:center; }

  /* Anything that is a link but renders as a text run gets vertical padding
     so the touch target is taller than the glyphs. */
  .card__link, .osmx__more, .faq-q { min-height:44px; display:flex; align-items:center; }

  /* ------------------------------------------------------------------------
     2.5 Forms — the iOS zoom rule
     ------------------------------------------------------------------------
     Any focusable field below 16px makes iOS Safari zoom the viewport on
     focus and NOT zoom back out. The user is then stranded on a page that is
     wider than the screen, mid-form. This is the single most common reason a
     mobile form is abandoned. Audited failures: text@13.3, website@13.3,
     select-one@15.4, stage@13.3.
     ------------------------------------------------------------------------ */
  input:not([type="hidden"]), select, textarea, .input, .field__in {
    font-size:16px;                             /* never below. not 15.9. */
    min-height:48px;
    width:100%;
    max-width:100%;
    border-radius:12px;
    padding:.75rem .9rem;
  }
  textarea { min-height:120px; line-height:1.55; }
  select { min-height:48px; padding-right:2.2rem; background-position:right .8rem center; }
  input[type="checkbox"], input[type="radio"] {
    width:22px; height:22px; min-height:0; flex:0 0 auto;
  }
  /* A checkbox is only 22px, so the LABEL carries the tap target. */
  label:has(> input[type="checkbox"]), label:has(> input[type="radio"]),
  .check, .check-row { display:flex; align-items:center; gap:.7rem; min-height:44px; }

  .form-row, .form-grid, .field-row {
    display:grid !important; grid-template-columns:1fr !important; gap:12px !important;
  }
  /* Submit buttons go full width — the thumb is at the bottom of the screen
     and a right-aligned 141px button is the hardest thing on the page to hit. */
  form .btn[type="submit"], form button[type="submit"] { width:100%; }

  /* ------------------------------------------------------------------------
     2.6 Header — a real iOS navigation bar
     ------------------------------------------------------------------------ */
  .site-header {
    height:calc(var(--header-h) + var(--dw-safe-t));
    padding-top:var(--dw-safe-t);               /* content clears the notch */
    background:rgba(5,7,13,.86);
    -webkit-backdrop-filter:saturate(180%) blur(18px);
    backdrop-filter:saturate(180%) blur(18px);
    border-bottom:1px solid var(--hairline);
  }
  .site-header .nav {
    display:flex; align-items:center; justify-content:space-between;
    gap:10px; width:100%;
  }

  /* The eight-item primary nav and its three dropdown menus are a pointer
     interaction. They live in the drawer on a phone, where every one of the
     twenty-five destinations is a 48px row instead of a hover target. */
  .nav__links, .has-dropdown, .dropdown { display:none !important; }

  /* Two actions in the bar, not three: one CTA and the menu. "Dashboard"
     is the least-tapped of the three and it is the first row of the drawer. */
  .nav__actions { display:flex; align-items:center; gap:8px; margin-left:auto; }
  .nav__actions .btn--callnav { display:none; }
  .nav__actions .btn--aqua {
    min-height:38px; padding-inline:.85rem; font-size:.84rem;
    letter-spacing:.01em; white-space:nowrap;
  }
  .menu-toggle { display:grid !important; }

  /* Brand lockup: the mark plus the name. The "Business Operating Systems"
     descender is what pushes the bar past 393px. */
  .brand { gap:.55rem; min-width:0; }
  .brand__icon { width:32px; height:32px; }
  .brand__sub { display:none; }
  .brand__name { font-size:.86rem; letter-spacing:.14em; white-space:nowrap; }

  /* Anchor targets must clear the sticky bar or the heading lands underneath
     it. This is why in-page nav "did nothing" on a phone. */
  :target, [id] { scroll-margin-top:calc(var(--header-h) + var(--dw-safe-t) + 12px); }

  /* ------------------------------------------------------------------------
     2.7 Drawer — full-screen sheet
     ------------------------------------------------------------------------ */
  .drawer {
    padding:
      calc(14px + var(--dw-safe-t))
      calc(var(--dw-gut) + var(--dw-safe-r))
      calc(24px + var(--dw-safe-b))
      calc(var(--dw-gut) + var(--dw-safe-l));
    background:#05070D;
    /* Momentum scrolling, and the drawer's own scroll never leaks to the
       page underneath it. */
    -webkit-overflow-scrolling:touch;
    overscroll-behavior:contain;
    /* 100dvh, not 100vh: on iOS Safari 100vh is the height WITHOUT the URL
       bar, so a 100vh drawer is ~90px taller than the screen and its last
       row sits under the browser chrome. */
    height:100dvh;
  }
  /* The drawer is now a three-part sheet — head, scrolling list, pinned action
     bar — instead of one long scroll. styles.css already makes .drawer a flex
     column; all this does is stop the SHEET from scrolling and hand the scroll
     to the list, so the head and the actions stay put.

     The padding moves off .drawer and onto its three children, because a pinned
     bar has to reach the physical bottom of the screen to look pinned: leaving
     the 24px bottom padding on .drawer would float the bar above a dead strip. */
  .drawer { overflow:hidden; padding:0; }
  .drawer__top {
    flex:0 0 auto; margin-bottom:.35rem;
    padding:calc(14px + var(--dw-safe-t))
            calc(var(--dw-gut) + var(--dw-safe-r)) 12px
            calc(var(--dw-gut) + var(--dw-safe-l));
    border-bottom:1px solid rgba(255,255,255,.06);
  }
  .drawer__brand { gap:.55rem; }
  .drawer__brand .brand__icon { width:30px; height:30px; }
  .drawer__brand .brand__name { font-size:.84rem; letter-spacing:.14em; }
  /* 46x46 is a fine tap target, but I.x is drawn on a 24-unit grid and was
     inheriting a 15.5px font size, so the glyph read as a 10px scratch inside a
     large empty tile — the least confident-looking control in the whole UI. */
  .drawer__top .menu-toggle { flex:0 0 auto; }
  .drawer__top .menu-toggle svg { width:22px; height:22px; stroke-width:2.2; }

  .drawer nav {
    flex:1 1 auto; min-height:0;
    overflow-y:auto; -webkit-overflow-scrolling:touch;
    overscroll-behavior:contain;
    /* 34px of bottom padding, not 18, so the last row clears the scrim added
       below. Without it the final link would sit under the fade at the bottom
       of the scroll and look half-erased at the one moment it should be
       clearest. */
    padding:.2rem calc(var(--dw-gut) + var(--dw-safe-r)) 34px calc(var(--dw-gut) + var(--dw-safe-l));
  }
  .drawer nav a {
    min-height:50px;
    display:flex; align-items:center;
    padding:0 .2rem;
    font-size:1.02rem;
    border-bottom:1px solid rgba(255,255,255,.06);
  }
  .drawer nav a:active { background:rgba(59,158,255,.1); }
  /* A sticky label with a background exactly its own height slices whatever
     row is passing under it clean through the middle of the glyphs, which reads
     as a rendering fault rather than as a header ("INDUSTRIES" was sitting on
     the top half of "Dental"). Bleeding the background out to the drawer edges
     and giving it a hairline makes it a bar: the row underneath goes fully
     behind something, which is what a sticky header is supposed to look like. */
  .drawer__group-label {
    margin:1.5rem 0 0;
    padding:.75rem calc(var(--dw-gut) + var(--dw-safe-l)) .5rem;
    margin-inline:calc(-1 * (var(--dw-gut) + var(--dw-safe-l)));
    position:sticky; top:0; z-index:2;
    background:#05070D;
    box-shadow:0 1px 0 rgba(255,255,255,.07), 0 10px 12px -8px #05070D;
  }

  /* The pinned action bar. A top hairline plus a slight lift separates it from
     the list it is sitting over, and the safe-area inset keeps the primary CTA
     off the home indicator. */
  .drawer__cta {
    flex:0 0 auto; margin:0;   /* the desktop base adds margin-top:1.4rem, which
                                  opened a 23px dead strip above a pinned bar */
    position:relative; z-index:3;
    display:flex; flex-direction:column; gap:10px;
    padding:14px calc(var(--dw-gut) + var(--dw-safe-r))
            calc(14px + var(--dw-safe-b)) calc(var(--dw-gut) + var(--dw-safe-l));
    background:linear-gradient(180deg, rgba(5,7,13,.86), #05070D 42%);
    border-top:1px solid rgba(255,255,255,.08);
    box-shadow:0 -18px 26px -20px #000;
  }
  .drawer__cta .btn { width:100%; margin:0; min-height:50px; }
  /* The menu is 30 links in a 634px window, so 18 of them are below the fold —
     and at 393x852, the most common iPhone size, the fold falls 1px into
     "Branding & Identity". A 1px clip is not a clip: the list reads as ending
     there, and more than half the navigation is invisible with nothing to
     suggest it exists.

     A hairline plus a drop shadow says "the bar is on top of something". It
     does not say "keep going". A gradient does, because a row that dissolves
     into the bar is a row that is continuing past it — the same cue a scroll
     indicator gives, without waiting for the user to already be scrolling.
     bottom:100% pins it to the 30px directly above the bar, so it tracks the
     bar at any screen height instead of guessing where the fold lands, and
     pointer-events:none keeps it off the links underneath. */
  .drawer__cta::before {
    content:""; position:absolute; left:0; right:0; bottom:100%; height:30px;
    background:linear-gradient(180deg, rgba(5,7,13,0), rgba(5,7,13,.72) 55%, #05070D);
    pointer-events:none;
  }

  /* ------------------------------------------------------------------------
     2.8 Hero — composed for a 393x852 screen
     ------------------------------------------------------------------------
     Was: a 2.065:1 wordmark scaled until it bled off both edges, a headline
     touching the viewport edge, an orphaned "PARTNER", a clipped app-icon
     row, and a min-height:100vh that guaranteed the fold landed mid-sentence.
     ------------------------------------------------------------------------ */
  .hero {
    min-height:auto;
    padding-block:clamp(30px, 8vw, 48px) clamp(40px, 10vw, 60px);
    overflow:clip;
  }
  .hero__inner { text-align:center; }

  .markwrap { max-width:82%; margin-inline:auto; }
  .hero__brandmark, .hero__brandmark--mono, .markpass {
    height:auto !important;
    width:100%; max-width:260px;
    margin-inline:auto;
  }
  .hero__mono-title {
    font-size:clamp(1.95rem, 8.2vw, 2.45rem);
    line-height:1.1;
    margin:1.1rem 0 .9rem;
  }
  .hero__pillars {
    font-size:.8rem; letter-spacing:.16em; text-transform:uppercase;
    line-height:1.9;
  }
  .hero__enter { width:100%; max-width:320px; }

  /* The app dock: five 74px icons plus gaps is 430px of content in a 393px
     viewport, so it clipped. Five across at 52px fits with room to spare and
     stays a single deliberate row rather than a wrap. */
  .appmarks, .hero__dock {
    display:grid; grid-template-columns:repeat(5,1fr);
    gap:10px; margin-top:1.6rem;
    max-width:320px; margin-inline:auto;
  }
  /* Scoped to the dock, and it has to be. `.am` is the app-mark chip, but it
     is also worn by `.quad__icon` — the 120px product icon that is the whole
     visual of the DW Portal and DW Cloud tiles in the quad grid. An unscoped
     `width:100% !important` made that icon 364px square inside a 366px tile,
     so the tile rendered as a flat blue slab painted over its own heading,
     subtitle and both buttons. Measured at 390px: icon 364x364, copy 364x211,
     the two overlapping from y+16 down. The `!important` is still needed here
     because styles.css sizes `.am` inside a max-width:560px block of its own. */
  .appmarks .am {
    width:100% !important; height:auto !important;
    aspect-ratio:1; border-radius:14px;
    /* §2.4 matches these on [role="button"] and hands them padding-inline of
       1.15rem. With border-box and a 56px cell that leaves a 19.2px content
       box, so `width:56%` on the logo resolved to 10.75px — the dock read as
       five blank grey tiles with a scratch in the middle. Same failure, same
       cure, as the icon buttons at §2.4: sized by its box, never by padding. */
    padding:0;
  }
  .appmarks .am img { width:56%; height:auto; }

  /* The five divisions — DIGITAL, APP, CLOUD, SECURE, ANALYTICS — are a
     centred flex row with flex-wrap and a gap that clamps up to 3.4rem. At
     390px the content box is 350px and the labels alone need about 300px, so
     the row wrapped 4+1 and left ANALYTICS sitting alone under the middle of
     the line. It read like a mistake rather than a set.

     Same cure as the dock directly above: an explicit five-column grid, and
     the label steps down so five tracks are honest rather than aspirational.
     350px across five tracks with 6px gaps gives 65.6px, and ANALYTICS at
     .68rem tracked .24em measures roughly 75px — hence .58rem and .08em,
     which lands it near 52px with room to spare. The trailing letter-spacing
     is trimmed off the last glyph so each label still optically centres. */
  .divisions {
    display:grid !important;
    grid-template-columns:repeat(5, minmax(0,1fr));
    gap:6px !important;
    margin-top:1.6rem;
    max-width:352px; margin-inline:auto;
  }
  .divisions span {
    font-size:.56rem; letter-spacing:.06em; gap:.4rem;
    text-align:center; line-height:1.2;
    padding-inline-start:.08em;
  }
  .divisions svg { width:20px; height:20px; }

  /* The orbit is a pointer-driven 3D arrangement; on a phone it renders as
     five icons piled on the mark. The ledger keeps its copy, drops the toy. */
  .ledger__orbit, .hero__orbit { display:none; }

  /* The scroll cue is a desktop affordance. Phone users know to scroll. */
  .scroll-cue { display:none; }

  /* ------------------------------------------------------------------------
     2.9 The scrollers — every horizontal track becomes a vertical stack
     ------------------------------------------------------------------------
     This is the defect the owner named first. Measured in a 393px viewport:
       .show-track            5,983px wide
       .show-track--sm       10,557px wide
       .osmx__scroll          1,000px wide
     A 10,557px track in a 393px window is twenty-seven screens of content
     hidden behind a gesture most people never make. On a phone the answer is
     not a better carousel, it is no carousel.
     ------------------------------------------------------------------------ */
  .show-wrap, .show-wrap--sm { overflow:visible; }
  .show-track, .show-track--sm {
    display:grid !important;
    grid-template-columns:1fr !important;
    gap:14px !important;
    overflow:visible !important;
    scroll-snap-type:none !important;
    /* The tracks were the ONLY content on the page flush to the bezel: every
       other section sits inside .container's 20px gutter, and the two shelves
       measured 0 -> 393 exactly. On a phone with rounded display corners that
       is not a full-bleed treatment, it is a clipped one — the outer tiles'
       own 14px corner radius gets eaten by the screen's, the card border shows
       on the top and bottom edges but not the sides, and the whole shelf reads
       as content that has overflowed rather than content that was placed. The
       hairline above the second shelf stays edge-to-edge on purpose; the cards
       come inside the same gutter as everything else, notch-aware in landscape
       exactly like .container. */
    padding-block:0 !important;
    padding-inline:
      calc(var(--dw-gut) + var(--dw-safe-l))
      calc(var(--dw-gut) + var(--dw-safe-r)) !important;
    width:auto !important;
    transform:none !important;
  }
  .show-card, .show-card--sm {
    flex:none !important;
    width:100% !important;
    max-width:100% !important;
    /* Fixed aspect instead of vh height: 72vh cards meant one card filled the
       screen and the user had no signal a second one existed. */
    height:auto !important;
    aspect-ratio:4/3;
    min-height:0;
    border-radius:18px;
    scroll-snap-align:none;
    background-size:cover; background-position:center;
  }
  /* The 30 small tiles go two-up. They are photo tiles with a name lockup and
     nothing else, so at 170px wide they still read — and two-up is what turns
     an endless feed back into a gallery. */
  .show-track--sm {
    grid-template-columns:repeat(2, minmax(0,1fr)) !important;
    gap:10px !important;
  }
  /* 4/3 was right when .show-copy was one thin line at bottom:14px. Now that
     the large card carries a scrim band with a caption and a full-width button,
     a 393x295 card leaves about 135px of clear photograph — the image becomes a
     texture behind a control. Portrait 4/5 gives the picture the room it needs
     and reads the way a product card should on a phone. The five flagship
     cards are worth the extra height; the 30 small tiles stay square. */
  .show-card:not(.show-card--sm) { aspect-ratio:4/5; }
  .show-card--sm { aspect-ratio:1/1; border-radius:14px; }
  /* Two tracks, adjacent, no heading between them: the large track's own
     "Show 9 more" button ended up 26px above the small grid, which is the same
     distance as the grid's internal gutter, so it read as a header for the
     tiles below rather than as the end of the shelf above. A hairline and real
     space make the second shelf a second shelf. */
  .show-wrap--sm {
    margin-top:34px; padding-top:30px;
    border-top:1px solid rgba(255,255,255,.07);
  }
  .show-card--sm .show-copy { display:none; }        /* no room for it two-up */
  .show-card--sm .show-lockup {
    top:auto !important; bottom:10px; left:10px !important; right:10px;
    font-size:.72rem !important; line-height:1.2;
  }
  .show-card--sm .show-lockup img { height:13px; }

  .show-lockup { top:14px !important; left:16px !important; font-size:.9rem !important; }
  /* The top of these photographs is not reliably dark — prod_commerce.jpg has
     lit ceiling rails right where the lockup sits. The card's ::after only
     applies .20 black at the very top, so the name gets a shadow of its own
     rather than a heavier veil that would dull the image. */
  .show-lockup { text-shadow:0 1px 12px rgba(0,0,0,.85), 0 0 3px rgba(0,0,0,.6); }

  /* ------------------------------------------------------------------------
     2.9a The large card's composition
     ------------------------------------------------------------------------
     Desktop lays .show-copy out as one horizontal line 26px in from the edges
     of a 1120x660 card: pill, then caption, side by side with room to spare.
     Narrowing that card to 393px and letting the same absolute positioning
     stand produced the thing the owner was describing — a white "Learn more"
     pill floating in the middle-left of the photograph with no relationship to
     anything, and the caption sitting directly on a reflective marble floor at
     the bottom with nothing behind it.

     On a phone the bottom of the card becomes a band: a gradient scrim that the
     caption can actually be read against, the caption above the button, the
     button full-bleed across the band. column-reverse is deliberate — the DOM
     order is pill-then-caption (correct for a desktop row), and reversing the
     column gives the reading order a phone wants, caption then action, without
     touching the markup. */
  .show-card:not(.show-card--sm) .show-copy {
    left:0 !important; right:0 !important; bottom:0 !important;
    display:flex; flex-direction:column-reverse; align-items:stretch;
    gap:12px; flex-wrap:nowrap;
    padding:72px 16px calc(16px);
    /* The ramp has to be well clear of the caption before the caption starts,
       because prod_commerce.jpg is a polished marble floor at exactly that
       height — white text on near-white stone. The text-shadow below is the
       belt to the scrim's braces: it costs nothing and covers the one card
       whose photograph is bright right where the type lands. */
    background:linear-gradient(180deg,
      rgba(4,5,8,0) 0%, rgba(4,5,8,.60) 30%, rgba(4,5,8,.90) 58%, rgba(4,5,8,.97) 100%);
    opacity:1 !important; transform:none !important;
  }
  .show-card:not(.show-card--sm) .show-copy i {
    font-size:1rem; line-height:1.35; color:#F4F7FB; font-weight:600;
    letter-spacing:-.005em;
    text-shadow:0 1px 14px rgba(0,0,0,.8);
  }
  /* A 105px-wide pill floating in the middle of a photo reads as debris. The
     same pill spanning the band reads as the card's action — and it is the only
     visible affordance on a control the whole card belongs to, so it should
     look like one at full width.

     It is glass rather than the desktop's solid white on purpose. Six stacked
     cards each with a full-bleed white pill is a column of six white bars: loud,
     repetitive, and it fights the photography the section exists to show. A
     translucent button sits on the image instead of on top of it, which is the
     "sophisticated" half of the brief. */
  .show-card:not(.show-card--sm) .show-pill {
    display:flex; align-items:center; justify-content:center;
    min-height:48px; padding:0 1.2rem;
    font-size:.94rem; letter-spacing:.01em; font-weight:650;
    color:#fff;
    background:rgba(255,255,255,.12);
    border:1px solid rgba(255,255,255,.30);
    -webkit-backdrop-filter:blur(14px) saturate(150%);
    backdrop-filter:blur(14px) saturate(150%);
    box-shadow:0 6px 22px rgba(0,0,0,.35);
  }
  .show-copy i { font-size:.92rem; line-height:1.4; }
  /* Prev/next arrows steer a track that no longer scrolls. */
  .show-nav, .show-prev, .show-next { display:none !important; }

  /* ------------------------------------------------------------------------
     2.9b Progressive disclosure — the cost of un-scrolling a scroller
     ------------------------------------------------------------------------
     Turning two horizontal ribbons into vertical stacks is right, but it is
     not free: 15 + 30 cards became 12,650px, a third of the whole homepage.
     On desktop those ribbons are opt-in — you scroll them if you want them.
     Collapsing to the first few restores that: the gallery is still complete,
     it is just no longer mandatory reading on the way to the pricing section.

     mobile.js adds [data-collapsed] and injects the button; if the JS never
     runs, every card shows and the page is merely long — never broken.

     The type selector matters: .show-more is appended as the LAST child, so
     `> *:nth-child(n+7)` would hide the button that does the expanding.
     Filtering on .show-card leaves it alone. */
  .show-track[data-collapsed] > .show-card:nth-child(n+7),
  .show-track--sm[data-collapsed] > .show-card:nth-child(n+9) { display:none; }

  .show-more {
    grid-column:1 / -1;
    margin-top:12px;
    width:100%; min-height:48px;
    display:inline-flex; align-items:center; justify-content:center; gap:.5rem;
    border-radius:14px;
    border:1px solid rgba(255,255,255,.16);
    background:rgba(255,255,255,.04);
    color:#EDF1F6;
    font:600 .95rem/1 var(--font-sans, system-ui, sans-serif);
    letter-spacing:.01em;
    touch-action:manipulation;
    -webkit-tap-highlight-color:transparent;
  }
  .show-more::after { content:"↓"; font-size:1.05em; line-height:1; }
  .show-more[aria-expanded="true"]::after { content:"↑"; }

  /* Generic rule for every other overflow-x container in the codebase, so a
     scroller added next month is contained by default instead of silently
     making the page scroll sideways. */
  /* .chip-row used to be in this list and is not any more. It is the service
     selector in the tools.html proposal builder, and it now wraps (see .chip in
     styles.css) — eight labels ending in "Conversion Optimization" behind a
     sideways drag is exactly the kind of scroller this phone pass exists to
     remove. A wrapping flex row cannot overflow, so the rule was doing nothing
     but describing the component wrongly to whoever reads this next. */
  .logos__row, .tab-scroll, .vaultband__strip {
    max-width:100%;
    overflow-x:auto;
    -webkit-overflow-scrolling:touch;
    overscroll-behavior-x:contain;   /* never hands the gesture to the page */
    scrollbar-width:none;
  }
  .logos__row::-webkit-scrollbar, .tab-scroll::-webkit-scrollbar { display:none; }

  /* .marquee__row and .qroll are NOT scrollers, and giving them overflow-x:auto
     was the wrong fix — it turned two pieces of decoration into draggable
     regions. Both are animated by transform inside a parent that already
     clips, so the correct value is visible (let the parent clip) and the
     correct parent value is clip.

       .marquee__row  transform-driven ticker, 808px of content by design
       .qroll         a fading rotator: 4 absolutely-positioned quotes stacked
                      on top of each other, so its scrollWidth is an artifact
                      of absolute children, not content the user can reach.  */
  .marquee, .qroll { overflow:hidden; overflow:clip; }
  .marquee__row, .qroll { overflow:visible !important; max-width:none; }

  /* This used to read "keep one static row" and set animation:none on
     `.marquee`. It never did anything: the animation is declared on
     `.marquee__row`, not on the wrapper, so the ticker has been moving on
     every phone since the day the rule was written.

     Leaving it moving is the right answer anyway, and stopping it now would
     make the strip worse. The row is sixteen items — content.js emits the
     eight service names twice so the loop seams invisibly — laid out at about
     808px inside a 390px clip. Frozen, a visitor sees "Web Design ·
     Development · Enterprise Hos" and a hard cut; moving, they see all eight.
     The cost is a compositor-only transform, which is not the per-frame
     repaint the old comment was worried about.

     Slower and smaller on a phone, because 26s across 390px is twice the
     angular speed it is across a desktop column and reads as a flicker. */
  .marquee { padding:14px 0; }
  .marquee__row { animation-duration:44s; gap:2rem; }
  .marquee__row span { font-size:.86rem; }
  .marquee__row svg { width:14px; height:14px; }
  .marquee__row + .marquee__row { display:none; }

  /* ------------------------------------------------------------------------
     2.10 The comparison table → a comparison stack
     ------------------------------------------------------------------------
     The five-column plan matrix has min-width:1000px, so on a phone it was
     either a 1,000px sideways scroll or (at 390/430px) an empty box. Below,
     each feature row becomes its own block: the feature name, then the four
     plans as labelled cells across one line. Column order is fixed in the
     markup, so nth-child carries the plan name — no HTML change needed.
     ------------------------------------------------------------------------ */
  .osmx__scroll { overflow:visible !important; }
  .osmx__tbl {
    min-width:0 !important;
    width:100% !important;
    display:block;
    table-layout:auto !important;
  }
  .osmx__tbl colgroup, .osmx__tbl col { display:none; }
  .osmx__tbl thead { display:none; }            /* replaced by the plan cards above */
  .osmx__tbl tbody { display:block; }

  .osmx__tbl tr {
    display:block;
    padding:14px 0;
    border-bottom:1px solid var(--hairline);
  }
  /* Group headings ("The foundation") stay as section rules. */
  .osmx__tbl tr.osmx__grp { padding:22px 0 8px; border-bottom:none; }
  .osmx__tbl tr.osmx__grp th {
    display:block; width:auto;
    font-size:.7rem; letter-spacing:.2em; text-transform:uppercase;
    color:var(--aqua-bright);
  }

  /* ---- the accordion heading (mobile.js §4d) ----------------------------
     Only the rows mobile.js has actually wired get the control treatment, so
     if that script never runs the headings stay plain text over a fully
     expanded matrix — the failure mode is "long", not "broken".
     The count sits in a pill on the right because a closed section has to say
     how much is behind it; without it, four collapsed headings look like the
     comparison was deleted. 46px min-height clears the 44px target with the
     row's own padding still doing the section spacing. */
  .osmx__tbl tr.osmx__grp.is-toggle { padding:10px 0 6px; }
  .osmx__tbl tr.osmx__grp.is-toggle th {
    display:flex; align-items:center; gap:.7rem;
    min-height:46px; padding:0 12px 0 14px; margin:0;
    border-radius:12px; cursor:pointer;
    background:rgba(255,255,255,.035);
    box-shadow:inset 0 0 0 1px rgba(255,255,255,.07);
    -webkit-tap-highlight-color:transparent;
    transition:background .18s ease, box-shadow .18s ease;
  }
  .osmx__tbl tr.osmx__grp.is-toggle.is-open th {
    background:rgba(59,158,255,.09);
    box-shadow:inset 0 0 0 1px rgba(59,158,255,.26);
  }
  .osmx__tbl tr.osmx__grp.is-toggle th:focus-visible {
    outline:2px solid var(--aqua-bright); outline-offset:2px;
  }
  .osmx__gt { flex:1 1 auto; text-align:left; }
  /* The count is deliberately not uppercase-tracked like the heading — at
     .2em a two-digit number reads as two separate numbers. */
  .osmx__gc {
    flex:none; min-width:22px; text-align:center;
    font-size:.72rem; font-weight:700; letter-spacing:0;
    color:var(--text-mut); font-variant-numeric:tabular-nums;
  }
  /* Chevron from two borders, same construction as the knowledge-base
     summary marker, so the two disclosure affordances on the site match. */
  .osmx__tbl tr.osmx__grp.is-toggle th::after {
    content:""; flex:none; width:7px; height:7px; margin:-3px 2px 0 0;
    border-right:2px solid var(--aqua-bright);
    border-bottom:2px solid var(--aqua-bright);
    transform:rotate(45deg); opacity:.75;
    transition:transform .25s cubic-bezier(.2,.7,.3,1), opacity .2s ease;
  }
  .osmx__tbl tr.osmx__grp.is-toggle.is-open th::after {
    transform:rotate(225deg); margin:3px 2px 0 0; opacity:1;
  }

  .osmx__tbl .osmx__lab {
    display:block; width:auto;
    padding:0 0 10px;
    font-size:.97rem; font-weight:600; color:#fff;
    border:none;
  }
  /* The four plan cells laid out as one labelled row. */
  .osmx__tbl tr:not(.osmx__grp) {
    display:grid;
    grid-template-columns:1fr;
  }
  .osmx__tbl .osmx__cell {
    display:grid;
    grid-template-columns:1fr auto;
    align-items:center;
    gap:10px;
    padding:9px 12px;
    border:none;
    border-radius:10px;
    background:rgba(255,255,255,.03);
    font-size:.88rem;
    margin-bottom:6px;
  }
  .osmx__tbl .osmx__cell.is-pop {
    background:rgba(59,158,255,.1);
    box-shadow:inset 0 0 0 1px rgba(59,158,255,.25);
  }
  /* The plan name each cell belongs to. Order is Essential, Business OS,
     Industry Pro, Enterprise — nth-child 2..5 because child 1 is the label. */
  .osmx__tbl tr:not(.osmx__grp) > *:nth-child(2)::before { content:"DW Essential"; }
  .osmx__tbl tr:not(.osmx__grp) > *:nth-child(3)::before { content:"DW Business OS"; }
  .osmx__tbl tr:not(.osmx__grp) > *:nth-child(4)::before { content:"DW Industry Pro"; }
  .osmx__tbl tr:not(.osmx__grp) > *:nth-child(5)::before { content:"DW Enterprise"; }
  .osmx__tbl .osmx__cell::before {
    font-size:.78rem; font-weight:600; letter-spacing:.03em;
    color:var(--text-mut);
    justify-self:start;
  }
  .osmx__tbl .osmx__cell.is-pop::before { color:var(--aqua-bright); }
  .osmx__tbl .osmx__y, .osmx__tbl .osmx__n { justify-self:end; }
  .osmx__tbl .osmx__y svg { width:18px; height:18px; }

  /* transform:none matters as much as position:static here — the badge is
     centred with left:58.75% AND translateX(-50%). Neutralising position
     killed the offset but left the transform, which still dragged it to
     [-64→104]. Both halves of a centring pair have to be undone together. */
  /* …but un-positioning it also stripped its meaning, and the pill chrome then
     works against the page. At 1440px it sits at left:58.75%, directly over the
     DW Business OS column head, so "Most Popular" needs no further words.
     Measured at 393px it was a 169×33 gradient pill standing alone at y9160
     with the table starting at y9203 — a badge in dead space, pointing at
     nothing, and the second gradient pill within one screen, because the
     DW Business OS plan card above already carries .osp__badge saying exactly
     the same thing. Meanwhile 43 cells below it are tinted aqua by .is-pop and
     nothing explained why.

     So on a phone it stops being a badge and becomes what it actually is here:
     the legend for those aqua rows. The swatch is a miniature of an .is-pop
     cell — same tint, same inset hairline — so the eye connects it to the rows
     without being told. The plan name is appended in CSS rather than the markup
     because the desktop pill must NOT carry it.

     white-space is returned to normal and the pill's padding/background are
     dropped: at 320px the full string measures wider than the 280px content
     box, and as a nowrap pill it was clipped mid-word. As plain legend text it
     simply wraps to a second line, which costs nothing without pill chrome
     around it. */
  .osmx__pop {
    position:static; display:block; margin:0 0 14px; padding:0;
    background:none; box-shadow:none; border-radius:0;
    color:var(--aqua-bright); white-space:normal; line-height:1.5;
    transform:none !important; left:auto !important; right:auto !important;
  }
  .osmx__pop::before {
    content:""; display:inline-block; vertical-align:-.32em;
    width:20px; height:20px; margin-right:.6rem; border-radius:6px;
    background:rgba(59,158,255,.1); box-shadow:inset 0 0 0 1px rgba(59,158,255,.25);
  }
  .osmx__pop::after {
    content:" · DW Business OS"; font-weight:600; letter-spacing:.04em;
    text-transform:none; color:var(--text-mut);
  }

  /* The hint is switched on at ≤1180px in styles.css:4193, where the matrix is
     still a five-column table inside a 1,000px-min-width horizontal scroller.
     At ≤767.98px the rules above turn that table into a stack: measured
     scrollWidth 351 === clientWidth 351, so there is no sideways travel at all.
     Telling someone to swipe a table that cannot move is a broken instruction,
     and on a phone they will try it. The stack is self-describing — every cell
     is prefixed with its plan name — so the hint has nothing left to add. */
  .osmx__hint { display:none !important; }

  .osmx__wrap { overflow:visible; }

  /* ------------------------------------------------------------------------
     2.11 Data tables (admin, analytics, portal, vault, command center)
     ------------------------------------------------------------------------
     mobile.js stamps every <td> with data-label taken from the matching
     <th> in <thead>, so each row becomes a self-describing card. The CSS
     below only applies once that attribute exists; a table that JS has not
     reached falls back to the contained-scroll rule at the end, which is
     ugly but never breaks the page.
     ------------------------------------------------------------------------ */
  table.data { display:block; max-width:100%; overflow-x:auto; -webkit-overflow-scrolling:touch; }

  /* styles.css:2896-2901 already had a narrow-screen strategy for these tables
     and it is the opposite of this one: `tbody{display:table;width:max-content}`
     plus `th,td{white-space:nowrap}` plus a right-edge fade gradient — i.e.
     "make it a horizontal scroller and hint that it scrolls". Overriding
     `display` was not enough. `width:max-content` and `white-space:nowrap`
     survived, so a stacked card measured 462px of unwrappable content inside a
     303px panel; because the panel clips, the overflow was not a scroll but
     silent truncation, which is worse. Every part of the old strategy has to be
     undone, including the fade — an affordance promising a sideways scroll that
     no longer exists is a lie told to the reader. */
  table.data[data-stacked], table.data[data-flow], table.data[data-empty] {
    width:auto; max-width:100%;
  }
  table.data[data-stacked] thead, table.data[data-stacked] tbody,
  table.data[data-flow] thead,  table.data[data-flow] tbody,
  table.data[data-empty] thead, table.data[data-empty] tbody {
    width:auto; min-width:0; max-width:100%;
  }
  table.data[data-stacked] th, table.data[data-stacked] td,
  table.data[data-flow] th,  table.data[data-flow] td,
  table.data[data-empty] th, table.data[data-empty] td {
    white-space:normal;
  }
  .panel:has(> table.data[data-stacked])::after,
  .panel:has(> table.data[data-flow])::after,
  .panel:has(> table.data[data-empty])::after { content:none; }

  /* The empty state. Seven column headings describing nothing, then one
     sentence — on a phone the headings are the only reason the thing was
     455px wide, so they go and the sentence is all that is left. */
  table.data[data-empty] { display:block; overflow:visible; }
  table.data[data-empty] thead { display:none; }
  table.data[data-empty] tbody,
  table.data[data-empty] tr,
  table.data[data-empty] td { display:block; min-width:0; }
  table.data[data-empty] td {
    padding:16px 0; border:none; text-align:left;
    overflow-wrap:anywhere; word-break:break-word;
  }
  .panel:has(> table.data[data-empty]) { overflow-x:clip; }

  table.data[data-stacked] {
    display:block; overflow:visible; border-collapse:separate;
  }
  table.data[data-stacked] thead { display:none; }
  table.data[data-stacked] tbody, table.data[data-stacked] tr { display:block; }
  table.data[data-stacked] tr {
    border:1px solid var(--line);
    border-radius:12px;
    padding:6px 12px;
    margin-bottom:10px;
    background:rgba(255,255,255,.02);
  }
  table.data[data-stacked] td {
    display:grid;
    grid-template-columns:minmax(0,42%) minmax(0,1fr);
    gap:12px;
    align-items:center;
    padding:9px 0;
    border-bottom:1px solid rgba(255,255,255,.05);
    font-size:.92rem;
  }
  table.data[data-stacked] tr td:last-child { border-bottom:none; }
  table.data[data-stacked] td::before {
    content:attr(data-label);
    font-size:.72rem; letter-spacing:.06em; text-transform:uppercase;
    color:var(--text-mut); font-weight:600;
  }
  /* A cell whose header was blank (an actions column) spans the full width. */
  table.data[data-stacked] td[data-label=""] { grid-template-columns:1fr; }
  table.data[data-stacked] td[data-label=""]::before { display:none; }

  /* A pill inside a stacked cell must still shrink-wrap its own word.
   *
   * The cell is a two-column grid, and CSS blockifies every grid item — so a
   * .pill, which is inline-block everywhere else on the site, became display:block
   * and then stretched to fill the whole value track. On the dashboard's invoice
   * cards that made the amber "Due" badge 148.7px wide around 49.4px of text: a
   * bar reaching the card's right edge instead of a tag, three times its own size,
   * and the loudest thing in a card whose actual subject is the amount. Six of them
   * on that page ("Due" x5, "Void").
   *
   * justify-self:start is the whole fix — it hands the item back its fit-content
   * width without un-blockifying it, so the pill's padding, radius and colour all
   * stay exactly as the component defines them. Deliberately not applied to the
   * actions column, where a full-width button IS the intent. */
  table.data[data-stacked] td > .pill,
  table.data[data-stacked] td > .pill-soft,
  table.data[data-stacked] td > .tag,
  table.data[data-stacked] td > .chip,
  table.data[data-stacked] td > .badge { justify-self:start; }

  /* A stacked table still measured 376px inside a 303px column on portal.html:
     minmax(0,1fr) lets the TRACK shrink, but an unbreakable token inside it —
     an invoice number like DW-2026-0142, a URL, an email — establishes a
     min-content width the track cannot go below. The value column has to be
     told it may break mid-token, and every descendant has to give up its
     min-width:auto default, or one long string re-widens the whole card. */
  table.data[data-stacked] tr,
  table.data[data-stacked] td { max-width:100%; }
  table.data[data-stacked] td,
  table.data[data-stacked] td * {
    min-width:0;
    overflow-wrap:anywhere;
    word-break:break-word;
  }
  /* The panels these tables sit in are themselves overflow-x:auto, so any
     residual pixel became a sideways drag of the whole card. Nothing inside a
     stacked table is ever meant to scroll horizontally. */
  .panel:has(table.data[data-stacked]) { overflow-x:clip; }

  /* The headless variant. There is no <thead> to read, so there are no labels
     and inventing them would be worse than omitting them — the reader can see
     that "Broken contact form" is a subject and "Open" is a status without
     being told. All this needs to do is stop being a table: the row becomes a
     card, each cell a line, and the 518px min-content width collapses. */
  table.data[data-flow] { display:block; overflow:visible; border-collapse:separate; }
  table.data[data-flow] tbody, table.data[data-flow] tr { display:block; }
  table.data[data-flow] tr {
    border:1px solid var(--line);
    border-radius:12px;
    padding:10px 12px;
    margin-bottom:10px;
    background:rgba(255,255,255,.02);
  }
  table.data[data-flow] td {
    display:block;
    border:none;
    padding:2px 0;
    font-size:.92rem;
    min-width:0;
    overflow-wrap:anywhere;
    word-break:break-word;
  }
  /* These rows are clickable (dashboard.php puts an onclick on the <tr>), so
     the card itself is the tap target and needs to be a comfortable one. */
  table.data[data-flow] tr[onclick] { min-height:56px; }
  /* Inline text-align:right on a cell is right for a table column and wrong
     for a stacked line — a status pill floating on the right of its own row
     reads as unrelated to the subject above it. */
  table.data[data-flow] td[style*="right"] { text-align:left !important; }
  table.data[data-flow] td[style*="right"] > .pill { margin-top:4px; }
  .panel:has(table.data[data-flow]) { overflow-x:clip; }

  /* ------------------------------------------------------------------------
     2.12 App shell (admin / analytics / portal / vault / command-center)
     ------------------------------------------------------------------------ */
  .app-side {
    /* It is already off-canvas; make sure it is off-canvas in a way that
       cannot contribute to document width. */
    position:fixed !important;
    left:0; top:0;
    width:min(86vw,320px) !important;
    height:100dvh;
    padding-top:calc(14px + var(--dw-safe-t));
    padding-bottom:calc(20px + var(--dw-safe-b));
    overscroll-behavior:contain;
    z-index:90;
  }
  .app-main, .app-body, .app-wrap { padding-inline:var(--dw-gut) !important; }
  .app-topbar, .app-bar {
    padding-top:var(--dw-safe-t);
    height:calc(56px + var(--dw-safe-t));
  }
  .dcard, .dkpi, .panel { border-radius:14px; }

  /* The account chip. app_top() puts it before the heading (order:-1), and its
     text is right-aligned because on desktop it sits to the LEFT of a 44px
     avatar at the far right of a wide bar. In a 353px column that alignment
     has nothing to align to: the company name started at the left edge while
     the email ended 190px in, and the ragged pair either side of a floating
     circle read as a layout fault. On a phone it becomes what it actually is —
     an account row: avatar first, text left, hairline under it. */
  .app-top .who {
    width:100%;
    gap:.6rem;
    padding-bottom:.85rem;
    margin-bottom:.2rem;
    border-bottom:1px solid var(--glass-brd);
  }
  .app-top .who > span:not(.av) { text-align:left !important; flex:1 1 auto; min-width:0; }
  .app-top .who .av { order:-1; width:38px !important; height:38px !important; }

  /* Four numbers should not cost four screens. styles.css collapses .kpi-grid
     to ONE column under 520px, so the dashboard's KPIs were 353x150 each —
     653px of scrolling to read four figures, which is the single clearest
     example of a desktop card shape surviving onto a phone. Two up, compact,
     and all four land in one screen. */
  .kpi-grid { grid-template-columns:1fr 1fr !important; gap:12px !important; }
  .kpi { padding:14px 14px 13px !important; border-radius:14px; }
  .kpi .lbl { font-size:.72rem; gap:.35rem; }
  .kpi .lbl svg { width:15px; height:15px; }
  /* The value has to survive "$7,239.00" in a 168px tile, so it scales with the
     viewport rather than sitting at a fixed 2rem, and long strings break rather
     than push the tile wide. */
  .kpi .val {
    font-size:clamp(1.05rem, 5.4vw, 1.42rem);
    margin:.3rem 0 .15rem;
    line-height:1.15;
    overflow-wrap:anywhere;
    font-variant-numeric:tabular-nums;
  }
  .kpi .chg { font-size:.7rem; line-height:1.3; display:block; }
  /* A KPI that is a link stays a 44px target even though the tile shrank. */
  .kpi a { min-height:0; }

  /* invoices.php builds the same pair of figures out of .panel instead of .kpi,
     with flex:1 1 200px — two 200px basis items cannot share a 353px row, so
     they wrapped and became two 320px-tall cards for two numbers. The rule
     needs (0,3,0) to outrank the page's own <style> block, which is later in
     the document and would otherwise win the tie. The inline font-size:1.7rem
     on .val is the one place !important is the only tool that reaches. */
  .app-main .inv-stat { gap:12px; }
  .app-main .inv-stat .panel { flex:1 1 0; min-width:0; padding:14px 14px 13px; }
  .app-main .inv-stat .val {
    font-size:clamp(1.05rem, 5.4vw, 1.42rem) !important;
    margin:.28rem 0 .15rem; line-height:1.15;
    overflow-wrap:anywhere; font-variant-numeric:tabular-nums;
  }

  /* A status tag trailing a heading needs its own line once the heading wraps.
   *
   * styles.css:362 makes every .panel h3 a flex row with justify-content:
   * space-between and align-items:center — the "Title ............ [pill]" lockup,
   * which is right when the title is one line. On projects.php at 393px the title
   * takes THREE lines inside 223px, so "Active" was pinned to the h3's right edge
   * and vertically centred against the whole block: floating in mid-air beside
   * line two with title text continuing underneath it, reading as a stray label
   * rather than the project's status. Measured pill L225.8 / R268 against an h3
   * L45 / R268, 66.7px tall on a 22.2px line box. Three of them on that page.
   *
   * Letting the row wrap is the fix. The title is an anonymous flex item whose
   * max-content basis exceeds 223px, so it takes the first flex line alone and the
   * pill falls to the next one at the left margin — name, then state, then the meta
   * line, which is the order the rest of the portal already uses on a phone. A
   * short enough title keeps both on one line, now separated by the gap instead of
   * shoved to opposite ends. The gap replaces the template's inline
   * margin-left:.4rem, which is why that needs !important to clear: it only makes
   * sense while the pill is inline, and it would offset the wrapped line.
   *
   * Scoped to .pj-head because a sweep of fourteen portal pages found this is the
   * only place a flex h3 both wraps and carries a trailing element — the dashboard's
   * "Invoices & Billing / All invoices →" pairs are one-line titles whose height
   * comes from the 44px tap target on the link, and they read correctly as-is. */
  .app-main .pj-head h3 {
    flex-wrap:wrap;
    align-items:flex-start;
    justify-content:flex-start;
    gap:.5rem .55rem;
  }
  .app-main .pj-head h3 > .pill { margin-left:0 !important; }

  /* ------------------------------------------------------------------------
     2.13 The video closer
     ------------------------------------------------------------------------
     video.closer__video measured [-12 → 405] in a 393px viewport on roughly
     eighteen pages: 12px off the left edge and 12px off the right, so every
     one of those pages scrolled sideways by 24px. inset:0 + width:100% is
     correct until a transformed or padded ancestor shifts the containing
     block; pinning left/right to 0 and letting width follow removes the
     dependency entirely.
     ------------------------------------------------------------------------ */
  .closer-sec { overflow:clip; }
  .closer__video {
    left:0 !important; right:0 !important;
    width:100% !important; max-width:100% !important;
    height:100% !important;
    object-fit:cover;
  }
  .closer__rays, .closer__scan { display:none; }   /* decorative, costly, unseen */

  /* ------------------------------------------------------------------------
     2.14 Desktop-pointer sections
     ------------------------------------------------------------------------ */
  /* The neon section literally instructed phone users to "MOVE YOUR CURSOR ·
     CLICK TO RE-WEAVE THE LIGHT". mobile.js rewrites the copy for touch;
     this sizes the section so it is one screen, not 96vh of canvas. */
  .neon { min-height:auto; padding-block:clamp(56px,14vw,80px); }
  .neon__copy { padding-inline:var(--dw-gut); }
  .neon__mark { width:auto; height:56px; }
  .neon__hint { margin-top:1.6rem; font-size:.68rem; letter-spacing:.16em; }

  /* Spline / WebGL scenes: a 4MB scene downloaded over cellular to be hidden
     by CSS is worse than no scene. display:none does not stop the fetch —
     mobile.js removes the element before the loader runs. This rule is the
     belt to that braces. */
  spline-viewer, .scene-3d, #splineHost { display:none !important; }

  .quad__tile { min-height:0; aspect-ratio:4/5; }
  .quad__tile h3 { font-size:1.25rem; }
  .quad__tile p { font-size:.95rem; }
  .quad__copy { padding:18px; }

  /* ------------------------------------------------------------------------
     2.15 Pricing cards
     ------------------------------------------------------------------------ */
  .prc__grid, .osgrid, .plans { grid-template-columns:1fr !important; gap:14px !important; }
  .prc__card, .oscard { width:100%; max-width:100%; transform:none !important; }
  .prc__switch, .osbill {
    display:inline-flex; width:100%; max-width:340px;
    margin-inline:auto; border-radius:999px;
  }
  .prc__tab, .osbill button { flex:1; min-height:46px; }
  .prc__thumb { display:none; }                 /* sliding pill mis-tracks at 1fr */
  .osfeat li, .prc__feat li { font-size:.94rem; line-height:1.5; padding-block:.3rem; }

  /* The subgrid block at styles.css:4009 exists to pin four side-by-side plan
     cards to nine shared row lines so price, setup note and CTA read across the
     row. It sets `.osrail{row-gap:0}` because in a four-column rail there is
     only one row and a row gap would instead open nine gaps INSIDE each card.

     On a phone .osrail is a single column, so there is nothing to align across
     — the subgrid does no work at all — and row-gap:0 welds the cards into one
     continuous slab. `.osp__badge` is absolutely positioned at top:-12px, so
     "MOST POPULAR" and "INCLUDES THE APP" land on the bottom border of the card
     ABOVE them and their glow washes over its last feature line. A badge that
     belongs to the card below it, sitting on the card above it, mislabels the
     product.

     So the phone opts out of the subgrid entirely and goes back to flex, which
     restores a real row gap wide enough for the badge to sit in cleanly. */
  .osrail { row-gap:32px; }
  .osp {
    display:flex; flex-direction:column;
    grid-row:auto; grid-template-rows:none;
  }
  .osp__tile, .osp__n, .osp__d, .osp__from,
  .osp__price, .osp__setup, .osp__save, .osp__cta, .osp__inc { grid-row:auto; }
  .osp__foot { display:block; grid-template-rows:none; margin-top:auto; }
  /* Now that there is a gap to sit in, centre the badge on the card's own top
     edge rather than pushing it a fixed 12px up into whatever is above. */
  .osp__badge { top:0; translate:-50% -50%; }

  /* ------------------------------------------------------------------------
     2.16 Footer
     ------------------------------------------------------------------------ */
  .site-footer { padding-top:44px; }
  .site-footer .footer-grid { gap:26px !important; padding-bottom:28px; }
  .footer-cta__in { display:block; text-align:left; }
  .footer-cta__in h3 { font-size:1.35rem; }
  .footer-cta__btns { margin-top:1.1rem; }
  /* The bottom of the page is not empty — two fixed controls live there.
     .totop is 48px in the left corner and .chat-fab is 56px in the right, both
     pinned above the safe area, so the last ~70px of the viewport is spoken for
     the moment you reach the end of a page. The social row is the last thing in
     the footer and is exactly what lands there: measured at 393px, the X link
     sat at 20,780 and the to-top button at 14,788 — a 42x40 overlap on a 44px
     target, on every public page. The link was not merely ugly, it was
     untappable, and it is the one row of the footer that has no duplicate
     anywhere else on the site.

     Reserving the height here rather than moving either button keeps both
     controls where a thumb expects them and costs 70px at the very bottom of a
     page that has already ended.

     The selector is .site-footer .footer-bottom and not .footer-bottom because
     styles.css:481 is `.site-footer .footer-bottom{padding:28px 0}` — (0,2,0)
     against (0,1,0) — and specificity does not care that this file loads last.
     The old padding-bottom here was calc(18px + safe) and it never applied at
     all: the measured gap under the social row was 28px on every page, i.e. the
     shorthand. The neighbouring .footer-grid rule already carries !important for
     exactly this reason, which is the tell that should have been read sooner.
     Two classes is enough; no !important needed. */
  .site-footer .footer-bottom {
    display:grid; gap:6px; text-align:left;
    padding-bottom:calc(88px + var(--dw-safe-b));
  }
  .footer-watermark { display:none; }
  .social { display:flex; gap:4px; }

  /* ------------------------------------------------------------------------
     2.17 Floating chat
     ------------------------------------------------------------------------
     It was a 56px pill overlapping body copy and sitting on the home
     indicator. On a phone it becomes a circular action button above the safe
     area; the label lives inside the panel it opens.
     ------------------------------------------------------------------------ */
  .chat-fab {
    right:calc(14px + var(--dw-safe-r));
    bottom:calc(14px + var(--dw-safe-b));
    width:56px; height:56px; padding:0;
    border-radius:50%;
    justify-content:center;
    font-size:0;                                /* hides "Ask Dark Water" */
    gap:0;
  }
  .chat-fab .av { width:34px; height:34px; }
  .chat-panel {
    position:fixed;
    left:calc(10px + var(--dw-safe-l));
    right:calc(10px + var(--dw-safe-r));
    bottom:calc(10px + var(--dw-safe-b));
    top:calc(var(--dw-safe-t) + 54px);
    width:auto; max-width:none; height:auto;
    border-radius:18px;
  }
  .chat-body { overscroll-behavior:contain; -webkit-overflow-scrolling:touch; }
  .chat-input input { font-size:16px; min-height:44px; }

  /* ------------------------------------------------------------------------
     2.18 Motion and cost
     ------------------------------------------------------------------------ */
  /* Fixed backgrounds are not composited on iOS — they repaint every frame
     and are the classic cause of janky scrolling on an otherwise fine page. */
  [style*="background-attachment:fixed"], .parallax, .bg-fixed {
    background-attachment:scroll !important;
  }
  /* Blur is the most expensive filter on a mobile GPU. Halve it, don't lose
     the glass look. */
  .glass, .card--glass, .btn--glass { backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px); }

  /* Reveal-on-scroll stays, but shorter and with less travel — a 40px slide
     on a 393px screen reads as a jolt. */
  .reveal { transform:translateY(14px); }
  .reveal.in { transform:none; }

  /* ------------------------------------------------------------------------
     2.19 Corrections from the post-implementation measurement pass
     ------------------------------------------------------------------------
     Every rule here answers one element the audit caught AFTER the layer was
     wired in. They are grouped separately rather than merged upward so the
     provenance stays readable: these are measured, not predicted.
     ------------------------------------------------------------------------ */

  /* .brain__copy is centred with left:50% + transform:translateX(-50%), and
     .reveal sets transform:translateY(28px) on the same element. Whichever
     rule lost the cascade, the other one broke: the block was measured at
     [120→473] in a 393px viewport, sliding in from the right on every reveal.
     Centring by inset instead of transform makes the two rules independent,
     so .reveal keeps its Y-slide and the copy can never travel sideways. */
  .brain__copy, .brain--alt .brain__copy {
    left:0 !important; right:0 !important;
    width:auto !important; max-width:100%;
    padding-inline:calc(var(--dw-gut) + var(--dw-safe-l)) calc(var(--dw-gut) + var(--dw-safe-r));
  }
  /* Inset centring alone is not enough: translateX(-50%) is still in the
     cascade and now shifts a full-width block to [-196→197]. Restating the
     transform at higher specificity than .reveal / .reveal.in — two classes
     beats one, three beats two — hands the Y-slide back without !important. */
  .brain__copy, .brain__copy.reveal { transform:translateY(14px); }
  .brain__copy.reveal.in { transform:none; }

  /* .closer__video carries transform:scale(1.06) and a 30s keyframe drifting
     to scale(1.13) — a 393px video rendering 416px wide, measured [-12→405]
     on ~18 pages. .closer clips it, so it was never a sideways scroll, but a
     continuous 30-second GPU animation behind a full-bleed video is battery
     the user is spending on something they cannot perceive on a 6" screen. */
  .closer__video {
    transform:none !important;
    animation:none !important;
    left:0 !important; right:0 !important;
    width:100% !important; max-width:100% !important; height:100% !important;
    object-fit:cover;
  }

  /* Decorative glows and sweeps sized in vw/percent overshoot the viewport by
     design, relying on an ancestor to clip. On a phone that ancestor is often
     the one whose overflow the layout above had to change, so clip locally. */
  .oshero, .oshero__wrap, .pviz, .pviz__stage, .woven, .neon, .ledger {
    overflow:hidden; overflow:clip;
  }
  .oshero__glow, .pviz__sweep, .hero__glow, .neon__glow {
    max-width:100% !important; left:0 !important; right:0 !important;
  }

  /* styles.css:1357 shrinks .totop to 42x42 below 640px — the same inverted
     assumption as :2955. It also sits at bottom:14px, i.e. underneath the
     home indicator on any notched iPhone, and in the same corner the chat
     FAB claims. Left corner, above the safe area, and actually 48px. */
  .totop {
    width:48px !important; height:48px !important;
    left:calc(14px + var(--dw-safe-l)) !important;
    bottom:calc(16px + var(--dw-safe-b)) !important;
  }

  /* "Compare every plan →" measured 184x17 and 156x16. Both sit at the end of
     a ~150-character block of fine print — a trailing call to action buried in
     the last line of a paragraph. Padding it out would overlap the line above,
     and 44px of leading inside prose looks broken, so the fix is to promote it
     to its own row. That is better on a phone regardless of the tap target: a
     CTA at the tail of five lines of grey 12px text is a CTA nobody finds.
     mobile.js tags only trailing links, so links inside a sentence are left
     exactly as they are. */
  a.dw-tap {
    display:flex; align-items:center;
    min-height:44px;
    margin-top:12px;
    font-size:.95rem; font-weight:600;
    touch-action:manipulation;
    -webkit-tap-highlight-color:transparent;
  }

  /* Mid-sentence links. padding-block on an INLINE element is invisible to the
     line-box calculation, so this buys a 44px+ hit area for zero layout shift.
     It must stay off anything with a background or border, which would then
     paint over the neighbouring lines — hence :not() rather than a bare rule. */
  a.dw-tap-inline:not(.btn):not(.pill):not(.chip) {
    padding-block:13px;
    touch-action:manipulation;
    -webkit-tap-highlight-color:transparent;
  }

  /* A control that fails on either axis just needs a box. This is the branch
     mobile.js takes for anything whose computed display is NOT inline — a
     button, a pill, an icon link, "Back to darkwater.work" at inline-flex
     175x21, the "Free" vault chip at flex 42x61 (tall enough, too narrow).

     !important is required here, not lazy: `.chat-quick button` is (0,1,1) and
     forces min-height:40px, which a bare `.dw-tap-ctl` at (0,1,0) would lose
     to. The class only ever lands on an element mobile.js has already
     MEASURED as under 44px on one axis, so it can never inflate something
     that was already correct.

     min-width is expressed as a minimum rather than a width so a wide-but-
     short control keeps its width and only grows vertically. */
  .dw-tap-ctl {
    min-width:46px !important;
    min-height:46px !important;
    touch-action:manipulation;
    -webkit-tap-highlight-color:transparent;
  }

  /* The 16px field rule loses to a page-level <style> block, and five portal
     pages have one. `.set input{font-size:.92rem}` in settings.php is (0,1,1),
     exactly the same weight as §2.5's `input:not([type="hidden"])`, and it is
     in the document AFTER mobile.css because it is inline in the page — so it
     wins on order and six fields measured 14.7px. documents.php is worse:
     `.upl input[type=file]` is (0,2,1) and wins outright at 13.6px.

     The fix is specificity, not !important: each :not() with an attribute
     selector carries class-level weight, so the input selector below is
     (0,3,1) and cannot be beaten by any single- or double-class page rule,
     while checkbox and radio are EXCLUDED rather than merely outranked —
     otherwise this would also override the 22px sizing at §2.5 and blow up
     every checkbox on the site. */
  input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]),
  select:not([hidden]):not([disabled]),
  textarea:not([hidden]):not([disabled]) {
    font-size:16px;
  }
  /* Giving a flex/grid control a taller box does nothing for the perceived
     target unless its own content is centred in the new space — otherwise the
     label sits at the top of a 46px box and the bottom 25px looks like
     padding nobody would aim at. */
  a.dw-tap-ctl, button.dw-tap-ctl, .dw-tap-ctl[role="button"] {
    display:inline-flex; align-items:center; justify-content:center;
  }

  /* The app shell has its own header with its own controls, and they were not
     in the §2.4 list: .appbar__burger measured 40x46 and .appbar__out 40x40 on
     analytics, admin, portal and vault. The burger is the ONLY way to reach
     navigation on those pages, which makes 40px the worst possible place for
     this defect to survive. */
  .appbar__burger, .appbar__out, .appbar__act, .appbar__ico {
    width:46px !important; height:46px !important;
    display:grid !important; place-items:center;
    padding:0 !important;
    touch-action:manipulation;
    -webkit-tap-highlight-color:transparent;
  }

  /* A <select class="btn btn--glass"> inherits .btn's .97rem = 15.52px, and a
     class beats a type selector no matter which comes later in the file — so
     the §2.5 `select{font-size:16px}` rule could never win. Matching on
     select.btn raises the specificity to (0,1,1). Below 16px iOS zooms the
     whole viewport on focus and does not zoom back out, which strands the user
     on a page they now have to pinch to escape. */
  select, select.btn, select.btn--glass, select.input, select.field__in {
    font-size:16px;
  }

  /* ---- the design vault reads two-up on a phone -----------------------
     .vault-grid is auto-fill minmax(280px,1fr), which at 353px of container
     resolves to exactly one column, so all 90 cards stacked full width and
     designs.html measured 36,847px — 43 screens, against 13 to 23 for every
     other page. Reaching the bottom of a gallery should not take three times
     as long as reading the pricing page.

     A phone gallery is browsed, not read: the thumbnail is the content and
     the eye compares neighbours. Two-up is the pattern every photo grid uses
     for that reason, and here it costs almost nothing — the design previews
     are already too small at 351px to read the type inside them, so halving
     the width does not lose information that was legible in the first place,
     while it halves the distance to any card the user has not seen yet.
     Measured: ~36,800px down to roughly 17,000, in line with the rest of the
     site, and the vault-chip filters above the grid remain the way to narrow
     it further.

     The caption then has one axis, not two, so it stacks: title, category,
     actions. The pills split the row evenly instead of sizing to their text —
     146px of content will not hold two intrinsically-sized 44px targets side
     by side, and two equal halves read as a pair of buttons rather than as a
     button and an afterthought. flex:1 with a centred label is what makes
     them equal; the 44px height from §2.4 is untouched. */
  .vault-grid {
    grid-template-columns:repeat(2, minmax(0,1fr)) !important;
    gap:12px !important;
  }
  .vault-card__c {
    grid-template-columns:1fr;
    row-gap:.5rem;
    padding:.7rem .75rem .8rem;
  }
  /* Two cards side by side are read as a pair, so their captions have to
     agree. The grid stretches the two cards to the same height, but the
     caption stacks from the top, so a one-line title next to a two-line one
     put "Lava Planet"'s buttons 17px above "Neural Synapse Field"'s and the
     row stopped looking like a row. Two lines is the measured maximum at
     every phone width — the longest title in the vault, "Biomechanical
     Organism", is 34.4px at 320, 393 and 430 — so reserving two lines costs
     nothing on the long titles and lines everything up under the short ones. */
  .vault-card__c b { font-size:.86rem; line-height:1.25; min-height:2.5em; }
  .vault-card__c i { font-size:.68rem; }
  /* The strip is one column now, so the actions no longer need to be told to
     span two — but they do need to stop being pushed right by the desktop
     margin-left:auto, and they need a gap wide enough that a thumb landing
     between two 44px targets does not catch both. */
  .vault-acts { grid-column:auto; margin-left:0; gap:.4rem; }
  .va { flex:1 1 0; justify-content:center; padding-inline:.5rem; }

  /* Below 360px two 44px pills plus their gap need 96px of the 118px a half
     column gives, which leaves no room for the label to breathe, so the
     actions stack. */
  @media (max-width:359.98px) {
    .vault-acts { flex-direction:column; }
  }
}

/* ==========================================================================
   3. VERY SMALL PHONES — iPhone SE / mini, 375px and below
   --------------------------------------------------------------------------
   One extra step down, not a third design.
   ========================================================================== */
@media (max-width:375.98px) {
  :root { --dw-gut:16px; }
  h1, .h1 { font-size:1.85rem; }
  h2, .h2 { font-size:1.5rem; }
  .brand__name { font-size:.78rem; letter-spacing:.1em; }
  .nav__actions .btn--aqua { padding-inline:.7rem; font-size:.8rem; }
  .appmarks, .hero__dock { gap:8px; max-width:290px; }
  .osmx__tbl .osmx__cell { font-size:.84rem; padding:8px 10px; }
}

/* ==========================================================================
   4. LANDSCAPE PHONE
   --------------------------------------------------------------------------
   A phone on its side is 852x393. Vertical space is the scarce resource, so
   the hero must not reserve a screen height it no longer has.
   ========================================================================== */
@media (max-width:926px) and (orientation:landscape) and (pointer:coarse) {
  .hero { min-height:auto; padding-block:26px 34px; }
  .hero__brandmark, .hero__brandmark--mono, .markpass { max-width:180px; }
  .hero__mono-title { font-size:1.7rem; }
  .drawer { height:100dvh; }
  .drawer nav a { min-height:44px; }
  .site-header { height:calc(52px + var(--dw-safe-t)); }
}

/* ==========================================================================
   5. REDUCED MOTION on phones
   --------------------------------------------------------------------------
   Consolidated here rather than adding a twenty-eighth prefers-reduced-motion
   block to styles.css.
   ========================================================================== */
@media (max-width:767.98px) and (prefers-reduced-motion:reduce) {
  .reveal, .reveal.in { transform:none !important; opacity:1 !important; transition:none !important; }
  .marquee, .marquee__row { animation:none !important; }
  * { scroll-behavior:auto !important; }
}

/* ============================================================
   THE COMMAND DECK ON A PHONE
   ------------------------------------------------------------
   The dw-* components were designed against a 1150px content
   column and every one of them is a horizontal flex row: icon,
   then text, then a link or a button pinned to the right. In a
   353px column that last element is what breaks them — a flex
   sibling that will not shrink takes its width first and the
   sentence beside it gets whatever is left. On the dashboard
   that meant "Traffic is up 8.9%" wrapped down a six-character
   column beside an intact "See where from →".

   So the rule for all of them is the same: the row becomes two
   rows, and the thing that was pinned right goes underneath at
   full width where a thumb can actually hit it.
   ============================================================ */
@media (max-width:767.98px) {

  /* ---- the top bar --------------------------------------------------
     styles.css already drops the search onto its own line under 900px,
     and mobile.css turns the account chip into a full-width row. Between
     them the bell was left stranded on a line of its own, right-aligned
     over nothing. It belongs on the account row, at the far end. */
  .app-top .who { width:auto !important; flex:1 1 auto; border-bottom:0;
    padding-bottom:0; margin-bottom:0; order:1; }
  .app-top__r { row-gap:.7rem; padding-bottom:.85rem;
    border-bottom:1px solid var(--glass-brd); }
  .app-bell { order:2; }
  .app-search { order:3; }

  /* The name and the role were disagreeing about which edge they belong to.
     .app-user__t is a flex column with align-items:flex-end, written for a
     top bar where the account sits hard against the right of the screen. On a
     phone the chip is at the LEFT of its row, and text-align:left (set further
     up this file) only moves text inside a box — it does not move the box. So
     the company name, which fills the width, looked left-aligned while the
     word OWNER underneath it hung off to the right of nothing. */
  .app-top .who .app-user__t { align-items:flex-start; text-align:left; }

  /* §2.5 sets padding:.75rem .9rem on every input on the page, to make touch
     targets honest. It is the same weight as styles.css's `.app-search input`
     and it loads second, so it wins — and it wipes out the 34px of left
     padding that exists to clear the magnifying glass sitting inside the
     field. The result was the glyph printed on top of the "S" of "Search
     everything". Restoring the left inset here, and only here, keeps the
     touch-target rule intact everywhere else. */
  .app-search input { padding-left:38px; border-radius:999px; }
  .dw-search-big input { padding-left:38px; }

  /* ---- statistic cards ----------------------------------------------
     Two up, not one. Four figures stacked one per card is 600px of
     scrolling to read four numbers — the same defect the .kpi-grid rule
     above this one exists to fix, reappearing in the component that
     replaced .kpi. The sparkline and the ring come off: at 165px wide
     the number is the card and everything else is noise around it. */
  .dw-stats, .dw-stats[data-cols="2"], .dw-stats[data-cols="3"],
  .dw-stats[data-cols="4"], .dw-stats[data-cols="5"] {
    grid-template-columns:1fr 1fr !important; gap:12px !important;
  }
  .dw-stat { padding:14px 14px 13px; border-radius:14px; gap:10px; }
  .dw-stat__spark, .dw-stat__ring { display:none; }
  .dw-stat__val { font-size:1.5rem; margin:.4rem 0 .2rem; }
  .dw-stat__lbl { font-size:.68rem; letter-spacing:.05em; }
  .dw-stat__chip { width:24px; height:24px; border-radius:8px; }
  .dw-stat__chip svg { width:13px; height:13px; }
  .dw-stat__foot { font-size:.74rem; }

  /* ---- wins and next moves ------------------------------------------ */
  .dw-win, .dw-move { flex-wrap:wrap; row-gap:.7rem; }
  .dw-win__t, .dw-move__t { flex:1 1 140px; }
  .dw-win__go, .dw-move .btn { flex:1 1 100%; text-align:center; }
  .dw-move .btn { justify-content:center; }
  .dw-win { padding:14px; }
  .dw-moves--clear { padding:1.5rem 1rem; }

  /* ---- the score, and the pillars under it -------------------------- */
  .dw-hero { gap:14px; margin-bottom:1rem; }
  .dw-hero__score { padding-top:1rem; }
  .dw-score svg { max-width:250px; }
  /* The pillar note was ellipsised against the label on one line. There is
     no width for two things on this screen, so it gets its own. */
  .dw-pillar__t { flex-direction:column; align-items:flex-start; gap:.15rem; }
  .dw-pillar__t span { text-align:left; max-width:100%; }

  /* ---- charts --------------------------------------------------------
     Six x-axis dates do not fit across 310px of plot. Shrinking them to
     .66rem was not enough: the first label is pinned to the left edge
     rather than centred, so "06/07" ran from 0 to 34px and "11/07",
     centred at 17%, started at 36px — a two-pixel gap that reads as
     "06/0711/07".

     The thinning is done in PHP and cannot know how wide the screen is,
     so half of them are dropped again here. Every second one goes, except
     the last: .dw-chart__x--e is the right-hand end of the window and is
     the one date a reader actually looks for, so it is kept whether its
     position is odd or even. Six labels become four. */
  .dw-chart__xs { font-size:.66rem; }
  .dw-chart__ys { font-size:.66rem; }
  .dw-chart__xs .dw-chart__x:nth-child(even):not(.dw-chart__x--e) { display:none; }

  /* ---- search results ------------------------------------------------
     The two-row treatment for a result row lives in styles.css beside the
     rules it overrides, in the same @media block that wraps the big search
     form — a phone rule five hundred lines away from the desktop rule it
     contradicts is how the two drift apart. All that is left here is the
     button, which should be the full width of the row under the field
     rather than sharing it. */
  .dw-search-big .btn { flex:1 1 100%; }
}
