/* ═══════════════════════════════════════════════════════════════════════════
   mobile-2027.css — the M2 mobile refinement layer
   ═══════════════════════════════════════════════════════════════════════════

   The site grew as a stack of layers (styles.css → … → nav-2027.css), each one
   authored desktop-first and each one adding its own breakpoints. That worked
   for layout, but it left the phone with the residue: components whose last
   collapse step was never written, touch targets sized for a mouse, scrollers
   that chain into the page behind them, and 15px form fields that make iOS zoom
   the whole viewport and never zoom back.

   This file is the sweep. It loads LAST — after nav-2027.css — so an equal
   specificity selector here wins on source order alone, which is why almost
   every rule below can stay at the same weight as the one it corrects instead
   of escalating to !important.

   Rules of the house are unchanged: flat surfaces (no gradients), locked
   palette tokens only, DM Serif display type untouched, and the Round-6
   corner-bracket eyebrow standard left alone. Nothing here changes a desktop
   layout — every rule is inside a max-width query, a pointer/hover query, or
   corrects something that was broken at every width.

   §1  Viewport & safe area          §6  H27 theme components
   §2  Header & mobile drawer        §7  M27 product surfaces
   §3  Footer                        §8  Forms & tables
   §4  Cookie banner                 §9  Touch targets
   §5  Chat widget
   ═════════════════════════════════════════════════════════════════════════ */


/* ══ §1 · VIEWPORT & SAFE AREA ═══════════════════════════════════════════ */

/* The page is viewport-fit=cover, so on a notched phone the header must own
   the status-bar strip. It was being handled with padding-top on .header —
   but .header is a fixed-height box under border-box, so padding SHRANK the
   content instead of growing the bar, crushing the logo into the burger.

   Folding the inset into the token instead keeps every consumer in sync: the
   spacer below the header, .mobile-menu's top offset, and scroll-padding-top
   for anchor links all read the same value. env() is 0 everywhere it does not
   apply, so desktop is untouched. */
:root { --header-height: calc(72px + env(safe-area-inset-top, 0px)); }
.header { padding-top: env(safe-area-inset-top, 0px); }


/* ══ §2 · HEADER & MOBILE DRAWER ═════════════════════════════════════════ */

/* The drawer was designed twice. The 2026 pass gave it a translateX slide with
   a pointer-events handoff; the original 2025 rule left `display:none` behind.
   display and transform change in the same frame, so the transition never got
   a chance to run in either direction and the panel hard-cut in and out.
   Swapping the display toggle for visibility restores the slide and makes the
   pointer-events choreography that was already written actually mean something. */
.mobile-menu { display: block; visibility: hidden; }
.mobile-menu.active { display: block; visibility: visible; }

/* overflow:hidden on <body> does not hold on iOS, and a scroller with no
   overscroll-behavior chains into the page the moment it hits either end —
   so the drawer scrolls, reaches its last link, and keeps going into the
   article behind it. Every other overlay in the codebase contains its scroll;
   this one was the exception. */
.mobile-menu { overscroll-behavior: contain; }

/* The drawer sits at bottom:0 on a cover-fit viewport. Its 40px bottom pad is
   static, so the final CTA ("Request a demo" / "Log in") landed inside the home
   indicator's gesture strip, where a tap competes with the OS swipe. The side
   insets matter too — a notched phone in landscape claims real width. */
.mobile-menu {
  padding-left:   max(var(--space-5), env(safe-area-inset-left, 0px));
  padding-right:  max(var(--space-5), env(safe-area-inset-right, 0px));
  padding-bottom: calc(var(--space-10) + env(safe-area-inset-bottom, 0px));
}

/* "Product / Solutions / Resources / Company" — the top level of the phone's
   only navigation — were wearing the 10px uppercase eyebrow token, which made
   them SMALLER than the child links nested under them. The hierarchy read
   upside down and the labels sat under the readable floor. The eyebrow
   treatment belongs on .mobile-menu__sublabel (the column headings inside an
   open section), and it keeps it. */
.mobile-menu__accordion .mobile-menu__label {
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-transform: none;
  color: var(--color-text);
}

/* …and the row that carries them was a ~36px target on the four most-tapped
   controls in the menu. */
.mobile-menu__accordion {
  min-height: 48px;
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
}

/* The 44px tap-target safety net in responsive-refinement.css lists `.hamburger`
   — a class that appears in no template (the markup is class="header__toggle"
   id="hamburger"), so the rule has always been inert. This is the burger, at
   the size that rule intended. */
.header__toggle { width: 44px; height: 44px; }

/* Between 769 and 1024px the header was logo + burger across a thousand pixels
   of empty rail, with the primary CTA reachable only by opening the drawer.
   There is room for it long before that. The !important matches the equally
   !important rule it corrects. */
@media (min-width: 641px) and (max-width: 1024px) {
  .header__actions { display: flex !important; }
  /* the ghost "Log in" link is the one that can go — the demo CTA is the point */
  .header__actions .nav-link--ghost { display: none; }
}

/* The mega-menu scrim is a fixed element inside <header>, and .header carries
   backdrop-filter — which makes it the containing block for fixed descendants.
   inset:0 therefore resolved against the 72px header box, so `top:72px` plus
   `bottom:0` computed to zero height: the scrim never painted and its
   click-to-close handler could never fire. On a touch tablet above 1024px
   (where the flyout is still live and there is no pointerleave) that left the
   open menu with no way out but the trigger itself.

   Pinning the scrim to the real viewport costs nothing on desktop and gives
   the tap-outside back. */
.n27-scrim {
  position: fixed;
  top: var(--header-height);
  left: 0;
  right: 0;
  bottom: 0;
  height: auto;
}
@supports (height: 100dvh) {
  .n27-scrim { height: calc(100dvh - var(--header-height)); }
}


/* ══ §3 · FOOTER ═════════════════════════════════════════════════════════ */

/* The footer body goes single-column at 1100px but the link nav stayed at four
   tracks until the 720px accordion took over. In between — 768px iPad portrait
   included — each track is ~153px, and "BILLING & INSURANCE" at 11.5px/700 with
   .14em tracking needs more than that beside its icon, so the heading wrapped
   under itself and the caret's margin-left:auto collapsed. */
@media (max-width: 1100px) and (min-width: 721px) {
  .f27-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Stacked link lists at ~39.75px with gap:0 means adjacent links touch — eight
   of them in the Platform column, no separation, all just under target size. */
@media (max-width: 720px) {
  .f27-col__list { gap: 2px; }
  .f27-col__list a { padding-block: 12px; }
}

/* Six legal links ("Privacy Policy", "Sitemap", "Terms of Use", …) at ~22px
   tall separated by 2px of horizontal gap. The ≤480 block already tried to fix
   this but restated the base padding verbatim, so it was a no-op. The "/"
   separators are flex children: once the row wraps they strand a leading slash
   at the start of the new line, so they leave on mobile too. */
@media (max-width: 720px) {
  .f27-legal { gap: 4px 6px; }
  .f27-legal a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 8px 10px;
  }
  .f27-legal span[aria-hidden="true"] { display: none; }
}


/* ══ §4 · COOKIE BANNER ══════════════════════════════════════════════════

   Deliberately empty. The banner ships its own <style> inside the include, and
   that block sits in <body> — later in the document than every <link> in
   <head>, so at equal specificity it beats this file no matter what order the
   stylesheets load in. Its mobile fixes (z-index above the chat widget, the
   safe-area bottom inset, 44px consent buttons) therefore live in
   templates/includes/cookie_banner.html, next to the rules they correct.

   The one thing this file does rely on is --ck27-h, which that include's script
   publishes on :root while the full-bleed banner is on screen. §5 uses it to
   lift the chat bubble clear of the consent buttons. */


/* ══ §5 · CHAT WIDGET ════════════════════════════════════════════════════ */

/* --ck27-h is set by the banner's own script while the banner is on screen and
   cleared when it is dismissed; the FAB rides above it and settles back down.
   Also lifts clear of the home indicator on its own account. */
.chat-widget {
  bottom: calc(24px + env(safe-area-inset-bottom, 0px) + var(--ck27-h, 0px));
  transition: bottom 260ms cubic-bezier(.16, 1, .3, 1);
}
@media (max-width: 480px) {
  .chat-widget { bottom: calc(16px + env(safe-area-inset-bottom, 0px) + var(--ck27-h, 0px)); }
}

/* The drawer is a full-screen panel at z-index 996; the FAB is 9999. With the
   menu open the bubble floated over the drawer's own CTA and stayed clickable,
   so a tap meant for "Request a demo" opened a chat on top of the navigation. */
body.menu-open .chat-widget { visibility: hidden; pointer-events: none; }

/* The message input inherits --text-sm, which computes to 13.2px at 320px.
   Under 16px, iOS Safari zooms the viewport on focus and does not zoom back —
   the user is left on a scaled page that now scrolls sideways, with the chat
   panel they were typing into off screen. */
@media (max-width: 768px) {
  .chat-widget__chat-input { font-size: 16px; }
}

@media (max-width: 480px) {
  /* right:-16px pinned the panel's right edge exactly at 100vw: no gutter on
     the right, 16px on the left, and the top corner radius plus the shadow
     sliced off. Symmetric gutters make it read as the floating card it is. */
  .chat-widget__panel {
    right: 0;
    width: calc(100vw - 32px);
    border-radius: var(--radius-xl);
  }
  /* messages had a hard 200px min-height it could not shrink below, so with
     the demo date picker open the stack needed ~582px inside a panel capped at
     ~547px on an iPhone SE — the last rows of the date grid and the panel
     footer were clipped away with no scroll affordance, and those dates were
     simply unreachable. */
  .chat-widget__messages { min-height: 120px; max-height: none; }
  .chat-widget__dynamic  { max-height: 40vh; }
}


/* ══ §6 · H27 THEME COMPONENTS ═══════════════════════════════════════════ */

/* A wrapping tab strip breaks its own indicator. Each button draws the active
   2px coral rule with ::after{bottom:-1px}, which only meets the container's
   border-bottom while the strip is one row — wrapped to two or three rows at
   phone width, a row-one indicator floats mid-strip and reads as a strikethrough.
   One scrolling row keeps the indicator where it belongs. */
@media (max-width: 640px) {
  .h27 .h27-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .h27 .h27-tabs::-webkit-scrollbar { display: none; }
  .h27 .h27-tabs button { flex: 0 0 auto; }
}

/* .h27-bignum is a baseline flex row: a 64px numeral beside its label, no wrap.
   At 320px the split's side has ~236px of content box, the numeral takes ~80px
   plus a 14px gap, and the label's longest word regularly needs more than the
   ~140px left. Neither the row nor the side clips, so the overrun was being
   amputated silently by .h27's overflow-x:clip. */
@media (max-width: 640px) {
  .h27 .h27-bignum { flex-wrap: wrap; }
}

/* --h27-num floors at 46px and .h27-wall__cell clips. A value like "$1.24M" or
   "18,400" at 46px DM Serif is wider than the ~244px cell at 320px, so it was
   being cut mid-glyph rather than shrinking. The --4 variant already floors
   lower; this brings the base variant in line. */
@media (max-width: 640px) {
  .h27 .h27-wall__num { font-size: clamp(34px, 11vw, 56px); }
}

/* The testimonial rail is a mandatory-snap x-scroller with no scroll
   containment — swiping past the last card chained into the document or armed
   the iOS back-swipe mid-carousel. Its sibling scroller already contains. */
.h27 .h27-rail {
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

/* ~39px on real navigation links */
@media (max-width: 640px) {
  .h27 .h27-pills a { padding-top: 13px; padding-bottom: 13px; }
}

/* --2 is the one grid with no intermediate step: --3 and --4 go 2-up at 900px
   and everything flattens at 640px, but a two-card row held its pair down to
   641px, where a card with 24px padding gets ~250px of content. */
@media (max-width: 760px) {
  .h27 .h27-grid--2 { grid-template-columns: minmax(0, 1fr); }
  /* A cell asking to span two tracks in a grid that now has one does not clamp
     — it invents an implicit second column and renders at roughly double the
     container width. The theme drops .h27-span-2 at 640, so collapsing the
     grid at 760 opened a 641–760 band where the two disagreed. Closing it here
     rather than per page, since it catches every future lead card too. */
  .h27 .h27-grid--2 .h27-span-2 { grid-column: span 1; }
}


/* ══ §7 · M27 PRODUCT SURFACES ═══════════════════════════════════════════ */

/* The shot-wall tab strip is authored class="m27-shots__tabs h27-tabs" and was
   given overflow-x:auto, scroll-snap and touch momentum — all of it dead code,
   because flex-wrap:wrap leaks in from .h27-tabs at equal specificity and was
   never reset. Flex items wrap instead of overflowing, so between 561 and 900px
   (where each tab is pinned to min-width:168px) the five screenshot tabs stacked
   into three rows of chunky cards and pushed the capture itself below the fold. */
.h27 .m27-shots__tabs { flex-wrap: nowrap; }

@media (max-width: 560px) {
  /* The label was display:none'd, leaving only .m27-shots__no — which is
     authored aria-hidden. Every tab in the wall therefore had an empty
     accessible name on a phone: five nameless tabs to a screen reader, five
     identical "01…05" chips to everyone else. Clipping instead of hiding keeps
     the name in the a11y tree; only the sub-label actually leaves. */
  .h27 .m27-shots__lbl {
    display: block;
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* three columns → two at 900px, and that was the last step. At 320px the
     panel's 18px side padding leaves 244px, the 22px gap takes another, and a
     16px icon plus its gap eats into what is left — about 85px of text per
     column at 13.5px, six characters to a line, for a six-item feature list. */
  .h27 .m27-caps__grid { grid-template-columns: minmax(0, 1fr); }

  /* the ≤560 crumb cleanup required a .m27-shots ancestor, so every standalone
     .m27-still kept its breadcrumb and crushed 38px of chrome trying to fit
     brand + crumb + live pill + a 90px nowrap "Full size" into ~256px */
  .h27 .m27-still .m27-app__crumb { display: none; }
}

/* Stills were also using the wall's window metrics only when nested inside it. */
.h27 .m27-still .m27-app { border-radius: 13px; }
.h27 .m27-still .m27-app__chrome { height: 40px; padding: 0 13px; }

/* --shot-cap is vh-based to stop portrait captures towering on a desktop, but
   nothing floored it: in phone landscape (740×360) 70vh is 252px, which caps a
   portrait still at ~131px wide — a thumbnail of a clinical screen. */
.h27 .m27-shot      { --shot-cap: max(360px, min(72vh, 620px)); }
.h27 .m27-shot--tall{ --shot-cap: max(360px, min(78vh, 720px)); }
.h27 .m27-still     { --shot-cap: max(320px, min(62vh, 560px)); }
.h27 .m27-still--tall { --shot-cap: max(320px, min(70vh, 640px)); }

/* An ultra-wide capture (--shot-ar up to 2.99) at 100% width is a pure
   downscale: at 320px the frame is ~266px across and ~97px tall, and a
   2123×862 schedule grid rendered into it is not a picture of software, it is
   a smudge. Capping the frame's flatness lets object-fit:cover crop instead of
   shrink, anchored top-left so the row labels — the part that carries the
   meaning — survive the crop. The plain declaration first is the fallback for
   engines that will not take a math function in aspect-ratio. */
@media (max-width: 700px) {
  .h27 .m27-shot__pic {
    aspect-ratio: var(--shot-ar, 2);
    aspect-ratio: min(var(--shot-ar, 2), 1.55);
  }
  .h27 .m27-shot__pic img { object-position: 0 0; }
  /* with pins gone below 900px the lightbox IS the way to read a capture, so
     its trigger stops being a 10.5px whisper in the chrome bar */
  .h27 .m27-shot__zoom {
    min-height: 34px;
    padding: 8px 13px;
    font-size: 12px;
  }
}

/* The lightbox bar is an unwrapped flex row of title + hint + close, and the
   close button carries margin-left:auto — so on a fixed overlay at 320px the
   overflow goes off the right edge invisibly, and the thing that goes is the
   only way out of a full-screen modal. */
@media (max-width: 640px) {
  .m27-lb__hint { display: none; }
  .m27-lb__t {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .m27-lb__x { width: 44px; height: 44px; }
}

/* Panning a 2123px capture to its edge scrolled the document behind the modal
   or triggered an edge-swipe dismissal. Given the crop above, this scroller is
   the full-fidelity reading experience for every real screenshot on the site. */
.m27-lb__scroll { overscroll-behavior: contain; }

/* :hover sticks on touch — and below 900px the notes list is the entire
   annotation, because the pins are hidden. A tapped row stayed offset and
   coral-ringed until another was tapped. .is-hot is what m27-shots.js actually
   drives, and it keeps working. */
@media (hover: none) {
  .h27 .m27-shot__notes li:hover:not(.is-hot) {
    transform: none;
    background: transparent;
    box-shadow: none;
  }
}

/* space-between with no gap and no wrap: six 10px uppercase tick labels with
   .06em tracking butt into each other in a ~244px panel body at 320px */
@media (max-width: 560px) {
  .h27 .m27-axis { gap: 4px; overflow: hidden; }
}


/* ══ §7b · HOMEPAGE DASHBOARD MOCK ═══════════════════════════════════════
   These two live in styles.css, which the homepage layer loads on top of but
   never corrects. Kept here rather than in home-2027.css because the mock's
   own rules are in the legacy layer and this is the only file that outranks
   both without escalating. */

/* .dashboard-v2__body is height:560px — a hard lock, not a minimum — with
   overflow:hidden, and nothing overrides it at any width. At the same time the
   ≤768 tier turns the content rows from two columns into one and the metrics
   into 2×2, which makes the content TALLER while the box stays 560px. Whatever
   falls past that line is sheared off with no scrollbar and no hint it exists. */
@media (max-width: 768px) {
  .dashboard-v2__body { height: auto; min-height: 0; overflow: visible; }
  .dashboard-v2__main { overflow: visible; }
}

/* The ≤900 correction that pulls this badge back inside the frame lives in
   styles.css at one class of specificity; home-2027.css re-declares the offset
   at two and loads later, so the correction has never applied. In the 769–900
   band the badge hangs 18px off the frame over the collapsed icon sidebar, and
   the stage's clip-path:none guarantees nothing trims it. */
@media (max-width: 900px) {
  .h27 .float-badge--info { left: 4px; }
}


/* ══ §7c · LEGACY LAYER CORRECTIONS ══════════════════════════════════════
   Four rules in styles.css / responsive-refinement.css that read as mobile
   handling but never take effect, each for a different cascade reason. */

/* Act 04 on the homepage. styles.css sets `.module-panel__info { padding:
   56px 48px 48px !important }` with no media query, which beats the 768px
   override that tries to reduce it to 32px/24px — !important wins over a
   non-important declaration no matter which matches. At 320px the panel box is
   280px wide, so 48px of padding on each side leaves 184px for a paragraph and
   a button. Matching the !important is the only way to answer it without
   editing the legacy file. */
@media (max-width: 768px) {
  .module-panel__info { padding: 32px 24px 28px !important; }
}

/* The chat flow's quick replies are its primary control and compute to ~31px
   (7px padding on an 11.5px line), and the booking slots to ~38px — sitting
   6px apart, so a mistap books a different appointment time. */
@media (pointer: coarse) {
  .chat-widget__qr-btn { min-height: 44px; padding-top: 11px; padding-bottom: 11px; }
  .cw-timepicker__slot { min-height: 44px; }
}

/* responsive-refinement.css folds env(safe-area-inset-*) into .container
   inside an @supports block, then overrides the same property four times lower
   down at 768/480/380/360 — media queries add no specificity, so the last
   match wins and the inset is gone on exactly the devices that have one. On a
   notched phone in landscape the inset is ~59px against 20px of padding, so
   the first ~39px of every line sits under the rounded corner.
   Restoring it here keeps the intended padding as the floor. */
@media (max-width: 768px) {
  .container {
    padding-left:  max(var(--space-5), env(safe-area-inset-left, 0px));
    padding-right: max(var(--space-5), env(safe-area-inset-right, 0px));
  }
}
@media (max-width: 480px) {
  .container {
    padding-left:  max(var(--space-4), env(safe-area-inset-left, 0px));
    padding-right: max(var(--space-4), env(safe-area-inset-right, 0px));
  }
}

/* Same intent, different trap: the inset was put on .footer-v2, but the footer
   element carries both .footer-v2 and .f27, and footer-2027.css — which loads
   later at equal specificity — sets `padding-inline: 0` on .f27. The rail is
   what actually owns the footer's horizontal padding, so the inset belongs
   there. */
@media (max-width: 768px) {
  .f27 .f27__rail {
    padding-left:  max(var(--f27-pad), env(safe-area-inset-left, 0px));
    padding-right: max(var(--f27-pad), env(safe-area-inset-right, 0px));
  }
}

/* The coverage-map HUD block in responsive-refinement.css marks every
   declaration !important except four, and home.css loads later — so the panel
   gets pinned to the viewport edges by its siblings while keeping full desktop
   internal padding, which is the crowding the block exists to prevent. */
@media (max-width: 600px) {
  .cov-hud-hero { padding: .65rem .8rem; }
  .cov-hud-hero__row { gap: .5rem; }
  .cov-hud-hero__trend { font-size: .65rem; padding: .2rem .45rem; }
}

/* the chat panel is full-bleed on a phone, so its own small print is being
   read at phone distance, not glanced at in a corner */
@media (max-width: 640px) {
  .chat-widget__footer,
  .cw-pricing__features li,
  .cw-summary__note { font-size: 12.5px; }
  .cw-pricing__badge { font-size: 11px; }
}


/* ══ §8 · FORMS & TABLES ═════════════════════════════════════════════════ */

/* The single highest-impact mobile defect on the site, and it is one line:
   any text field under 16px makes iOS Safari zoom the viewport on focus and
   never zoom back. The user finishes the form on a scaled page that now
   scrolls horizontally — so a form field creates the exact whole-page overflow
   the rest of this file exists to prevent. Applied by input type rather than by
   class so it catches the demo form, the support form, the privacy request,
   the auth pages and anything added later. */
@media (max-width: 768px) {
  input[type="text"],
  input[type="email"],
  input[type="tel"],
  input[type="url"],
  input[type="password"],
  input[type="search"],
  input[type="number"],
  input[type="date"],
  input[type="datetime-local"],
  select,
  textarea {
    font-size: 16px;
  }
}

/* A wide table inside an overflow-x wrapper is the right pattern, but without
   momentum scrolling and containment it fights the page it sits in. */
@media (max-width: 900px) {
  .ptw, .wp-tbl, .h27-tblwrap {
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
  }
}

/* Prose that can carry a pasted URL or a long unbroken identifier — article
   bodies and legal documents — should break it rather than push the column. */
@media (max-width: 640px) {
  .h27 p, .h27 li, .prose p, .prose li, .article-body p, .article-body li {
    overflow-wrap: break-word;
  }
}


/* ══ §9 · TOUCH TARGETS ══════════════════════════════════════════════════ */

/* A coarse-pointer floor for the controls that sit below it, scoped to the
   pointer rather than the width so a touch laptop gets it too. Deliberately
   narrow: it names real controls instead of blanket-padding every <a>, which
   would wreck inline links inside body copy. */
@media (pointer: coarse) {
  .btn,
  .header__toggle,
  .mobile-menu__link,
  .mobile-menu__accordion,
  .f27-col__head,
  .ck27__btn,
  .chat-widget__btn,
  .h27-btn {
    min-height: 44px;
  }

  /* Both logos are ~30px tall links sitting in a much taller bar — the box has
     the room, the link just never claimed it. Growing the link, not the image,
     leaves the lockup exactly where it is. */
  .header__brand,
  .f27-brand__logo { min-height: 44px; align-items: center; }

  /* .h27-link is the site's quiet "Explore →" link: a 26px inline-flex row
     whose underline is a background-image pinned to the bottom of the padding
     box, so padding cannot be used to grow it without dragging the rule off
     the text. An overlay claims the tap area instead and changes no layout.
     Verified free: nothing else in the codebase uses this pseudo-element. */
  .h27 .h27-link { position: relative; }
  .h27 .h27-link::after {
    content: "";
    position: absolute;
    inset: -9px -6px;
  }

  /* the zoom trigger is the only route into a full-size capture once the pins
     go — §7 already made it visible, this makes it tappable */
  .h27 .m27-shot__zoom { min-height: 44px; }

  /* the support page's channel links (email, phone) are block-level 19px
     links in a ~31px box — the biggest type on the card and one of the
     smallest targets on it */
  .h27 .sp-big { padding-block: 7px; }
}


/* ══ §10 · SMALL-TYPE FLOOR ══════════════════════════════════════════════

   The product surfaces deliberately use very small type — they are pictures of
   software, and real UI chrome is small. That reads correctly on a desktop
   where the mock is 900px wide. On a 320px phone the same labels land at
   8.5–10.5px while the mock is now the whole screen, and several of them carry
   content the surrounding copy actually refers to: the CPT and ICD chips, the
   visit line under a patient name, the era tags on the history journey.

   These floors keep every label subordinate to the copy around it — nothing
   here reaches 13px — while pulling the smallest of them back over the line
   where a phone can render them at all. */
@media (max-width: 640px) {
  .h27 .m27-tag,
  .h27 .m27-state,
  .h27 .m27-rec__m          { font-size: 11px; }
  .h27 .m27-pan__s          { font-size: 11.5px; }
  .h27 .h27-jrn__tag        { font-size: 11.5px; }
  .h27 .m27-still__cap      { font-size: 12.5px; }

  /* The hero mock tunes these down again at three classes deep — 8.5px and
     9.5px — because it was drawn against a fixed 486px frame. At ≤900 the
     stage gives up that cap and takes the full column, so the reason for the
     extra shrink is gone while the shrink itself stayed. Matched at the same
     depth so the mobile value actually lands. */
  .h27 .m27-hero .m27-state,
  .h27 .m27-hero .m27-tag,
  .h27 .m27-hero .m27-rec__m { font-size: 11px; }

  /* the pill that dates the chart it sits on ("Last 6 months") */
  .dashboard-v2__card-pill { font-size: 11px; }

  /* "Chapter 3 of 11" — the only thing telling a phone reader where the
     section picker will take them */
  .h27 .h27-index__pickmeta { font-size: 11.5px; }

  /* the caption under each KPI figure — "Documentation time", "Transcription
     accuracy". The number means nothing without it. */
  .h27 .m27-kpi span { font-size: 11.5px; }
}
