/* ============================================================================
   Booking homepage designs — one markup, six layouts.

   Served for /home/{accountName} through templates/bookingHomeDesigns.html +
   js/app-v2/bookingHomeDesignsView.js + templates/legacy/app-v2/bookingHomeDesignsTemplate.html.

   The business picks its layout in Online Booking → Booking Page → Homepage Settings ("APPEARANCE");
   the value is stored on BookingPreferencesJDO.homePageDesign, allow-listed by
   Constants.HOME_PAGE_DESIGNS, and applied by the view as data-pts-design on .pt-home-shell.

     clean      (base, default)  balanced cards
     showcase                    large visual hero, 2-up with a featured wide tile
     directory                   dense list-first, minimal radii and shadows
     soft                        rounded and spacious, hero in a rounded frame
     spotlight                   hero-led landing page, flat identity row, 4-up tiles
     split                       sticky identity rail beside a scrolling content column

   Everything is scoped under .pt-home-shell with a pts- prefix and --pts-* tokens, so this
   file cannot affect newbookingpage.css or any existing booking template. Adding a design
   means adding one [data-pts-design="…"] block here plus one option in the settings dropdown
   and one entry in Constants.HOME_PAGE_DESIGNS — no markup or view changes.
   ============================================================================ */

/* Page-level reset — safe because only templates/bookingHomeDesigns.html loads this file.
   newbookingpage.css (loaded before this one, for the shared login/appointments modals) sets
   body{display:flex;flex-direction:column;overflow:auto}. An `overflow` value other than
   visible on an ancestor makes that ancestor the scrollport, which stops position:sticky from
   ever engaging — that would silently break the sticky header, section nav and split rail. */
html,body{overflow:visible}
body{display:block;background:#f6f6f8}
#pt-home-root{display:block}

/* Visually hidden server-rendered SEO summary in the shell (outside .pt-home-shell). */

/* ==========================================================================
   BASE = "clean". Every other design overrides tokens and a handful of rules.
   ========================================================================== */
.pt-home-shell{
  /* ACCENT — the merchant's BOOKING PAGE THEME. Only --pts-accent and --pts-accent-on are ever
     stated per theme (see the [data-pts-accent] blocks below); the other three are DERIVED from it,
     so a new theme is one line and the five can never drift out of step. What each one is for:
       --pts-accent        the fill: Book-now buttons, the header brand mark, badge tints
       --pts-accent-on     text ON that fill — white for every theme except yellow, which takes dark
                           ink exactly as the classic .yellow-summary-color pair does
       --pts-accent-ink    accent-hued text on a LIGHT surface (eyebrows, "Book now →" links, badge
                           glyphs, active nav pills). Never use --pts-accent for text: at 9% of the
                           themes it fails contrast outright (yellow on white is 1.8:1)
       --pts-accent-soft   the pale tint behind badges, chips and today's hours row
       --pts-accent-hover  the primary button's hover fill — a darkened accent, so --pts-accent-on
                           stays readable on it and the button does not flip its text colour
     The purple below is the fallback when no theme resolves; it is also what every account rendered
     before the theme was wired in. The mixes reproduce the hand-picked values it used to carry
     (#f2ecff / #5521b5) to within a shade. */
  --pts-accent:#6d28d9;
  --pts-accent-on:#fff;
  --pts-accent-rgb:109,40,217;
  --pts-accent-soft:#f2ecfc;--pts-accent-ink:#551fa9;--pts-accent-hover:#5e22bb;
  --pts-badge-sub:#e5d8f8;--pts-accent-fill:#e8ddf9;--pts-header-tint:#fbfafe;--pts-avatar-tint:#e5d8f8;
  --pts-edge-12:#d8d0e9;--pts-edge-14:#d6cce8;--pts-edge-18:#d1c5e8;--pts-edge-30:#c2aee6;--pts-edge-35:#bca4e5;
  --pts-map-water:#e4f2fa;--pts-review-mine-bg:#def5e7;
  --pts-badge-open-bg:#daf3e4;--pts-badge-open-today-bg:#cdf0db;
  --pts-badge-closed-bg:#fbe4e4;--pts-badge-closed-today-bg:#f9dbdb;
  --pts-bg:#f6f6f8;--pts-surface:#fff;--pts-surface-2:#fafafa;
  --pts-surface-rgb:255,255,255;--pts-bg-rgb:246,246,248;--pts-line-rgb:231,231,235;
  --pts-text:#18181b;--pts-muted:#6f6f78;--pts-line:#e7e7eb;
  --pts-success:#178642;--pts-warning:#e58a00;--pts-danger:#c92a2a;
  /* Pill FILL hues, siblings of the inks above (same naming idea as --pts-accent-soft). The inks are
     deep, so a pastel mixed from them comes out grey-green / grey-pink; these brighter hues mix to the
     mint and blush a status pill wants while the TEXT stays the semantic ink. Fill only — never text:
     they are too light to pass contrast on their own.
     ⚠️ NOT referenced by any rule any more, and NOT dead: these two are the SOURCE the precomputed
     --pts-badge-*-bg / --pts-review-mine-bg literals below are mixed from (see the accent-token note at
     the top — color-mix() had to leave the stylesheet for browser support). Change a hue here, then
     re-run scripts/gen-accent-tokens.py; deleting them loses the only record of where those fills came
     from, and BookingHomeDesignsColorCompatTest checks the literals against exactly these values. */
  --pts-success-soft:#2fbf6a;--pts-danger-soft:#e35555;
  /* The floating "Ready when you are" pill, as TOKENS, because the footer's bottom padding is derived from
     them (see .pts-footer). --pts-cta-h is the pill's measured height with the shell's FULL stylesheet set
     (newbookingpage.css leaks into it) and the template's real markup — the button carries
     `pts-btn-small`, and a probe that drops that class reports 60px instead of 54px, which is how the
     footer ended up 7px too tall once already. Re-measure it if the pill's padding, type or content
     changes; nothing else needs touching, the padding follows. */
  --pts-cta-h:54px;--pts-cta-gap:11px;
  /* Picktime's heart, for the footer credit. The brand value is #fb3957 — what every classic booking
     footer paints — but those footers are near-black, and the SAME red is a different colour to the eye
     on white: 4.62:1 on the classic's #1e1e1e against 3.61:1 here, where it goes pink and washed rather
     than red. So the hue is kept and darkened until it matches the classic's own contrast against its
     own ground (#d8314b, 4.71:1) — the same "a fill is not a foreground" reasoning as --pts-accent-ink.
     Dark appearance restores the raw brand value, which is exactly the classic's situation. */
  --pts-heart:#d8314b;
  --pts-page:1120px;--pts-gap:28px;
  --pts-radius:12px;--pts-radius-sm:12px;--pts-shadow:0 18px 50px rgba(24,24,27,.08);
  --pts-header-h:66px;--pts-overlap:-58px;--pts-section:66px;
  /* The mark-to-wordmark gap of the brand lockup. ONE token because the page draws that lockup TWICE —
     .pts-brand in the header and .pts-footer-lockup in the footer — and the two are read as the same
     logo: the footer sat at 11px against the header's 6px and read as a looser, different lockup. The
     footer mark is the larger box (38px vs 34px), so the gap is the only thing that can hold them
     together; keep both on this token rather than restating a number in either place. */
  --pts-brand-gap:6px;
  --pts-brand-max:260px;
  /* Banner sizing — see .pts-cover. The cover has NO fixed height: it derives one from the
     banner's own aspect ratio so the whole image is always visible. 2.5 is the shipped industry
     banners' ratio (1000x400) and the pre-load placeholder; bookingHomeDesignsView publishes the
     real ratio once the image has loaded. --pts-cover-max is only a guard against a near-square
     upload turning the hero into a viewport-tall block.
     --pts-cover-ar-default is the OTHER case: a per-industry stock banner, which the page crops to
     this measure instead of sizing itself to the source (see .pts-cover-default). 4 is not a taste
     call — it is the aspect the banner cropper locks merchant uploads to
     (imageParams.aspect = 4 in views/onlineBookingView.js), so a business that never uploaded a
     banner gets a hero the same height as one that did. */
  --pts-cover-ar:2.5;--pts-cover-max:min(88vh,900px);--pts-cover-ar-default:4;
  /* ...and --pts-cover-w is the same guard on the OTHER axis. The four block designs run the
     banner full-bleed, so on a very wide display (a 1920px monitor, a 2560px one, a TV) it kept
     growing while every other thing on the page stayed inside the 1120px page measure: a
     ~700px-tall band of photograph above a narrow column of content, with the identity card
     floating in the middle of it. The cap is drawn on the LAPTOP line, which is the whole decision:
     a laptop keeps the full-bleed hero the merchant's banner was designed against, and every
     display wider than a laptop frames it instead. 1750 is that line — the widest ordinary laptop
     (16" MacBook = 1728 CSS px) plus a small allowance, so the fit is not knife-edge — and it stays
     well clear of a 1080p monitor, which must NOT go full-bleed. So the two numbers that matter are
     the bounds, not this exact value: keep it at or above 1728 and below 1920 (what the guard test
     asserts). Past the cap the box keeps its ratio and the auto inline margins centre it over the
     page (the same treatment `soft` already gives its framed banner). */
  --pts-cover-w:1950px;
  --pts-tiles:3;--pts-rows:2;--pts-team:4;
  /* Category-block rhythm (see .pts-cat): space ABOVE a category header, and between the header and the
     items it introduces. Tokens rather than per-design rules on purpose — a design block would out-rank
     `.pts-cat:first-of-type{margin-top:0}` (0,2,0) with its own (0,3,0) selector and indent the FIRST
     category of every group. Re-point these instead of restating the margins. */
  --pts-cat-gap:24px;--pts-cat-lead:14px;
  --pts-font:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;

  /* CONTENT TYPE SCALE (headings are separate — see the mobile blocks at the bottom of the file):
       15px  body / lead copy — section + panel intros, and the inherited default that buttons and the
             header brand pick up
       14px  primary text — links, prices, reviewer names, review + group-intro copy, header nav
       13px  secondary text — identity meta, tile copy, contact + detail rows, chips, nav pills, hours
             rows (13.5, half a step up so a row outweighs its status pill)
       12px  captions and labels — eyebrows, counts, tile meta, notes, footer, hours pill (12.5)
       11px  micro only — review dates, price sub-notes, CTA sublines
     Everything was one step (~1px) smaller, which read as cramped next to the 16-46px headings. Do not
     drop content below 11px, and keep every step relative: bumping one value in isolation is what
     flattens the hierarchy. Icon/initial glyph sizes (.pts-logo, .pts-*-avatar, .pts-*-icon,
     .pts-modal-close) are box-relative, NOT part of this scale — leave them out of any resize.

     TRACKING LADDER (letter-spacing) — the page has TWO systems, and every HEADING AND CARD TITLE
     belongs to the first:
       · DISPLAY text tightens as it grows, in 0.005em steps off the size:
           28px -.04   ·   27px -.045 (the business name, one notch tighter by design)
           22px -.035  ·   20px -.03  ·   17-18px -.025  ·   16px -.02  ·   15px -.015  ·  14px -.01
       · MICRO UPPERCASE labels open up instead: .pts-eyebrow +.12em, .pts-subhead / .pts-cat-name +.06em.
     Body copy, prices, metas, buttons and sentence-case CTAs stay `normal`: tracking is for type that is
     either large enough to close up or small-and-capped enough to need air.
     ⚠️ A heading left at `normal` is not neutral, it is OFF the ladder — at 15-18px the browser default
     is looser than its neighbours, so the same offering name set visibly wider in a class row than in a
     service tile. Four surfaces sat outside it (.pts-row h4, .pts-team-card h3, .pts-faq summary,
     .pts-loc-card h3) and were brought on; BookingHomeDesignsContentScaleTest pins the table.
     ⚠️ TWO PAIRS ARE DELIBERATELY OFF-LADDER, and each is off it for the same reason — the two halves of
     ONE object must track alike, which beats matching their own size band. `.pts-footer-name` (17px)
     holds -.02em to stay in step with the header's `.pts-brand` (16px), because the two ARE one lockup
     (see the brand-gap note); `.pts-map-face strong` (15px) holds -.01em to stay in step with
     `.pts-map-note strong` (13px), the same branch name in the panel's other state. Do not "correct"
     either one to its size band — that splits a pair a visitor reads as a single mark.
     State tracking in `em`, never px, so the responsive size overrides at the bottom of the file scale it
     for free — they set the SIZE only and inherit this. */
  display:block;background:var(--pts-bg);color:var(--pts-text);
  font-family:var(--pts-font);line-height:1.5;font-size:15px;min-height:100vh;

  /* 🔑 EVERY STRING ON THIS PAGE IS MERCHANT DATA, so one unbreakable run of characters must never be
     able to push the page sideways. `overflow-wrap` is INHERITED, so stating it once here covers every
     field — the business name, an offering's description, a FAQ answer, an address, a reservations
     email, a meeting URL — instead of a rule per text holder that the next new field would miss.

     Measured before this: a business name with a single unbroken run of 25 characters overflowed its
     own box at 320px and, because nothing upstream clips, propagated all the way to .pt-home-shell —
     a horizontal scrollbar on the whole page with content cut off, in all seven designs. At 55
     characters it was +306px at 320px and still +234px at 768px. The same break hit the FAQ summary
     (which pushed .pts-faq-mark off-screen), the location contact rows (a long email overflowed even
     at 1240px), "About {name}", the footer copyright and the team card.

     ⚠️ `anywhere`, NOT `break-word`: only `anywhere` also lets the MIN-CONTENT size shrink, and these
     are flex items (contact rows, fact cells, cards) whose min-content contribution is what dragged
     their container wide. `break-word` would leave that intrinsic floor in place and keep overflowing.

     ⚠️ It cannot loosen anything deliberately clipped: `overflow-wrap` has no effect where wrapping is
     forbidden, so the one-line ellipsised names (.pts-name, .pts-user-name, .pts-brand-name, .pts-btn)
     keep their `white-space:nowrap` behaviour exactly. The team card's 2-line clamp DOES now break a
     run-on name rather than spilling out of the card, which is the point. */
  overflow-wrap:anywhere;
}
.pt-home-shell *{box-sizing:border-box}

/* ==========================================================================
   BOOKING PAGE THEME → ACCENT
   The merchant picks one of ten swatches in Online Booking → Settings ("BOOKING PAGE THEME"),
   stored on BookingPreferencesJDO.themeColor and already driving the booking FLOW through
   newbookingpagetheme.css's `<theme>-summary-color` classes. This page hands off to that flow, so it
   wears the same colour: bookingHomeDesignsView.accent() resolves the value against this exact
   allow-list and writes it as data-pts-accent on .pt-home-shell.

   The hexes are the swatches themselves (onlineBookingTemplate.html's `.colors-box` icons) — keep
   them in step with that picker. Note `default` is BLACK, not the purple this page used before the
   theme was wired in: an account that never touched the setting has a black-buttoned booking page,
   and the homepage matching it is the whole point. To go back to purple for unthemed accounts,
   delete the `default` block (the base fallback then applies) — but the flow will disagree.

   Only --pts-accent is stated; -soft / -ink / -hover derive from it in the base block above. Yellow
   is the one theme that needs more: it is too light for white text (so -on goes dark, mirroring
   .yellow-summary-color) and far too light for accent TEXT (#eeba0b on white is 1.8:1), so its ink
   is mixed much darker — 55% lands at 5.4:1 on white and 5.2:1 on its own soft tint.
   ========================================================================== */
.pt-home-shell[data-pts-accent="default"]{--pts-accent:#1e1e1e;--pts-accent-rgb:30,30,30;
  --pts-accent-soft:#f4f4f4;--pts-accent-ink:#171717;--pts-accent-hover:#1a1a1a;--pts-badge-sub:#d7d7d7;
  --pts-accent-fill:#dbdbdb;--pts-header-tint:#f9f9f9;--pts-avatar-tint:#f4f4f4;
  --pts-edge-12:#cfcfd2;--pts-edge-14:#cbcbce;--pts-edge-18:#c3c3c6;--pts-edge-30:#ababae;--pts-edge-35:#a1a1a3}
.pt-home-shell[data-pts-accent="teal"]{--pts-accent:#008080;--pts-accent-rgb:0,128,128;
  --pts-accent-soft:#e8f4f4;--pts-accent-ink:#006464;--pts-accent-hover:#006e6e;--pts-badge-sub:#d1e8e8;
  --pts-accent-fill:#d6ebeb;--pts-header-tint:#f9fcfc;--pts-avatar-tint:#d1e8e8;
  --pts-edge-12:#cbdbde;--pts-edge-14:#c7d9dc;--pts-edge-18:#bdd4d8;--pts-edge-30:#a2c8cb;--pts-edge-35:#96c3c6}
.pt-home-shell[data-pts-accent="violet"]{--pts-accent:#7b2cbf;--pts-accent-rgb:123,44,191;
  --pts-accent-soft:#f3ecf9;--pts-accent-ink:#602295;--pts-accent-hover:#6a26a4;--pts-badge-sub:#e7d9f3;
  --pts-accent-fill:#eaddf5;--pts-header-tint:#fcfafd;--pts-avatar-tint:#e7d9f3;
  --pts-edge-12:#dad1e6;--pts-edge-14:#d8cde5;--pts-edge-18:#d4c5e3;--pts-edge-30:#c7afde;--pts-edge-35:#c1a6dc}
.pt-home-shell[data-pts-accent="pink"]{--pts-accent:#e05780;--pts-accent-rgb:224,87,128;
  --pts-accent-soft:#fcf0f4;--pts-accent-ink:#af4464;--pts-accent-hover:#c14b6e;--pts-badge-sub:#f9e1e8;
  --pts-accent-fill:#fae4eb;--pts-header-tint:#fefbfc;--pts-avatar-tint:#f9e1e8;
  --pts-edge-12:#e6d6de;--pts-edge-14:#e6d3dc;--pts-edge-18:#e6cdd8;--pts-edge-30:#e5bccb;--pts-edge-35:#e5b5c6}
.pt-home-shell[data-pts-accent="green"]{--pts-accent:#41ac5e;--pts-accent-rgb:65,172,94;
  --pts-accent-soft:#eef8f1;--pts-accent-ink:#338649;--pts-accent-hover:#389451;--pts-badge-sub:#ddf0e2;
  --pts-accent-fill:#e1f2e5;--pts-header-tint:#fafdfb;--pts-avatar-tint:#ddf0e2;
  --pts-edge-12:#d3e0da;--pts-edge-14:#d0dfd7;--pts-edge-18:#c9dcd2;--pts-edge-30:#b5d5c1;--pts-edge-35:#add2ba}
.pt-home-shell[data-pts-accent="orange"]{--pts-accent:#e76f51;--pts-accent-rgb:231,111,81;
  --pts-accent-soft:#fdf2ef;--pts-accent-ink:#b4573f;--pts-accent-hover:#c75f46;--pts-badge-sub:#fbe5e0;
  --pts-accent-fill:#fbe8e3;--pts-header-tint:#fefbfb;--pts-avatar-tint:#fbe5e0;
  --pts-edge-12:#e7d9d9;--pts-edge-14:#e7d6d5;--pts-edge-18:#e7d1cf;--pts-edge-30:#e7c3bd;--pts-edge-35:#e7bdb5}
.pt-home-shell[data-pts-accent="blue"]{--pts-accent:#2088bf;--pts-accent-rgb:32,136,191;
  --pts-accent-soft:#ebf4f9;--pts-accent-ink:#196a95;--pts-accent-hover:#1c75a4;--pts-badge-sub:#d7eaf3;
  --pts-accent-fill:#dbecf5;--pts-header-tint:#f9fcfd;--pts-avatar-tint:#d7eaf3;
  --pts-edge-12:#cfdce6;--pts-edge-14:#cbdae5;--pts-edge-18:#c3d6e3;--pts-edge-30:#abcbde;--pts-edge-35:#a1c6dc}
.pt-home-shell[data-pts-accent="darkblue"]{--pts-accent:#03045e;--pts-accent-rgb:3,4,94;
  --pts-accent-soft:#e8e8f1;--pts-accent-ink:#020349;--pts-accent-hover:#030351;--pts-badge-sub:#d2d2e2;
  --pts-accent-fill:#d7d7e5;--pts-header-tint:#f9f9fb;--pts-avatar-tint:#d2d2e2;
  --pts-edge-12:#ccccda;--pts-edge-14:#c7c7d7;--pts-edge-18:#bebed2;--pts-edge-30:#a3a3c1;--pts-edge-35:#9798ba}
.pt-home-shell[data-pts-accent="yellow"]{--pts-accent:#eeba0b;--pts-accent-on:#1e1e1e;--pts-accent-rgb:238,186,11;
  --pts-accent-soft:#fdf9e9;--pts-accent-ink:#836606;--pts-accent-hover:#cda009;--pts-badge-sub:#433a1b;
  --pts-accent-fill:#fcf4d8;--pts-header-tint:#fffdf9;--pts-avatar-tint:#fcf3d3;
  --pts-edge-12:#e8e2d0;--pts-edge-14:#e8e1cc;--pts-edge-18:#e8dfc3;--pts-edge-30:#e9daa8;--pts-edge-35:#e9d79d}
.pt-home-shell[data-pts-accent="red"]{--pts-accent:#d00000;--pts-accent-rgb:208,0,0;
  --pts-accent-soft:#fbe8e8;--pts-accent-ink:#a20000;--pts-accent-hover:#b30000;--pts-badge-sub:#f7d1d1;
  --pts-accent-fill:#f7d6d6;--pts-header-tint:#fef9f9;--pts-avatar-tint:#f7d1d1;
  --pts-edge-12:#e4cbcf;--pts-edge-14:#e4c7ca;--pts-edge-18:#e3bdc1;--pts-edge-30:#e0a2a5;--pts-edge-35:#df9699}

/* Visually-hidden helper for the server-rendered crawlable summary in templates/bookingHomeDesigns.html
   (class="pts-sr-only"). ⚠️ USED BY THE THYMELEAF SHELL, not by the app-v2 template or the view — a
   dead-class audit that only scans bookingHomeDesignsTemplate.html + bookingHomeDesignsView.js will
   report it as unused. Deleting it makes the whole SEO block render as raw text above the header.
   Clip rather than display:none so the content stays available to crawlers and assistive tech. */
.pts-sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);
  white-space:nowrap;margin:-1px;padding:0;border:0}
.pt-home-shell h1,.pt-home-shell h2,.pt-home-shell h3,.pt-home-shell h4{margin:0;text-wrap:balance}
/* font-size:inherit is a LEAK RESET, not decoration: newbookingpage.css — loaded by this same shell —
   sets a bare `a{font-size:16px}`, which every link this page doesn't size picks up. That is why
   "Get directions" rendered 16px inside a 13px contact row and the footer credit 16px inside 12px
   footer text. A link belongs to the scale of the text it sits in; the two links that are deliberately
   larger (.pts-btn, .pts-brand-name) pin their own size below. Same class of bare-tag leak as the
   `section{padding:12px 0}` reset further down. Note this rule is (0,1,1), so anything sizing an <a>
   must out-specify it or come later in the file. */
.pt-home-shell a{color:inherit;text-decoration:none;font-size:inherit}
.pt-home-shell img{max-width:100%}
/* newbookingpage.css — loaded by this same shell — styles bare tags, and BOTH .pts-section and
   .pts-group are <section> elements, so its `section{padding:12px 0}` silently added 24px to
   every section AND every group (~240px of dead page height, and every gap 24px looser than
   designed). The heading/anchor resets above already neutralise its h1/h2/h3 margins. This page
   owns its own vertical rhythm via --pts-section / .pts-group. */
.pt-home-shell section{padding:0}
.pt-home-shell button{font:inherit;cursor:pointer}
.pts-eyebrow{font-size:12px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--pts-accent-ink); margin-bottom: 12px;}

/* shared centred measure */
.pts-rail,.pts-main{width:min(var(--pts-page),calc(100% - 36px));margin-left:auto;margin-right:auto}

/* ---------- buttons ---------- */
/* 16px is one step above body — a primary CTA should be the loudest non-heading text on the page. It
   used to arrive by accident, inherited from newbookingpage.css's bare `a{font-size:16px}`; pinned here
   so the link reset above cannot silently shrink every Book-now button to body size.
   ⚠️ Shell-prefixed for SPECIFICITY: buttons are usually <a>, and `.pt-home-shell a{font-size:inherit}`
   is (0,1,1), so a bare `.pts-btn{font-size}` (0,1,0) loses to it and the button renders at body size. */
.pt-home-shell .pts-btn{min-height:42px;padding:0 17px;border:0;border-radius:999px;display:inline-flex;
  align-items:center;justify-content:center;gap:8px;font-size:16px;font-weight:800;white-space:nowrap;
  transition:transform .18s ease,background .18s ease,box-shadow .18s ease,border-color .18s ease}
.pts-btn:hover{transform:translateY(-1px)}
/* .pt-home-shell a{color:inherit} is (0,1,1) and outranks a bare class, so anything colouring
   an <a> must carry the shell prefix. */
/* The Book-now button IS the booking page theme — fill and text both. ⚠️ The text colour must be
   --pts-accent-on and not #fff: yellow (#eeba0b) carries white at 1.8:1, an unreadable button, which
   is exactly why the classic .yellow-summary-color pairs it with dark ink instead. And the hover fill
   is --pts-accent-hover (a darkened accent), NOT --pts-accent-ink: ink is tuned for readable TEXT, so
   on a light theme it is dark enough that the label would have to flip colour mid-hover. */
.pt-home-shell .pts-btn-primary{background:var(--pts-accent);color:var(--pts-accent-on);
  box-shadow:0 8px 22px rgba(var(--pts-accent-rgb),.24)}
.pt-home-shell .pts-btn-primary:hover{background:var(--pts-accent-hover);color:var(--pts-accent-on)}
.pt-home-shell .pts-btn-secondary{background:var(--pts-surface);color:var(--pts-text);border:1px solid var(--pts-line)}
/* Same (0,2,0) shape as .pt-home-shell .pts-btn above and later in the file, so it wins for the small
   header pill; a bare .pts-btn-small would lose to both that and the <a> reset. */
.pt-home-shell .pts-btn-small{min-height:36px;padding:0 14px;font-size:13px}
.pt-home-shell :focus-visible{outline:3px solid rgba(var(--pts-accent-rgb),.45);outline-offset:3px}

/* ---------- inline fontello icons ---------- */
/* Two glyphs that ride INSIDE a text run rather than in a box of their own: the CTA arrow
   (`Book now →`, on hosts from 12px to 16px) and the dropdown caret. Sized in `em` so one rule follows
   every host's own font-size — .pts-btn 16, .pts-link 14, .pts-tile-cta/.pts-row-cta/.pts-team-link 12 —
   instead of a per-host override that would drift. They were `&rarr;` / `&#9662;` entities, i.e. at the
   mercy of whichever system font supplied them; `line-height:1` + the `:before` margin reset keep the
   glyph on the label's centre (the button/CTA rows are flex, so its own box is what gets centred). */
/* ⚠️ `align-self:center` is load-bearing: `.pts-team-link` is an inline-flex host with no align-items,
   so the default `stretch` put the arrow's short (line-height:1) box at the TOP of the label and it
   rendered superscripted. Inert in the block/inline hosts, where vertical-align does the centring. */
.pts-arrow,.pts-caret{flex:0 0 auto;align-self:center;line-height:1;font-size:.8em}
/* ⚠️ In an INLINE host — `.pts-link` ("View all"), `.pts-tile-cta`, `.pts-row-cta` — NOTHING centres the
   icon: `align-self` above only does anything in a flex host, so the icon box is baseline-aligned, and
   this glyph's ink is not centred in its own em box (it rises .77em above the baseline and dips .06em
   below it). The arrow therefore hung **2.20px below the label's cap-height centre**, dipping past the
   baseline the text sits on — which is what reads as a sagging arrow beside "View all".
   The offset raises the icon box so the arrow's SHAFT (what the eye tracks, not the ink box) sits on that
   centre: measured +2.20px before → 0.00px after at a 15px label, ±0.10px at 12px. It is in `em` of the
   icon, so one value holds at every size.
   It is on `.pts-arrow` ALONE, deliberately: .18em is derived from this glyph's ink, not the chevron's,
   and every flex host (`.pts-btn`, `.pts-team-link`, and the caret's `.pts-lang`/`.pts-user-btn`) ignores
   `vertical-align` outright — so they keep the box-centring they already had. */
.pts-arrow{vertical-align:.18em}
.pts-arrow:before,.pts-caret:before{margin:0;width:auto;line-height:1;vertical-align:middle}
.pts-caret{font-size:.72em;opacity:.75}

/* ---------- header ----------
   ⚠️ EVERY backdrop-filter on this page is stated TWICE, -webkit- first. Safari only dropped the prefix
   in 18 (Sept 2024) — 9 through 17 need `-webkit-backdrop-filter`, so the unprefixed line alone means no
   glass on years of shipped iOS/macOS Safari. It degrades cleanly rather than breaking (the bar keeps the
   95%-opaque `background` above, which is exactly why that background must stay a real rgba() colour and
   not lean on the blur to be legible) — but the same pairing is already the house pattern in
   css/app-v2/login-page-v2.css, so matching it costs one line. `BookingHomeDesignsBackdropFilterTest`
   fails on any unpaired declaration, INCLUDING a `none` reset (see the directory design's header). */
.pts-header{position:sticky;top:0;z-index:70;height:var(--pts-header-h);
  background:rgba(var(--pts-surface-rgb),.95);
  border-bottom:1px solid rgba(var(--pts-line-rgb),.88);
  -webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}
.pts-header-inner{height:100%;width:min(var(--pts-page),calc(100% - 36px));margin:auto;
  display:flex;align-items:center;justify-content:space-between;gap:20px}
.pts-brand{display:flex;align-items:center;gap:var(--pts-brand-gap);font-weight:900;
  min-width:0;max-width:var(--pts-brand-max)}
.pts-brand-logo{width:34px;height:34px;border-radius:10px;object-fit:cover;flex:0 0 auto}
/* width is pinned because newbookingpage.css sets a bare img{width:100%;height:auto}, which would
   otherwise stretch the mark across the header. NOTE: images/picktime-logo-nav.svg is NOT usable
   here — its fills are white/#E5E6E6 for the dark left sidenav, so it is invisible on this light
   header. This colour mark reads on both themes, and the wordmark beside it is text in
   currentColor so it follows the theme too. */
.pts-brand-picktime{width:34px;height:34px;border-radius:6px;object-fit:contain;flex:0 0 auto}
.pts-brand-mark{width:34px;height:34px;border-radius:10px;background:var(--pts-accent);color:var(--pts-accent-on);
  display:flex;align-items:center;justify-content:center;font-weight:900;flex:0 0 auto}
/* Pinned for the same reason as .pts-btn — it also used to inherit the leaked bare `a{16px}`. */
.pts-brand-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:16px}
/* The header's brand: the classic booking page's CREDIT BADGE (.branding / .powerwedby / .powerwedbyPT —
   "Powered by" over "Picktime") instead of the mark + wordmark lockup, minus the classic's 318deg
   corner-ribbon rotation, which only makes sense pinned to a banner's corner. ALL SEVEN designs and both
   appearances — the badge says what the lockup only implies, and a per-design split would show a visitor
   comparing two Picktime booking pages two different product credits. Emitted where the view says so
   (m.brandBadge = branding && badgesBrand()), so a white-labeled account still gets its own logo + name.
   Nothing here is per-design: one set of rules, and the plate follows each design's own accent token.
   Two things are load-bearing:
   (a) the plate WEARS THE MERCHANT'S BOOKING PAGE THEME — --pts-accent fill with --pts-accent-on ink,
       exactly like .pts-btn-primary, NOT the classic's literal #1e1e1e on #fff and not the near-black
       --pts-text this shipped with. Three reasons: the classic badge sits on a photograph while this one
       sits on the header's own ground; the header is the one place a second dark plate competes with the
       Book-now CTA beside it; and --pts-accent-on is the token that keeps the label readable on every
       swatch (white on the yellow theme is not — that pairing is the whole point of the token). It
       follows the appearance for free too: the accent is theme-independent and the header behind it is
       the surface in both. ⚠️ Never state a literal here, and never use --pts-accent as the TEXT
       colour (see --pts-accent-ink) — here it is a fill and the ink comes from -on.
   (b) it is sized against the 34px lockup it replaces — 5px padding + a 12px line + an 18px line ≈ 40px
       in a 66px header, i.e. still 13px of air top and bottom, and ~30px NARROWER than the lockup, which
       is the axis that matters: the header row's 320px fit was measured value by value and the brand is
       its ONE shrinkable item. Retune the type and the padding together. The classic sets its two lines
       at 7px/14px; 7px is below this page's 11px content floor (the badge is real text, not a glyph
       box), so the caption sits on the 11px micro tier and the padding pays the difference. The 2px
       row-gap is the badge's internal rhythm — at 0 the caption sat on the wordmark's ascenders. */
.pts-brand-badge{display:flex;flex-direction:column;align-items:center;justify-content:center;
  flex:0 0 auto;gap:3px;padding:7px 12px;border-radius:6px;background:var(--pts-accent);
  color:var(--pts-accent-on);letter-spacing:0;text-align:center}
.pts-brand-badge-top{font-size:11px;font-weight:600;line-height:1.1;
  color:var(--pts-badge-sub)}
.pts-brand-badge-name{font-size:15px;font-weight:800;line-height:1.15}
.pts-header-nav{--pts-header-pill-h:38px;
  display:flex;flex-wrap:wrap;align-content:flex-start;gap:4px;
  height:var(--pts-header-pill-h);min-width:0;overflow:hidden}
.pt-home-shell .pts-header-nav a{font-size:14px;color:var(--pts-muted);padding:0 11px;border-radius:999px;
  height:var(--pts-header-pill-h);display:inline-flex;align-items:center;white-space:nowrap}
.pts-header-nav a:hover{background:var(--pts-surface-2);color:var(--pts-text)}
.pts-header-actions{display:flex;align-items:center;gap:9px}
/* Shell-prefixed so the font-size actually applies: `.pt-home-shell button{font:inherit}` above is
   (0,1,1) and the `font` shorthand resets font-size, so a bare `.pts-login{font-size:…}` was dead and
   this button silently rendered at the inherited body size. 14px matches the header nav beside it. */
/* Language chip — same ghost-pill treatment as .pts-login beside it. Shell-prefixed for the same reason:
   `.pt-home-shell button{font:inherit}` is (0,1,1) and its `font` shorthand would reset a bare font-size.
   The label can be long ("English (US)", "French (Français)"), so it is capped and ellipsised rather than
   allowed to squeeze the rest of the header. */
.pt-home-shell .pts-lang{min-height:36px;padding:0 11px;border:none;border-radius:999px;
  background:var(--pts-surface);color:var(--pts-muted);font-size:13px;font-weight:700;
  display:inline-flex;align-items:center;gap:6px;max-width:170px;white-space:nowrap}
.pt-home-shell .pts-lang:hover{background:var(--pts-surface-2);color:var(--pts-text)}
.pts-lang-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* `white-space:nowrap` for the same reason `.pts-btn` carries it: this label is two words in English and
   longer in most locales ("Se connecter", "Anmelden"), and as a flex item of the actions row it would
   break across two lines on a phone and grow a 36px control inside a 66px header rather than staying one
   pill. It is the ONLY signed-out control in that row (see the ≤720 note), so it must not deform. */
.pt-home-shell .pts-login{min-height:36px;padding:0 13px;border:1px solid var(--pts-line);border-radius:999px;
  background:var(--pts-surface);color:var(--pts-text);font-size:14px;font-weight:800;white-space:nowrap}
.pts-login:hover{background:var(--pts-surface-2)}
.pts-user{position:relative}
.pts-user-btn{min-height:38px;display:flex;align-items:center;gap:8px;padding:4px 8px 4px 5px;
  border:1px solid var(--pts-line);border-radius:999px;background:var(--pts-surface);color:var(--pts-text)}
/* Same fill as .pts-lang and .pts-login, its two siblings in this row — and it is the ONE of the three
   that shipped without a hover, which reads as a chip that is not a control (it opens the user menu).
   The signed-in and signed-out headers occupy the same slot, so a visitor must not get feedback in one
   state and none in the other. */
.pts-user-btn:hover{background:var(--pts-surface-2)}
.pts-avatar{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  background:var(--pts-accent-soft);color:var(--pts-accent-ink);font-size:10px;font-weight:900;flex:0 0 auto}
/* 130px, not 110: at the 13px content scale a two-word name ("Yashwanth Reddy") no longer fit 110px and
   ellipsised even on a desktop header that had room to spare. Phones cap it lower — see the ≤470 block. */
.pts-user-name{font-size:13px;font-weight:800;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pts-user-menu{position:absolute;right:0;top:calc(100% + 10px);width:220px;padding:8px;border:1px solid var(--pts-line);
  border-radius:14px;background:var(--pts-surface);box-shadow:0 18px 52px rgba(0,0,0,.18);
  opacity:0;transform:translateY(-5px) scale(.98);pointer-events:none;transform-origin:top right;transition:.18s ease}
.pts-user.pts-open .pts-user-menu{opacity:1;transform:none;pointer-events:auto}
.pts-user-menu-head{padding:9px 10px 11px;border-bottom:1px solid var(--pts-line);margin-bottom:5px}
.pts-user-menu-head strong{display:block;font-size:13px}
.pts-user-menu-head span{display:block;color:var(--pts-muted);font-size:11px;margin-top:1px;word-break:break-all}
.pts-user-menu a{display:block;padding:9px 10px;border-radius:9px;font-size:12px}
.pts-user-menu a:hover{background:var(--pts-surface-2)}

/* ---------- cover / hero ----------
   THE WHOLE BANNER IS ALWAYS VISIBLE, AT EVERY WIDTH. The box takes its height from the image's
   own ratio (`aspect-ratio`, so height = coverWidth / --pts-cover-ar) and the image is
   `contain`-fitted, so narrowing the viewport scales the banner down instead of cropping its
   left/right edges. It used to be a fixed per-design height (--pts-cover-h) + `object-fit:cover`,
   which cropped the sides at every width whose ratio was wider than the image's (~37% lost off a
   1000x400 banner on a 1440px screen, and progressively more as the window narrowed).
   Consequences to keep in mind when touching this:
   · The per-design cover heights are GONE — at a given width every design is the same height,
     because any other height would have to crop or letterbox. Tune the shared look via
     --pts-cover-ar / --pts-cover-max, not by reintroducing a fixed height.
   · The image is absolutely positioned so the aspect-ratio box, not the image, owns the height
     (an in-flow child would fight it before the image has loaded).
   · The box is capped on BOTH axes: --pts-cover-max (height) and --pts-cover-w (width). The width cap
     is what stops the full-bleed designs from painting a 700px-tall band across a 2560px display while
     the rest of the page stays inside the 1120px page measure; it is a max-width, never a width, for
     exactly the reason `soft` states below — a definite width blocks the ratio's height-to-width
     transfer and letterboxes the banner on a short window instead of scaling it down. The three
     designs that frame the banner themselves (soft, split, splitwide) declare their own max-width at
     (0,3,0) and out-rank this one, so the cap reaches only the four full-bleed block designs.
   · margin-inline:auto is load-bearing on a SHORT window. When --pts-cover-max binds, the ratio
     transfers that limit to the width too (CSS Sizing 4), so the box narrows instead of growing
     taller — height-limited rather than width-limited — and the auto margins keep that narrower
     box centred instead of parked against the left page edge.
     In the two split designs the same auto margins do the centring on their own flex line, where the
     box is `flex:0 0 100%` capped by a max-width — so those blocks re-declare the margin as a
     shorthand (top offset + auto) rather than dropping it. (Under the previous GRID shells this was a
     trap: an auto inline margin cancels a grid item's stretch sizing, and with no in-flow content —
     the image is absolute — the cover collapsed to 0x0 and the banner vanished. Flex has no such
     interaction, but keep the shorthand: those designs still need their own top offset.) */
.pts-cover{position:relative;height:auto;aspect-ratio:var(--pts-cover-ar);
  max-height:var(--pts-cover-max);max-width:var(--pts-cover-w);margin-inline:auto;
  overflow:hidden;background:#15151a;
  transition:border-radius .24s ease,width .24s ease,margin .24s ease}
.pts-cover img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:contain;object-position:center;display:block}
/* THE ONE EXCEPTION to "never crop the banner", and it is not the same picture: this is the
   per-industry stock photo the shell falls back to when the account has NO banner of its own
   (/bookingPage/banner/<Industry>.jpg — see the th:with in bookingHomeDesigns.html). Nothing on it
   belongs to the business, so there is nothing to preserve, and its shipped 2.5:1 source is far
   squarer than the 4:1 the banner cropper locks merchant uploads to — full-bleed at 2.5:1 that was a
   ~585px band on an ordinary desktop, so an account that never uploaded a banner got a hero roughly
   twice the height of one that did. Cropping it to the same measure is what makes the two cases
   agree. Keep `contain` + the source ratio for a real upload: that image IS the merchant's art.
   bindCoverRatio() bails out for this class, or the published source ratio would override it. */
.pts-cover.pts-cover-default{aspect-ratio:var(--pts-cover-ar-default)}
.pts-cover.pts-cover-default img{object-fit:cover}
.pts-cover::after{content:"";position:absolute;inset:0;
  background:linear-gradient(90deg,rgba(7,7,10,.4),rgba(7,7,10,.05) 58%,rgba(7,7,10,.25))}
/* hidden in clean/directory/split, shown by showcase/spotlight */
.pts-hero-msg{display:none;position:absolute;z-index:2;top:50%;transform:translateY(-50%);
  left:max(18px,calc((100% - var(--pts-page))/2));max-width:460px;color:#fff}
.pts-hero-eyebrow{font-size:12px;font-weight:900;letter-spacing:.12em;text-transform:uppercase;color:#d8c5ff}
.pts-hero-title{font-size:46px;line-height:1.02;margin:10px 0 16px;color:#fff;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.pts-hero-cta{margin-top:4px}

/* ---------- identity card + quick facts ---------- */
.pts-rail{position:relative;z-index:5;margin-top:var(--pts-overlap)}
.pts-card{background:var(--pts-surface);border:1px solid var(--pts-line);border-radius:var(--pts-radius);
  box-shadow:var(--pts-shadow);padding:22px;display:flex;align-items:center;gap:18px}
/* The middle child is the identity text block. Selected by exclusion so the template needs no extra class:
   the mark can be an <img> OR a <div>, and the actions column carries its own. `min-width:0` is the flex
   equivalent of the old `minmax(0,1fr)` track — without it a long business name refuses to shrink. */
.pts-card > *{flex:0 0 auto}
.pts-card > div:not(.pts-logo):not(.pts-rail-actions){flex:1 1 auto;min-width:0}
/* Sized against the card's CONTENT, not in isolation: the identity card holds a 30px title + the meta row
   + the Share pill, so an 82px mark read undersized beside it. 104px matches that stack.
   The radius tracks the size at a constant ~0.29 (24/82 -> 30/104) — that ratio is the mark's squircle
   look; scaling the box without it makes a big mark read as a plain rounded square. Every per-design and
   responsive override below keeps its own ratio for the same reason. */
/* ⚠️ The initial plate WEARS THE MERCHANT'S BOOKING PAGE THEME — --pts-accent fill with --pts-accent-on ink,
   the same pairing as .pts-brand-mark and .pts-btn-primary. It shipped as a literal near-black (#1b1b20 on
   #fff), i.e. the DEFAULT theme's accent baked in, so a teal or violet account got a black square beside its
   themed buttons — the same class of bug as the team avatar's lilac gradient stop. --pts-accent stays a FILL here and never the text colour, and -on is what keeps the initial
   readable on the yellow theme (white on it is 1.8:1). */
.pts-logo{width:104px;height:104px;border-radius:12px;background:var(--pts-accent);color:var(--pts-accent-on);
  display:flex;align-items:center;justify-content:center;
  font-size:27px;font-weight:900;object-fit:cover;overflow:hidden}
/* 🔑 THE BUSINESS NAME IS A HEADING THAT WRAPS, so it is sized and led for TWO lines, not one. It shipped
   at 32px/1.05 — display-poster values, right for the one-line name it was drawn with and wrong for the
   real field: merchants write long names ("Fitness Gym Fitness Gym Fitness Gym … Fi"), which wrap to two
   or three lines at the card's ~950px measure. Two things went wrong there and BOTH are measured, not
   taste: at 32px the pair of lines stood 67px tall in a 165px card and outweighed everything the card
   holds (a 28px section heading is the loudest type anywhere else on the page); and 1.05 leading is
   TIGHTER THAN THE INK — 33.6px of leading against 30.5px of ink for "Fitness Gym Fi" left a **3.1px**
   gap, so the descenders of one line all but touched the caps of the next and the block read as a slab.
   30px/1.15 keeps a real step above the 28px h2 (weight + the negative tracking widen it further) and
   opens that gap to **5.9px**, i.e. ~0.20em — the same order as the h2's own 1.12. Keep the leading on
   this side of ~1.12: it is the only thing standing between a two-line name and overlapping ink, and
   every per-design (directory/split 27px) and responsive (22/19px) override inherits it. */
.pts-title{font-size:30px;line-height:1.15;letter-spacing: 0.2px}
/* Industry · sub-industry under the business name (the classic's muted 12px <li> row, in this page's own
   token language at the secondary content size). The separator is a ::before dot on every span after the
   first, mirroring the classic's list-style:disc on its second item — so one value alone shows no stray
   punctuation. */
.pts-industry{display:flex;flex-wrap:wrap;align-items:center;gap:9px;margin:5px 0 0 1px;
  font-size:13px;font-weight:500;color:var(--pts-muted)}
.pts-industry span + span::before{content:"\2022";margin-right:9px;opacity:.6}
.pts-meta{display:flex;flex-wrap:wrap;gap:7px 13px;margin:5px 0px 10px;color:var(--pts-muted);font-size:13px}
.pts-rating{color:var(--pts-warning);font-weight:800}
/* ⚠️ The chip is an <a> when the reviews section exists, and the shell's `a{color:inherit}` reset is
   (0,1,1) — it out-ranks the bare `.pts-rating` (0,1,0) and would strip the chip's colour. Hence the
   element-qualified selector. No hover decoration: the pointer cursor is the whole affordance, and an
   underline on a coloured chip inside the meta row read as noise. */
.pt-home-shell a.pts-rating{color:var(--pts-warning);cursor:pointer}
/* ⚠️ Scoped to .pts-meta, exactly like the dot below, and for the same reason: `.pts-open` is ALSO the
   user menu's open-state class (`.pts-user.pts-open` — see the toggle in bindInteractions). Unscoped,
   opening that menu painted its whole contents — the customer's name, their email and all three links —
   success GREEN at weight 760, because they simply inherit from the container the class lands on. The
   dot rule below was already scoped for this collision; the colour itself was missed. */
.pts-meta .pts-open{color:var(--pts-success);font-weight:800}
.pts-meta .pts-closed{color:var(--pts-danger);font-weight:800}
/* Status dot. MUST stay scoped to .pts-meta — `.pts-open` is also the user-menu's open-state class
   (.pts-user.pts-open), which must not sprout a bullet. */
.pts-meta .pts-open::before,.pts-meta .pts-closed::before{content:"";display:inline-block;
  width:6px;height:6px;margin-right:5px;border-radius:50%;background:currentColor;vertical-align:middle}
/* Share cluster — a ghost pill in the identity card that opens a popover of share targets, mirroring the
   classic mini-website's share tooltip. Styled off the user menu (same surface/radius/shadow) so the page
   has one popover language. `.pts-share-open` deliberately does NOT reuse `.pts-open`: that class is the
   identity card's "Open now" status AND the user menu's state hook, so it would tint this menu green. */
/* Social icons then the Share pill, on one line.
   🔑 ONE WRAP CONTEXT: the icons are DIRECT children of this row — do not wrap them in a `.pts-socials`
   box (the FOOTER keeps one; its parent is a column, so its icons need their own row). ⚠️ Flex breaks
   lines on an item's HYPOTHETICAL main size — its flex-basis, `auto` = max-content — BEFORE it shrinks
   anything, so a wrapper is ONE item whose hypothetical size is every icon on a single line (6x34 + 5x9
   = 249px). The pill was therefore pushed onto a line of its OWN the moment one icon would have wrapped
   (measured: a 234px card column on a phone), and only then did the wrapper shrink to the row and wrap
   INTERNALLY — leaving obvious free space beside the last icon that the pill could never move into,
   being a sibling of the wrapper rather than of the icons. Same trap as the split rail's content column
   and the banner's `flex:0 0 100%`, and the reason `.pts-socials` is footer-only.
   The gap is therefore ALSO the icon rhythm: it stays 9px, in step with `.pts-socials`, so the page's
   two icon rows read as one treatment. */
.pts-social-row{display:flex;flex-wrap:wrap;align-items:center;gap:9px;margin-top:0px}
.pts-share{position:relative}
.pt-home-shell .pts-share-btn{display:inline-flex;align-items:center;gap:7px;min-height:34px;padding:0 12px;
  border:1px solid var(--pts-line);border-radius:999px;background:var(--pts-surface);color:var(--pts-muted);
  font-size:12px;font-weight:800}
.pt-home-shell .pts-share-btn:hover{background:var(--pts-surface-2);color:var(--pts-text)}
/* NOTE: this z-index only orders the popover WITHIN .pts-rail, which is itself a stacking context
   (position:relative;z-index:5). The sticky .pts-nav sits at 60, so it paints over this entire subtree no
   matter how high a number goes here — see the .pts-share-lift rule further down, which raises the rail. */
/* ⚠️ The CLOSED popover must be `display:none`, NOT merely transparent. It used to hide with
   `opacity:0;pointer-events:none`, which leaves the box in layout: a 196px absolute panel anchored to the
   pill's left edge, so as soon as the pill sat far enough right — i.e. from the width where the social row
   fits on ONE line (~470px) up to ~630px, in ALL SEVEN designs — its right edge landed past the viewport
   and the whole PAGE gained a horizontal scrollbar (measured: document scrollWidth 616 at a 560px viewport)
   plus a card whose scroll box was 69px wider than the card. Invisible ink, real overflow. `visibility:hidden`
   would not have fixed it either — a hidden box still creates scrollable overflow; only taking it out of
   layout does. The fade therefore moves from a transition to an animation on the OPEN state (the reduced-motion
   block already clamps animation-duration), which is also why the transition is gone: display is not
   transitionable, so keeping one would only have delayed the paint.
   `max-width` is the belt to that braces: even with no JS at all (see clampShareMenu, which slides the open
   menu back inside the viewport) the panel can never be wider than the screen it opens on.
   NOTE: the z-index only orders the popover WITHIN .pts-rail, which is itself a stacking context
   (position:relative;z-index:5). The sticky .pts-nav sits at 60, so it paints over this entire subtree no
   matter how high a number goes here — see the .pts-share-lift rule further down, which raises the rail. */
.pts-share-menu{display:none;position:absolute;top:calc(100% + 8px);left:0;z-index:30;min-width:196px;
  max-width:calc(100vw - 16px);padding:6px;
  background:var(--pts-surface);border:1px solid var(--pts-line);border-radius:var(--pts-radius-sm);
  box-shadow:var(--pts-shadow)}
.pts-share.pts-share-open .pts-share-menu{display:block;animation:pts-share-pop .16s ease}
@keyframes pts-share-pop{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
.pts-share-head{margin:0 0 4px;padding:7px 9px;border-bottom:1px solid var(--pts-line);
  color:var(--pts-muted);font-size:12px}
.pts-share-menu a,.pt-home-shell .pts-share-copy{display:flex;align-items:center;gap:9px;width:100%;
  padding:8px 9px;border:0;border-radius:9px;background:none;color:var(--pts-text);font-size:13px;
  text-align:left;cursor:pointer}
.pts-share-menu a:hover,.pt-home-shell .pts-share-copy:hover{background:var(--pts-surface-2)}
.pt-home-shell .pts-share-copy.pts-share-copied{color:var(--pts-success)}
/* FOOTER ONLY. The identity card's icons are direct children of `.pts-social-row` (see the ⚠️ above);
   this wrapper survives because the footer brand block is a flex COLUMN, where the icons need a row box
   of their own — and there is nothing beside them there to be pushed onto its own line. */
.pts-socials{display:flex;flex-wrap:wrap;gap:9px}
.pt-home-shell /* Icon buttons. `line-height:1` plus flex centring keeps every glyph on the button's centre — fontello
   faces differ in ascent, so leaving the inherited line-height in place drops some of them a pixel or two
   (the classic patches its YouTube glyph with a hand-tuned `margin-top:4px` for exactly this reason). */
.pts-social{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;
  border-radius:10px;border:1px solid var(--pts-line);background:var(--pts-surface);color:var(--pts-text);
  font-size:15px;line-height:1;transition:border-color .16s ease,color .16s ease}
.pts-social i{display:block;line-height:1;width:auto;margin:0}
.pt-home-shell .pts-social:hover{border-color:var(--pts-accent);color:var(--pts-accent-ink)}
.pt-home-shell .pts-social:hover{color:var(--pts-accent-ink);background:var(--pts-accent-soft);
  border-color:var(--pts-edge-35)}
.pts-rail-actions{display:flex;flex-direction:column;align-items:flex-end;gap:10px}
.pts-note{font-size:12px;color:var(--pts-muted)}
.pts-facts{margin-top:14px;display:flex;flex-wrap:wrap;--pts-fact-basis:25%;background:var(--pts-surface);
  border:1px solid var(--pts-line);border-radius:var(--pts-radius);box-shadow:var(--pts-shadow);overflow:hidden}
.pts-fact{display:flex;align-items:center;gap:11px;padding:15px 17px;border-right:1px solid var(--pts-line);
  flex:0 1 var(--pts-fact-basis);min-width:0}
.pts-fact:last-child{border-right:0}
/* 🔑 THE ARRANGEMENT IS THE FACT COUNT'S — the same rule the gallery obeys through data-pts-gal, and for
   the same reason: never an unfilled cell. The strip is a bordered panel with dividers, so a row sized
   for four holding three ends in a visibly empty quarter, which reads as a failed render rather than a
   layout. That is exactly the failure the locations tile was added to prevent for one-location accounts
   (see buildLocationFact) — but the tile only gets an account to FOUR facts when it has all four to
   state. An account whose booking page lists no team members has three, and measured before this it left
   a 280px void at ≥1141px and a 432px void on the second line at 921-1140px.

   The count comes from the template as data-pts-facts (m.facts.length), so a new fact needs no CSS.
   🔑 These are deliberately kept at (0,2,0) — the LOWEST specificity that still beats `.pts-facts`
   (0,1,0) — because two other rules have to out-rank them and would otherwise need matching inflation:
     · the split designs run this strip ONE PER LINE in their narrow rail, and their (0,3,0) rule below
       must keep winning (it is already void-free, so a count basis must not drag it into a multi-up row);
     · the responsive blocks re-state the basis, and a media query adds NO specificity of its own.
   An earlier revision wrote these as `[data-pts-design]:not([data-pts-design^="split"]) …` — (0,5,0),
   which silently beat BOTH, holding a 3-up row all the way down and rendering 116px cells at 375px. */
.pts-facts[data-pts-facts="1"]{--pts-fact-basis:100%}
.pts-facts[data-pts-facts="2"]{--pts-fact-basis:50%}
.pts-facts[data-pts-facts="3"]{--pts-fact-basis:33.3333%}
/* ⚠️ The fact glyph is a FONTELLO icon, not a unicode character — the four facts used to draw `≡ ✓ ♦ ◎`,
   which are four unrelated codepoints from whatever system font resolves them, so at one font-size they
   rendered at wildly different optical sizes (`≡` and `◷` looked half the size of `♦`/`◎`). One icon font
   = one set of metrics, the same fix `.pts-social` already uses on this page. The class comes from the
   view (`buildFacts`); `:before` must reset fontello's `width:1em` + `margin:0 .2em`, or the box's flex
   centring is thrown off by that side margin. No `font-weight` — the icon font has one weight. */
.pts-fact-icon{width:34px;height:34px;flex:0 0 auto;border-radius:8px;background:var(--pts-accent-soft);
  color:var(--pts-accent-ink);display:flex;align-items:center;justify-content:center;font-size:18px;
  line-height:1}
.pts-fact-icon:before{margin:0;width:auto;line-height:1}
.pts-fact-copy{min-width:0}
.pts-fact-copy strong{display:block;font-size:13px}
.pts-fact-copy span{display:block;margin-top:1px;color:var(--pts-muted);font-size:12px; font-weight: 300; letter-spacing: 0.2px;}
.pts-fact-copy strong,.pts-fact-copy span{display:-webkit-box;-webkit-line-clamp:2;
  -webkit-box-orient:vertical;overflow:hidden}

/* ---------- in-page nav ---------- */
/* Wrapper around the in-page nav + main. A plain block everywhere except the split designs, where it is
   the content COLUMN of the flex shell — see those blocks. */
.pts-col-main{display:block}
/* 🔑 THE HAIRLINE UNDER THE PILLS HAS TWO STATES, and which one shows says whether the bar is FLOATING
   or STUCK. At rest it is the ROW's rule — inset to the pill row's own measure, so it reads as the end
   of that row and lines up with the cards and section heads above it. Once the bar sticks under the
   header it becomes the BAR's rule, edge to edge: a stuck bar is chrome laid across the page, and a line
   that stops short of both edges makes it read as a floating panel with the page sliding past either
   side of it.
   ⚠️ The 1px is ALWAYS in the box (`border-bottom` stays, only its COLOR is transparent at rest) and the
   at-rest line is painted in the border's own place (`bottom:-1px`) — toggling the border in and out
   would change the bar's height by a pixel at the exact moment it sticks and nudge the whole page.
   ⚠️ The at-rest line is anchored to the BAR, not to .pts-nav-inner, even though it is the inner's width
   it draws: the split designs give the bar a fixed --pts-nav-h (56px) that the row's own content
   (58px) overflows, so an inner-anchored line sits 2px BELOW the bar's bottom edge and would jump 3px
   the moment the border took over. Its geometry comes from --pts-nav-line-l/-w, which bindNavScroll
   MEASURES off .pts-nav-inner and republishes on resize — a measurement, not a fourth restatement of
   `min(--pts-page, 100% - 36px)`: that expression is already stated in four places (base, the split
   block, the ≤920 split reset, the 24px-gutter block) and a copy here would drift out of step with
   whichever one a later edit touches. The fallbacks make the line the full bar until JS measures. */
.pts-nav{position:sticky;top:var(--pts-header-h);z-index:60;margin-top:18px;
  background:rgba(var(--pts-bg-rgb),.87);border-bottom:1px solid transparent;
  -webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}
.pts-nav::after{content:"";position:absolute;bottom:-1px;height:1px;pointer-events:none;
  left:var(--pts-nav-line-l,0px);width:var(--pts-nav-line-w,100%);background:var(--pts-line)}
.pts-nav.pts-stuck{border-bottom-color:var(--pts-line)}
.pts-nav.pts-stuck::after{display:none}
/* ⚠️ The pill row OVERFLOWS and its scrollbar is deliberately hidden, so it needs its own affordance.
   The row is `overflow-x:auto` with `scrollbar-width:none` (a visible bar under a sticky glass bar reads
   as a broken panel), which is fine while every pill fits — and it does at the 1120px page measure. The
   split designs give the column ~752px, and a business with all the sections (Services · Classes ·
   Resources · About · Team · Gallery · Reviews · Location · Working Hours) needs ~797px: the last pill
   was clipped with NO way to reach it — no bar, no arrows, no JS, and a vertical wheel scrolls the page.
   So the two ends carry arrow buttons, shown only when there is something that way (pts-can-left /
   pts-can-right, toggled by bindNavScroll in bookingHomeDesignsView.js).
   The width/centring lives on .pts-nav-inner rather than the scroller itself, so the arrows can anchor to
   the ROW's ends: the scroller is the overflow box and must stay flush with them — an arrow anchored to
   .pts-nav instead would sit out in the page gutter in the five non-split designs, whose row is centred
   inside a wider bar. Move the width and the arrows go with it. */
.pts-nav-inner{position:relative;width:min(var(--pts-page),calc(100% - 36px));margin:auto;display:flex}
.pts-nav-scroll{flex:1 1 auto;min-width:0;display:flex;gap:5px;
  overflow-x:auto;scrollbar-width:none;padding:9px 0;scroll-behavior:smooth;
  /* The fade is now per-side and OFF by default, driven by the same two classes as the arrows — the old
     unconditional right-edge fade washed out the final pill even when it was the last one and nothing
     lay beyond it. Both stops collapse to a hard edge at 0px, so an unfaded side is fully opaque. */
  --pts-nav-fade-l:0px;--pts-nav-fade-r:0px;
  -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 var(--pts-nav-fade-l),
    #000 calc(100% - var(--pts-nav-fade-r)),transparent 100%);
  mask-image:linear-gradient(90deg,transparent 0,#000 var(--pts-nav-fade-l),
    #000 calc(100% - var(--pts-nav-fade-r)),transparent 100%)}
.pts-nav-scroll::-webkit-scrollbar{display:none}
.pts-nav.pts-can-left .pts-nav-scroll{--pts-nav-fade-l:34px}
.pts-nav.pts-can-right .pts-nav-scroll{--pts-nav-fade-r:34px}
/* ⚠️ HIDDEN EVERYWHERE BY DEFAULT — the only rule that reveals them is in the >920px SPLIT block, and
   that narrowness is deliberate, not an oversight:
   · below 921px the row is swiped, and a touch scroll is its own affordance — arrows there would be
     chrome overlaying the pills for a gesture the visitor already has;
   · the five block designs get the full 1120px page measure, where the row does not overflow at all.
   Everywhere they are not shown the FADE still is (it rides the same two classes) — a soft edge reading
   "more this way" costs nothing and is exactly the hint a swipe needs.
   Pointer affordance only, hence aria-hidden + tabindex=-1: a keyboard user tabs the pills and the
   browser scrolls each into view on focus, so as tab stops these would be two dead stops per page.
   They overlay the scroller's ends, which is why they are absolute rather than flex items: as a flex
   item each would permanently steal 34px from the row, making the overflow they exist to solve worse.
   Each is drawn as a ROUND BUTTON on the accent tint — the same fill/ink/hairline as an active pill
   (.pts-nav-scroll a.pts-active) — so it reads as a control of this row rather than a glyph floating
   over the pills. It must stay a circle centred on the row (top:50% + translateY), NOT the old
   full-height strip: a stretched box with a fill would paint a tinted band down the whole bar. The fill
   is opaque by construction (--pts-accent-soft mixes the accent into the page ground, it is not a
   translucent wash), which is what lets a pill scroll cleanly behind it; the edge fade (widened to 64px
   wherever the arrows are actually shown — see the >920px split block) dissolves the pill well before it
   reaches the button, so the two read as a dissolve rather than a collision. The soft drop shadow is
   what says "this floats above the row": without it a tinted chip sitting flush against a pill label
   reads as two controls that have run into each other. */
.pts-nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:32px;height:32px;
  display:none;align-items:center;justify-content:center;padding:0;
  border:1px solid rgba(var(--pts-accent-rgb),.2);border-radius:999px;
  background:var(--pts-accent-soft);color:var(--pts-accent-ink);
  box-shadow:0 2px 6px rgba(24,24,27,.09);
  font-size:12px;line-height:1;cursor:pointer;z-index:2}
.pts-nav-arrow:hover{background:var(--pts-accent-fill);
  border-color:rgba(var(--pts-accent-rgb),.34);color:var(--pts-accent-ink)}
.pts-nav-arrow i:before{margin:0;width:auto;line-height:1}
.pts-nav-prev{left:0}
.pts-nav-next{right:0}
.pts-nav-scroll a{flex:0 0 auto;padding:9px 13px;border:1px solid transparent;border-radius:999px;
  color:var(--pts-muted);font-size:13px;font-weight:800}
.pts-nav-scroll a:hover,.pts-nav-scroll a.pts-active{color:var(--pts-accent-ink);background:var(--pts-accent-soft);
  border-color:rgba(var(--pts-accent-rgb),.2)}

/* ---------- sections ---------- */
.pts-main{padding:26px 0 80px}
.pts-section{margin-top:var(--pts-section);scroll-margin-top:148px}
/* The page's first section starts flush under main's own padding. This used to be the job of a
   `.pts-section-anchor` utility, which collapsed the empty #pts-overview announcement section that stood
   here; both went with the announcement bar (see the template) — #pts-offerings is main's first child now. */
.pts-section:first-child{margin-top:0}
.pts-section-head{display:flex;align-items:end;justify-content:space-between;gap:20px;margin-bottom:18px}
.pts-section-head h2{font-size:28px;line-height:1.12;margin:4px 0}
.pts-section-head p{margin:0;color:var(--pts-muted);font-size:15px; font-weight: 300; letter-spacing: 0.2;}
/* The head's action stays whole and stays on the right at every width — it is the shrink-proof half
   of the row (the text block beside it carries min-width:0 and wraps instead). */
.pts-section-head > .pts-link{flex:0 0 auto;white-space:nowrap}
.pt-home-shell .pts-link{color:var(--pts-accent-ink);font-size:14px;font-weight:800}
/* scroll-margin so an in-page pill lands the group below the sticky header, not under it. */
.pts-group{margin-top:30px;scroll-margin-top:148px}
.pts-group:first-child{margin-top:0}
/* Long-category preview: everything past the limit is in the DOM but hidden until that category's own
   button flips data-pts-collapsed (see applyPreviewLimit / bindInteractions). The attribute rides the
   LIST CONTAINER, not the group — the limit is per category, so each long category collapses on its own
   and a short one beside it is untouched. The container is the one box both layouts have in the
   categorised AND the flat unlabelled case, which is why the state lives there. Scoped to the collapsed
   state, so a list that never had overflow — or one already expanded — is untouched. */
/* ⚠️ Shell-prefixed (0,4,0) on purpose. A design that re-declares `display` on its cards —
   `[data-pts-design="directory"] .pts-tile{display:flex}` — matches at the same 0,3,0 as the unprefixed
   form and wins on source order, so `directory` kept rendering every item while still showing the button. */
/* ⚠️ `.pts-team` joins the same rule rather than getting one of its own: a long TEAM previews behind
   the identical control (see TEAM_PREVIEW_LIMIT / buildTeam), and a second hide rule is the kind of
   copy that drifts — `directory` re-declaring a card's `display` broke the catalogue's rule exactly
   once already, and a separate team rule would have had to be found and fixed a second time. */
.pt-home-shell .pts-tiles[data-pts-collapsed="true"] .pts-overflow,
.pt-home-shell .pts-rows[data-pts-collapsed="true"] .pts-overflow,
.pt-home-shell .pts-team[data-pts-collapsed="true"] .pts-overflow{display:none}
.pt-home-shell .pts-cat-more{display:block;margin:18px auto 0;padding:11px 22px;font-size:14px;
  font-weight:600;color:var(--pts-accent-ink);background:var(--pts-surface);border:1px solid var(--pts-line);
  border-radius:999px;cursor:pointer;transition:border-color .18s ease,box-shadow .18s ease}
.pt-home-shell .pts-cat-more:hover{border-color:var(--pts-accent);box-shadow:var(--pts-shadow)}
/* 🔑 THE HEAD'S BOTTOM MARGIN *IS* THE SPACE ABOVE THE FIRST CATEGORY, so it reads the SAME TOKEN that
   separates every later one. It was a literal 16px against `--pts-cat-gap`'s 24 (34 in directory), so a
   group's first category header sat visibly tighter than its siblings — measured 16px above TREATMENTS
   and 34px above ASSESSMENTS in one group — and the row of category "tabs" had no single rhythm to read.
   ⚠️ Re-point the TOKEN, never this value: stated as a literal the two drift apart again the moment a
   design tunes `--pts-cat-gap` (which `directory` does), and they must move together by construction.
   ⚠️ `.pts-cat:first-of-type{margin-top:0}` STAYS — the head supplies that space now, so the two are not
   additive, and the reset is still what a per-design `.pts-cat` margin would out-rank (see the token
   comment at the top of the file). This is also why the fix is NOT "delete the reset and let the margins
   collapse": collapsing takes the LARGER of the two, so it is only even while the token exceeds the
   head's own margin, and a design that tuned the token below it would silently get the old 16px back.
   The flat `.pts-subhead` branch (no `.pts-cat` involved) keeps its own rhythm — its 22px `margin-top`
   collapses with this one exactly as it did before. */
.pts-group-head{display:flex;justify-content:space-between;align-items:end;gap:18px;
  margin-bottom:var(--pts-cat-gap)}
.pts-group-head h3{font-size:22px}
.pts-group-head p{margin:3px 0 0;color:var(--pts-muted);font-size:14px; font-weight: 300; letter-spacing: 0.2px;}
.pts-count{border:1px solid var(--pts-line);border-radius:999px;background:var(--pts-surface);padding:7px 11px;
  font-size:12px;color:var(--pts-muted);font-weight:800;white-space:nowrap}
.pts-subhead{font-size:13px;font-weight:900;letter-spacing:.06em;text-transform:uppercase;
  color:var(--pts-muted);margin:22px 0 12px}

/* ---------- category-wise catalogue (emitted only where the view asks: categorisesGroups) ---------- */
/* The merchant's own categories, listed the way the classic mini-website's catalogue lists them: a colour
   dot, the name, its count, and a caret that opens/closes the items. A native <details>/<summary>, so the
   marker is suppressed the same way the FAQ rows do it (list-style + the -webkit pseudo-element) and the
   state costs no JS. Kept on the SAME type as .pts-subhead — 13px/850/uppercase/.06em — because it
   replaces that label and must read at the same rank, not as a new heading level.
   Deliberately unfilled: the classic uses a plain ruled row, and a filled bar above a row of bordered
   cards reads as a second card. The hairline + caret is the affordance, as on the FAQ rows. */
.pts-cat{margin-top:var(--pts-cat-gap)}
.pts-cat:first-of-type{margin-top:0}
.pts-cat-head{display:flex;align-items:center;gap:10px;padding:0 0 11px;margin-bottom:var(--pts-cat-lead);
  border-bottom:1px solid var(--pts-line);cursor:pointer;list-style:none}
.pts-cat-head::-webkit-details-marker{display:none}
/* The dot is the PAGE ACCENT — the merchant's booking page theme — not the category's own
   `categoryColor`. This page themes everything off one colour (Book now, the active pill, today's hours
   row), so per-category dots from the `bg-type-*` palette read as a second, unrelated colour system
   imported from the dashboard: a blue dot above black buttons on an unthemed account, and a clash with
   the accent on a themed one. Deliberate deviation from the classic catalogue's `.color-ball`, which
   carries the category colour. Because nothing else paints this element there is no `bg-type-*`
   specificity race, so the fill lives on the element's own rule. */
.pts-cat-ball{flex:0 0 auto;width:11px;height:11px;border-radius:50%;background:var(--pts-accent)}
/* min-width:0 so a long category name WRAPS inside its share of the row rather than pushing the count and
   the caret off the end — the name is the only item here that may be arbitrarily long, and the other two
   must never break (a count split across lines, or a caret on its own line, reads as a broken row). */
.pts-cat-name{min-width:0;font-size:13px;font-weight:900;letter-spacing:.06em;text-transform:uppercase;
  color:var(--pts-text)}
.pts-cat-count{flex:0 0 auto;font-size:12px;font-weight:800;color:var(--pts-muted);white-space:nowrap}
.pts-cat-count::before{content:"·";margin-right:6px;opacity:.6}
/* Fontello caret (never a character — see the icons note). The box states its own font-size and resets the
   font's 1em width + side margins so the glyph sits on the flex centre.
   `margin-left:auto` right-aligns it — the standard accordion affordance, at the far end of the row it
   toggles. The `margin-right` is the design's inset: flush against the end of the hairline the glyph read
   as a rule terminating the section rather than as a control, so it is pulled 20px inboard. Keep BOTH —
   dropping the auto margin packs the caret against the count, dropping the inset restores the flush edge.
   ⚠️ It does NOT flip on the open state: the chevron points DOWN in both states, by design. So there is no
   `.pts-cat[open]` transform and no transform transition to animate one — re-adding either brings the
   up-arrow back. What the caret marks here is "this row expands", not which way it currently is. */
.pts-cat-caret{flex:0 0 auto;margin-left:auto;margin-right:20px;font-size:13px;line-height:1;
  color:var(--pts-muted)}
.pts-cat-caret::before{margin:0;width:auto}

/* ---------- offering tiles / rows (column counts come from the design tokens) ---------- */
/* N-up lists are flex-wrap + a calculated basis. The basis lives in a custom property so per-design and
   responsive overrides can restore it by name (e.g. showcase's lead tile goes 100% then back). `0 1 basis`
   — never `1 1` — keeps a short last row at column width instead of stretching it, matching the old grid. */
.pts-tiles{display:flex;flex-wrap:wrap;gap:14px;
  --pts-tile-basis:calc((100% - (var(--pts-tiles) - 1) * 14px) / var(--pts-tiles))}
.pts-tiles > .pts-tile{flex:0 1 var(--pts-tile-basis);min-width:0}
.pts-rows{display:flex;flex-wrap:wrap;gap:14px;
  --pts-row-basis:calc((100% - (var(--pts-rows) - 1) * 14px) / var(--pts-rows))}
.pts-rows > .pts-row{flex:0 1 var(--pts-row-basis);min-width:0}
.pts-tile{position:relative;display:flex;flex-direction:column;background:var(--pts-surface);
  border:1px solid var(--pts-line);border-radius:var(--pts-radius-sm);padding:18px;
  --pts-tile-h:218px;height:var(--pts-tile-h);
  transition:box-shadow .2s ease,border-color .2s ease}
/* Same hover as .pts-row (the classes layout): accent border + shadow, and NO transform. Tiles used to
   translateY(-3px), which reads as the card jumping — worse in a tiled row, where lifting one tile leaves a
   visible step against its neighbours, and worse still when the pointer lands near a tile's edge and the
   lift moves the card out from under it (hover flickers on/off). Border + shadow alone give the same
   affordance without moving anything. Keep the two layouts' hover identical. */
.pts-tile:hover{box-shadow:var(--pts-shadow);
  border-color:var(--pts-edge-30)}
/* The initials badge: ONE type size across services, classes and resources. The tile icon used to carry
   no font-size at all — it inherited the shell's 15px at weight 900 while the class row's badge was
   12px/850, so the same initials rendered visibly larger on a service card than on a class row. The BOXES
   still differ (40px in a tile, 58px in a row) because the layouts do; only the type is shared. */
.pts-tile-icon,
.pts-row-badge{font-size:14px;font-weight:900}
.pts-tile-icon{width:40px;height:40px;border-radius:12px;background:var(--pts-accent-soft);color:var(--pts-accent-ink);
  display:flex;align-items:center;justify-content:center;margin-bottom:14px;overflow:hidden;flex:0 0 auto}
.pts-tile-icon img{width:100%;height:100%;object-fit:cover}
.pts-tile h4{font-size:16px;margin-bottom:5px}
/* An offering's NAME is ONE LINE, ellipsised — services, classes and resources alike (the row's own h4
   picks this up below). A merchant who types a sentence into the name field ("Boxing Fitness - improve
   their overall fitness, and work toward their personal health and wellness goals.") got a three-line
   heading that dwarfed its own description and stretched every card sharing its flex line to match.
   ⚠️ Both hosts already carry `min-width:0` (`.pts-tiles > .pts-tile`, and the row's text column) — without
   it a nowrap heading sets the flex item's min-content width and blows the card out of its column. */
.pts-tile h4,
.pts-row h4{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* 🔑 The full name is a `title` on the NAME, so the tooltip appears over the name and NOWHERE ELSE. That is
   a two-part problem, and this pair is the whole answer:
   · the heading is UNDER `.pts-tile-link` (an absolute overlay across the entire card), so the pointer
     never reaches it — hence `position:relative;z-index:2` to lift it above the overlay. Putting the title
     on the overlay instead does fire, but from anywhere in the card: over the price, the meta, the empty
     ground. That is what this replaced.
   · ⚠️ but the heading's LINE BOX is the full width of the card, so lifting the whole h4 would also steal
     the click from every empty pixel beside a short name — a dead spot on a card that is supposed to be one
     big link. So the h4 is lifted with `pointer-events:none` and only the inline `.pts-name` takes
     `pointer-events:auto`: the glyphs hover and click, the blank remainder of the line passes straight
     through to the overlay.
   `.pts-name` is an <a> to the same URL for exactly that reason — raising it takes the click off the
   overlay, and the name is the most natural thing on the card to click (it also gets middle-click /
   open-in-new-tab for free).
   ⚠️ …and being an <a> is precisely why it must state `font:inherit`. `newbookingpage.css` styles BARE
   TAGS and leaks into this page — `a{font-weight:400;font-size:16px;color:#1e1e1e}` — and the shell's
   `.pt-home-shell a` reset neutralises the COLOUR and the SIZE but not the WEIGHT, so wrapping the
   heading's text in an anchor silently dropped every service, class and resource name from 700 to 400
   (the whole catalogue stopped being bold). The shorthand covers family/size/weight/style/line-height in
   one declaration — the same fix `.pt-home-shell button{font:inherit}` already makes for buttons — and
   `letter-spacing` is not part of it, so the tile's -.02em keeps inheriting from the heading. */
.pts-tile h4,
.pts-row h4{position:relative;z-index:2;pointer-events:none}
.pts-name{font:inherit;pointer-events:auto}
.pts-tile p{font-size:13px;color:var(--pts-muted);margin:0 0 14px}
/* Two-line fold on an offering's description, same doctrine as the review comment's: the clamp is CSS
   (so it is exact at any width, whatever the tile's column count) and markClampedTiles() adds the button
   only to the descriptions that ACTUALLY clip — a character count cannot decide that. Depth is a token
   because it is a per-design choice: spotlight's taller tile has always previewed three lines, and it
   used to clamp them with no way at all to read the rest.
   ⚠️ The button MUST be painted above `.pts-tile-link` — the whole tile is a link, laid over the card as
   an absolute overlay, so a fold button below it is not clickable at all: every Read More would start a
   booking instead of unfolding the text. `.pts-tile-cta` sits at z-index 1 for the mirror-image reason
   (it must NOT swallow the click), so this one goes above both. */
.pts-tile{--pts-desc-lines:1}
.pts-tile-desc.pts-clamp{display:-webkit-box;-webkit-line-clamp:var(--pts-desc-lines);
  -webkit-box-orient:vertical;overflow:hidden}
/* The paragraph's own bottom margin is the air before the tile's meta row; left in place with a button
   after it, it reads as a control detached from the text it folds — so the fold moves that spacing onto
   the button (markClampedTiles marks the paragraph `pts-folded` for exactly as long as a button is
   attached). Beats `.pts-tile p` (0,1,1) on class count, so no element qualifier is needed. */
.pts-tile-desc.pts-folded{margin-bottom:0}
.pt-home-shell .pts-tile-more{position:relative;z-index:2;align-self:flex-start;display:inline-block;
  margin:5px 0 14px;padding:0;font-size:12px;font-weight:400;color:var(--pts-accent-ink);
  background:none;border:0;cursor:pointer}
.pts-tile-meta{display:flex;flex-wrap:wrap;gap:7px 12px;color:var(--pts-muted);font-size:12px;
  margin-top:auto;padding-top:11px;border-top:1px solid var(--pts-line)}
.pts-price{font-size:14px;font-weight:900;color:var(--pts-text);margin-left:auto}
/* ⚠️ `margin-top:auto` is the BASE, 12px the special case — the two swap round when the meta footer is
   absent. .pts-tile-meta is what holds the tile's bottom edge (its own `margin-top:auto`), and it is not
   rendered at all when SHOW DURATION and SHOW PRICE are both off; without this the CTA rode up under the
   description and the "Book now" labels across a row stopped lining up. `directory` resets it to 0 at
   (0,3,0), which still out-ranks the adjacent-sibling rule. */
.pts-tile-cta{position:relative;z-index:1;color:var(--pts-accent-ink);font-size:12px;font-weight:800;margin-top:auto;
  pointer-events:none}
.pts-tile-meta + .pts-tile-cta{margin-top:12px}
.pts-tile-link{position:absolute;inset:0;border-radius:inherit}
.pts-row{position:relative;display:flex;gap:15px;align-items:center;
  background:var(--pts-surface);border:1px solid var(--pts-line);border-radius:var(--pts-radius-sm);padding:16px;
  transition:box-shadow .2s ease,border-color .2s ease}
.pts-row:hover{border-color:var(--pts-edge-30);box-shadow:var(--pts-shadow)}
.pts-row > *{flex:0 0 auto}
/* 🔑 The text column is `flex:1 1 0`, NOT `1 1 auto` — a ZERO basis is what keeps it BESIDE the badge.
   ⚠️ Flex breaks lines on an item's flex-basis, before it shrinks anything, and `auto` resolves to
   max-content — which for this column is the whole NAME on one line (`.pts-row h4` is `white-space:nowrap`)
   plus its description. The ≤470 block wraps the row on purpose (price + CTA drop onto their own indented
   lines), so with an `auto` basis a long name pushed the WHOLE text column below the badge while a short one
   stayed beside it: the layout changed shape from item to item, exactly the "misaligned" reading. A zero
   basis can never overflow the line, and since this is the only growable item its rendered width is
   unchanged at every width (measured identical 320-1240px). Same trap, same fix, as the split shell's
   content column and the identity card's social row. */
.pts-row > div:not(.pts-row-badge):not(.pts-row-price){flex:1 1 0;min-width:0}
.pts-row-badge{width:58px;height:58px;border-radius:14px;background:var(--pts-accent-soft);color:var(--pts-accent-ink);
  display:flex;align-items:center;justify-content:center;text-align:center;line-height:1;overflow:hidden}
.pts-row-badge img{width:100%;height:100%;object-fit:cover}
.pts-row h4{font-size:15px;margin-bottom:3px}
.pts-row p{font-size:12px;color:var(--pts-muted);margin:0}
/* A class row describes itself exactly like a service tile — the same `.pts-tile-desc.pts-clamp`
   paragraph, so it goes through the same measure, the same clamp and the same Read More button. Only the
   spacing is this layout's own, and all three rules are (0,3,0)/(0,2,0) on purpose:
   ⚠️ `--pts-desc-lines` is declared on `.pts-tile`, and an undeclared custom property makes
   `-webkit-line-clamp:var(--pts-desc-lines)` invalid at computed-value time — i.e. NO clamp at all, an
   unfolded wall of text in the row. It has to be stated here too.
   ⚠️ `.pts-tile-desc.pts-folded{margin-bottom:0}` is (0,2,0) and declared EARLIER in this file, so a bare
   `.pts-row .pts-tile-desc{margin-bottom}` (also 0,2,0) would win on source order and keep the air under a
   folded description — hence the explicit folded rule, and the button's own trim: its base `14px` bottom is
   the air before a COLUMN tile's meta row, which is far too much above this row's one-line meta. */
.pts-row{--pts-desc-lines:1;--pts-row-h:104px;height:var(--pts-row-h)}
.pts-row .pts-tile-desc{font-size:13px;margin:0 0 5px}
.pts-row .pts-tile-desc.pts-folded{margin-bottom:0}
.pt-home-shell .pts-row .pts-tile-more{margin:2px 0 5px}
.pts-tile.pts-expanded{height:auto;min-height:var(--pts-tile-h)}
.pts-row.pts-expanded{height:auto;min-height:var(--pts-row-h)}
.pts-desc-row{display:flex;align-items:baseline;gap:8px;min-width:0;margin:0 0 14px}
.pts-desc-row .pts-tile-desc{flex:0 1 auto;min-width:0;margin:0; font-weight: 300; letter-spacing: 0.2px;}
.pt-home-shell .pts-desc-row .pts-tile-desc + .pts-tile-more{flex:0 0 auto;margin:0;white-space:nowrap}
.pts-row .pts-desc-row{margin-bottom:5px}
/* 🔑 EXPANDED, the button flows INLINE AFTER THE LAST WORD — the row stops being a flex line.
   `align-items:baseline` aligns each item's FIRST baseline, which is exactly right while the paragraph
   is one clamped line and wrong the moment it is not: an unfolded description left "Read Less" parked
   beside line ONE, mid-sentence, with the rest of the text running on underneath it (measured at the
   1120px page measure: the button at y=102 against a last line at 143..158, i.e. two lines above the end
   of the text it closes, and up to 423px to the right of the words on its own line in `soft`). It read as
   a control belonging to the line it sat on rather than to the block it folds. A flex item cannot flow
   with text, so the expanded wrapper turns back into a plain block and the paragraph into an inline run —
   which puts the button in the SAME inline formatting context as the description and lands it after the
   final word, wrapping onto its own line only when it does not fit.
   ⚠️ The 8px is stated here because it comes from the wrapper's `gap` while the row is flex, and a `gap`
   does nothing in block layout — and `markClampedText` inserts the button with `$p.after()`, so there is
   no whitespace text node between them: without it the label welds onto the last word ("hRead Less").
   ⚠️ Scoped to the CARD's `pts-expanded` (toggled by bindTextFolds on `.pts-tile, .pts-row`), never to
   the absence of `pts-clamp`: a review comment matches neither of those cards, and its own button is
   deliberately a line of its own. (0,4,0)/(0,5,0), which is what beats the flex rules above — both the
   paragraph's own `.pts-desc-row .pts-tile-desc` and the button's `.pt-home-shell … + .pts-tile-more`. */
.pts-tile.pts-expanded .pts-desc-row,.pts-row.pts-expanded .pts-desc-row{display:block}
.pts-tile.pts-expanded .pts-desc-row .pts-tile-desc,
.pts-row.pts-expanded .pts-desc-row .pts-tile-desc{display:inline}
.pt-home-shell .pts-tile.pts-expanded .pts-desc-row .pts-tile-more,
.pt-home-shell .pts-row.pts-expanded .pts-desc-row .pts-tile-more{margin-left:8px}
.pts-row-price{text-align:right;font-size:13px;font-weight:900}
.pts-row-price span{display:block;color:var(--pts-muted);font-size:11px;font-weight:400;margin-top:3px; letter-spacing: 0.2px;}
/* Row CTA — the classes layout's equivalent of .pts-tile-cta, in its OWN column so adding it does not
   grow the row's height. `pointer-events:none` for the same reason as the tile's: the whole row is the
   link (.pts-tile-link covers it), so the label must not swallow the click. */
.pts-row-cta{position:relative;z-index:1;color:var(--pts-accent-ink);font-size:12px;font-weight:800;
  white-space:nowrap;pointer-events:none}

/* ---------- about / team / reviews / locations / footer ---------- */
.pts-panel{background:var(--pts-surface);border:1px solid var(--pts-line);border-radius:var(--pts-radius);padding:25px}
.pts-panel h2{font-size:28px;margin:7px 0 13px}
/* ⚠️ pre-line, for the same reason a FAQ answer gets it: this is the merchant's own prose and the line
   breaks inside a paragraph are content. aboutParagraphs() splits on a run of breaks and leaves single
   ones in the text, which only a pre-line box can render — the page escapes what it prints, so a <br>
   is not an option. It cannot affect indentation: the template emits the text as the <p>'s only child,
   already trimmed. */
.pts-panel p{font-size:15px;color:var(--pts-muted);margin:0 0 12px;white-space:pre-line; line-height: 1.6;letter-spacing: 0.2px; font-weight: 300;}
.pts-detail-row{display:flex;gap:15px;padding:12px 0;
  border-bottom:1px solid var(--pts-line);font-size:13px}
.pts-detail-row > strong{flex:0 0 112px}
.pts-detail-row > span{flex:1 1 auto;min-width:0}
.pts-detail-row:last-child{border-bottom:0}
.pts-detail-row span{color:var(--pts-muted)}
.pts-team{display:flex;flex-wrap:wrap;gap:14px;
  --pts-team-basis:calc((100% - (var(--pts-team) - 1) * 14px) / var(--pts-team))}
.pts-team > .pts-team-card{flex:0 1 var(--pts-team-basis);min-width:0}
/* 🔑 EVERY TEAM CARD IS THE SAME SIZE, AND SO IS EVERY CARD'S CONTENT — the same doctrine as `.pts-tile`
   / `.pts-row`, and it takes the same two parts.
   · A FIXED HEIGHT, not a `min-height` and not the flex line's own `stretch`: a wrapping flex line
     equalises the cards that SHARE it and nothing more, so with the merchant's longest name on line 1 the
     rows below it came out shorter (measured, 4-up: 184px against 161.5px — one name wrapping to two
     lines, i.e. exactly one 22.5px line of difference between one row of cards and the next).
   · A flex COLUMN, which is what lets the card's last item claim the free space instead of leaving a
     ragged edge INSIDE it (see `.pts-team-link` below).
   🔑 `--pts-team-h` IS DERIVED FROM WHAT THE CARD ACTUALLY RENDERS, term by term, and it is the TALLEST
   the content can be — 16 padding + 84 avatar + a two-line name (45) + 3 + a one-line designation (18) +
   16 padding, plus the 1px borders under `newbookingpage.css`'s universal border-box = 184. Nothing can
   exceed it: the name is clamped at two lines, the designation ellipsised at one, and the ≤470 block's
   smaller heading only makes the content shorter (measured 181px), so it is a ceiling everywhere and
   never clips.
   ⚠️ THE ONE TERM THAT IS NOT IN THAT SUM IS THE "View availability" LINK, because the template does not
   currently render it (it is commented out there, not deleted). It was worth 28px — the link's own 18px
   line plus the 10px of air above it — so RESTORING THE LINK MEANS PUTTING 28px BACK ON THIS TOKEN (212).
   Restoring it without that is not a cosmetic slip: the card has no `overflow`, so the link would simply
   hang out of the bottom of every card. The `.pts-team-link` rule below is kept for the same reason the
   markup is — so restoring it is one uncomment plus this one number.
   ⚠️ And the content stays TOP-aligned — no `justify-content:center`. With the link gone the spare space
   is real (a member with no designation has ~40px of it), and centring the block would split it above and
   below, which pushes that card's avatar DOWN relative to its neighbours': the avatars are the row's
   strongest horizontal line, and breaking their alignment reads far worse than the space under a short card. */
.pts-team-card{background:var(--pts-surface);border:1px solid var(--pts-line);border-radius:var(--pts-radius);
  padding:16px;text-align:center;display:flex;flex-direction:column;
  --pts-team-h:184px;height:var(--pts-team-h)}
.pts-team-avatar{width:72px;height:72px;border-radius:50%;margin:0 auto 12px;display:flex;align-items:center;justify-content:center;
  /* Second stop was a hard-coded lavender (#ded7ef) — the one place the purple survived a theme
     change, so a teal page grew lilac team avatars. Derived from the accent at the same weight. */
  background:linear-gradient(145deg,var(--pts-accent-soft),var(--pts-avatar-tint));
  color:var(--pts-accent-ink);font-size:20px;
  font-weight:900;overflow:hidden}
.pts-team-avatar img{width:100%;height:100%;object-fit:cover;border-radius:50%}
/* 15px on the tracking ladder (see the CONTENT TYPE SCALE block) — a person's name is a card title like
   any other, and at `normal` it read looser than the offering names in the section above it. */
.pts-team-card h3{font-size:16px;margin-bottom:3px}
/* A member's name is capped at TWO lines. The card is a fixed-width column in a 4-up row, so a long name
   pushed the role and the "View availability" link down and — a flex line stretching to its tallest item —
   took every card beside it with it. Two lines rather than the catalogue's one because a person's name is
   commonly two words and this card is far narrower than a tile.
   ⚠️ This is a CLAMP, not the catalogue's `text-overflow` — that only ever ellipsises a SINGLE line, so it
   cannot express "two lines, then …". The clamp overflows on the HEIGHT axis instead of the width, which is
   why markClippedNames() tests both axes. The full name is the `title` of the inline `.pts-team-name`
   (kept only while the name is really clipped); nothing has to be lifted here — unlike a tile or a row,
   this card has no `.pts-tile-link` overlay painted over its heading. */
.pts-team-card h3{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* 🔑 THE DESIGNATION IS ONE LINE, ELLIPSISED, AND ITS FULL TEXT IS THE HOVER — the NAME's treatment
   above, not the catalogue's Read More fold. Merchants write real prose into this field, so it has to be
   capped either way; what the button cost was a LINE OF THE CARD, given to the one member whose
   designation ran long — i.e. it re-created the very height difference the cap exists to remove, and
   parked a second control under a card whose actual action ("View availability") sits right below it. A
   designation is a LABEL: one line of it identifies the member, and the rest is a tooltip away.
   ⚠️ The <p> is the CLIPPING BOX and the inline `.pts-team-role` carries the `title` — the same split as
   the h3 / `.pts-team-name` above, and the reason markClippedNames() measures the parent (an inline
   element reports clientWidth 0, so measuring the child would title every designation on the page).
   ⚠️ And the paragraph deliberately carries NEITHER `.pts-tile-desc` NOR `.pts-clamp`: those two classes
   ARE markClampedText()'s selector and its clipped test, so dropping them is what guarantees no Read More
   can ever be attached here — there is no second rule to keep in step. */
.pts-team-card p{font-size:12px;color:var(--pts-muted);margin:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* ⚠️ DORMANT: the template does not currently render this link (commented out there, not deleted), so
   this rule is kept in step with that markup rather than deleted with it — and restoring the one means
   restoring the other PLUS the 28px it is worth on `--pts-team-h`, which is stated on the card's own rule
   above. Everything below is what it needs the moment it comes back:
   `margin-top:auto` is what pins the link to the CARD'S BOTTOM, so every link on a line sits on one row
   however the names above them wrapped. The 10px is `padding-top`, not the margin it replaced: an `auto`
   margin absorbs the card's free space, and the tallest card on a line has none to give, which would
   leave that one link flush against its designation.
   ⚠️ `align-self:center` is load-bearing on a flex-column card, not decoration — see above. (The
   `align-self:center` on `.pts-arrow` is a different element solving a different problem: the arrow
   inside THIS link.) */
.pt-home-shell .pts-team-link{display:inline-flex;align-self:center;margin-top:auto;padding-top:10px;
  color:var(--pts-accent-ink);font-size:12px;font-weight:800}

/* ---------- gallery (the merchant's own photos, after the team) ----------
   🔑 THE ARRANGEMENT IS THE PHOTO COUNT'S, and it obeys three rules: AT MOST 6 PHOTOS
   (Constants.GALLERY_MAX_COUNT), AT MOST 2 ROWS, and NEVER AN EMPTY CELL. A fixed "lead + two-up column"
   mosaic broke the last one at every count the column could not fill — 4 photos put two beside each other
   and left an empty square next to the third, which reads as a failed upload rather than a layout.

     1 → one featured photo, the full measure                        (1 row)
     2 → two equal halves                                            (1 row)
     3 → FEATURED photo spanning both rows + 2 stacked beside it      (2 rows, 50% / 50%)
     4 → an equal 2 × 2 grid                                         (2 rows)
     5 → FEATURED photo spanning both rows + a 2 × 2 block beside it  (2 rows, 50% / 2 × 25%)
     6 → an equal 3 × 2 grid                                         (2 rows)

   So an ODD count features its first photo — the arrangement is uneven, and something has to carry the
   extra weight — while an EVEN count is a grid of equals, where no photo is more important than another.
   That is the MOSAIC family, which stops at 6; from 7 to 12 the section is a CONTACT SHEET instead (see
   the block further down). The view groups the photos accordingly and hands the raw count over as
   `data-pts-gal`; the tokens below are re-pointed per count and EVERY rule reads a token, so a count is a
   token block and nothing else. 2 and 4 need no block at all: they are the defaults. ⚠️ A THIRTEENTH photo
   has no column count that divides it and nowhere to go but a fourth row, so the cap and these blocks move
   together — design the count here before raising the constant.

   ⚠️ FLEXBOX ONLY, like the rest of this file. Grid is the obvious tool for a mosaic and is exactly what
   must not be used here (BookingHomeDesignsFlexLayoutTest fails on any Grid property; see the header
   comment). A flex line only puts items SIDE BY SIDE, so the column beside a featured photo has to be a
   real box — .pts-gallery-rest — with its own wrapping line. That box is the only structural reason the
   template emits two containers.

   Every cell is the same 3:2 the editor crops to, so a portrait upload cannot break the arrangement;
   object-fit:cover absorbs whatever ratio actually arrives (an older upload, or an API-written row).
   That shared ratio is what the widths are SOLVED against: a column of k photos of width C stands
   k·C/1.5 + (k−1)·g tall, so a featured photo is pinned to whatever makes it exactly that tall —
   otherwise the shorter side of the row leaves the very gap this is about. */
/* ⚠️ justify-content, NOT align-items (which would stop a featured photo spanning its two rows). It is a
   no-op for every arrangement whose lines are full — their cells sum to exactly 100% — and exists for the
   two PRIME contact-sheet counts, 7 and 11, where no column count divides the photos and one line is
   always short. A short line pushed against the left edge reads as a failed upload, which is the very
   thing this arrangement set exists to prevent; centred, it reads as the end of a sheet. It also covers
   the counts that become short-lined in the ≤920 collapse, where 4-and-5-across drop to thirds. */
.pts-gallery{display:flex;flex-wrap:wrap;gap:var(--pts-gal-gap);justify-content:center;
  --pts-gal-gap:14px;
  --pts-gal-ar:3/2;
  /* DEFAULTS = the even counts 2 and 4: equal halves, so 2 is one row and 4 wraps into 2 × 2. Note
     --pts-gal-item is read inside .pts-gallery-rest, where 100% is the COLUMN's width, so the same
     "half" token means half the column there. Everything is 0 1 — never 1 1: a growable cell stretches
     to the full width the moment it is alone on a line, and a growable cell on the SHORT line of a prime
     count would swell to fill the width the missing photo left, which is exactly the distortion the
     centring above replaces. */
  --pts-gal-half:calc((100% - var(--pts-gal-gap)) / 2);
  --pts-gal-third:calc((100% - var(--pts-gal-gap) * 2) / 3);
  /* The contact-sheet columns (7-12). Each is an EXACT division of the line — n cells plus (n−1) gaps
     sum to precisely 100% — which is what lets a full line sit flush while justify-content only ever
     moves a SHORT one (see below). */
  --pts-gal-quarter:calc((100% - var(--pts-gal-gap) * 3) / 4);
  --pts-gal-fifth:calc((100% - var(--pts-gal-gap) * 4) / 5);
  --pts-gal-lead:var(--pts-gal-half);
  --pts-gal-rest:var(--pts-gal-half);
  --pts-gal-item:var(--pts-gal-half);
  --pts-gal-tile:var(--pts-gal-half)}
/* 3 — the featured photo SPANNING BOTH ROWS, at TWO THIRDS of the measure, with the other two stacked
   full-width in the third beside it. Solving L/1.5 = 2C/1.5 + g against L + C + g = 100% gives
   C = (100% − 2.5g)/3, and that solution is why the split is 2:1 rather than 50/50: at half the measure
   the two stacked photos are 3:2 each, so the column stands ~740px tall at the page measure and the
   featured photo has to stretch into a tall portrait to match it — a gallery taller than the viewport,
   with cells bigger than the section above it. Widening the featured photo shortens EVERYTHING (at 1120px:
   ~496px tall instead of ~741, stacked cells 241px instead of 364) and lets every cell keep the editor's
   3:2 crop, so nothing is stretched at all. ⚠️ Do NOT "restore" 50/50 without re-solving the height. */
.pts-gallery[data-pts-gal="3"]{
  --pts-gal-rest:calc((100% - var(--pts-gal-gap) * 2.5) / 3);
  --pts-gal-lead:calc(100% - var(--pts-gal-gap) - var(--pts-gal-rest));
  --pts-gal-item:100%}
/* 5 — the featured photo at ~50% beside two columns of ~25%, again spanning both rows. Here the span
   falls out of the arithmetic instead of needing a stretch: the column's photos are (R − g)/2 wide, so
   the column stands 2(R − g)/3 + g tall, and R = (100% − 1.5g)/2 makes the featured photo exactly that
   tall while still 3:2. That leaves it a HAIR wider than half — splitting the line dead down the middle
   leaves it ~5px short of the column, and a small gap is still a gap. */
.pts-gallery[data-pts-gal="5"]{
  --pts-gal-rest:calc((100% - var(--pts-gal-gap) * 1.5) / 2);
  --pts-gal-lead:calc(100% - var(--pts-gal-gap) - var(--pts-gal-rest))}
/* 4 — an equal 2 × 2 grid, and the ONE arrangement whose cells are the full half-measure repeated over
   TWO rows, so its natural 3:2 height doubles: ~740px at the page measure, against ~490 for both of the
   other two-row counts and the 520px cap a single photo gets. A cell here is therefore shown at 2:1
   (~560px for the block) — object-fit:cover takes the extra crop off the 3:2 the editor produced. The
   ratio is per-count rather than a px `max-height` deliberately: a fixed cap would crop a wide viewport
   harder than a narrow one, while a ratio crops every width identically. */
.pts-gallery[data-pts-gal="4"]{--pts-gal-ar:2/1}
/* 6 — an equal 3 × 2 grid: thirds all round, first cell included. Its cells are only a third wide, so
   3:2 already lands the block at ~490 and it needs no such adjustment. */
.pts-gallery[data-pts-gal="6"]{--pts-gal-lead:var(--pts-gal-third);--pts-gal-tile:var(--pts-gal-third)}
/* ---- 7 to 12: a CONTACT SHEET, not a mosaic ----------------------------------------------------------
   Above six photos NOTHING is featured. Two reasons, and the first is the honest one: with a dozen
   pictures no single one is the hero — the section has become a sheet of the place rather than a
   composition about one view of it. The second is arithmetic: every featured shape here is a photo pinned
   to the exact height of the column beside it, and a column of 6, 8 or 10 stacked photos stands three to
   five times the height of the picture it would have to match. So each count is an equal grid whose
   COLUMN COUNT is chosen so the last line comes out full wherever the count allows:

     7  → 4 across, 2 rows   (4 + 3 — prime, short last line CENTRED)
     8  → 4 across, 2 rows   (4 + 4)
     9  → 3 across, 3 rows   (3 + 3 + 3, cells flattened to 2:1)
     10 → 5 across, 2 rows   (5 + 5)
     11 → 4 across, 3 rows   (4 + 4 + 3 — prime, short last line CENTRED)
     12 → 4 across, 3 rows   (4 + 4 + 4)

   Measured at the 1120px page measure: heights 373 · 373 · 574 · 298 · 567 · 567 against the mosaic's own
   520 · 369 · 496 · 567 · 371 · 499 (re-measured in the same pass — the figures recorded elsewhere in this
   file read ~5px lower, so compare within one basis, not across the two). One band either way, so the
   height doctrine holds. Cells are 270 · 270 · 364 · 213 · 270 · 270 wide, and the narrowest cell the set
   ever shows is 211px, just above the ≤920 step; no width overflows the page.

   ⚠️ 9 needs the COUNT-4 TREATMENT and for the same reason. A third of the measure is 364px, so a 3:2 cell
   is 243px tall and three rows stand ~756px — half a viewport taller than anything else in the set. At 2:1
   the block comes back to ~574px while its cells stay 364px WIDE, which is a better photograph than the
   270px a 4-across row would give it; object-fit:cover takes the extra crop off the editor's 3:2. A
   per-count RATIO, never a px max-height (which crops a wide viewport harder than a narrow one).

   ⚠️ 10 is FIVE across rather than 4 + 4 + 2 deliberately: five fills both of its lines exactly, and the
   two-row block (~298px) is shorter than the three-row one (~567px) it would otherwise stand. It is also
   the narrowest cell in the set, so it is the one count that gives that up EARLY — at ≤1140px it takes
   quarters like its neighbours (see that block); by 921px five across is 166px.
   ⚠️ At the full measure only 7 and 11 are short-lined, because only they are prime. The narrow steps make
   short lines of other counts too, and the centring that handles all of them lives on .pts-gallery. */
.pts-gallery[data-pts-gal="7"],.pts-gallery[data-pts-gal="8"],
.pts-gallery[data-pts-gal="11"],.pts-gallery[data-pts-gal="12"]{
  --pts-gal-lead:var(--pts-gal-quarter);--pts-gal-tile:var(--pts-gal-quarter)}
.pts-gallery[data-pts-gal="9"]{
  --pts-gal-lead:var(--pts-gal-third);--pts-gal-tile:var(--pts-gal-third);--pts-gal-ar:2/1}
.pts-gallery[data-pts-gal="10"]{
  --pts-gal-lead:var(--pts-gal-fifth);--pts-gal-tile:var(--pts-gal-fifth)}
.pts-gallery-lead{flex:0 1 var(--pts-gal-lead);min-width:0;margin:0}
.pts-gallery-rest{flex:0 1 var(--pts-gal-rest);min-width:0;
  display:flex;flex-wrap:wrap;gap:var(--pts-gal-gap);align-content:flex-start}
.pts-gallery-item{flex:0 1 var(--pts-gal-item);min-width:0;margin:0}
/* The even counts' remaining cells. Siblings of the lead, not children of .pts-gallery-rest, precisely
   because nothing is featured: every cell is the same size, the first one included. */
.pts-gallery-tile{flex:0 1 var(--pts-gal-tile);min-width:0;margin:0}
/* One photo: it is alone on the line, so it takes the whole measure rather than sitting in a half-width
   box beside nothing — but capped, since a full-measure 3:2 is 746px, a whole viewport of one picture. */
.pts-gallery-lead:only-child{flex-basis:100%;max-height:520px}
.pts-gallery figure{position:relative;overflow:hidden;border-radius:var(--pts-radius);
  background:var(--pts-surface-2);border:1px solid var(--pts-line);aspect-ratio:var(--pts-gal-ar)}
.pts-gallery img{width:100%;height:100%;object-fit:cover;display:block}
/* The caption sits ON the photo, so it needs its own ground: a scrim, not just white text, or a label
   over a pale wall is unreadable. Fixed white/black — this is text over an arbitrary photograph, not a
   themed surface, so it must not follow --pts-accent-on or the page's text tokens. */
.pts-gallery figcaption{position:absolute;left:0;right:0;bottom:0;padding:26px 14px 12px;
  color:#fff;font-size:13px;font-weight:800;line-height:1.35;
  background:linear-gradient(180deg,rgba(9,9,12,0),rgba(9,9,12,.72));
  text-shadow:0 1px 2px rgba(0,0,0,.35)}
/* A cell's hit area: a real <button> covering the whole photo, so the preview is reachable by keyboard and
   announced as a control (the same reason the branch rows and the map pill are buttons, not clickable
   divs). Transparent, and ABOVE the caption's scrim — the caption is not interactive, so covering it costs
   nothing, while a button that stopped at the caption would leave a dead strip across the bottom of every
   captioned photo. zoom-in is the cursor that says "this opens larger"; the focus ring is inset because
   .pts-gallery figure clips (overflow:hidden) and an outside ring would be cropped away. */
.pts-gallery-open{position:absolute;inset:0;z-index:2;width:100%;height:100%;padding:0;border:0;
  background:none;cursor:zoom-in}
.pts-gallery-open:focus-visible{outline:2px solid var(--pts-accent);outline-offset:-3px}

/* ---------- gallery preview (lightbox) ----------------------------------------------------------------
   Every cell above is a 3:2 CROP at a fraction of the page measure, so the sheet can never show a photo
   whole — this is where the merchant's photo is shown as uploaded. It reuses the reviews overlay's chrome
   (.pts-modal scrim, body.pts-modal-lock, .pts-modal-close) so the page has ONE dialog language, but it
   has no .pts-modal-panel: the photo IS the dialog, floating on the scrim, which is why the close button
   and both arrows are children of the overlay.
   ⚠️ Written as `.pts-modal.pts-lightbox` (0,2,0) deliberately: these overrides must beat every bare
   `.pts-modal` rule — including the ≤620 `padding:0` — and this block sits BEFORE those in the file, where
   source order would lose. */
.pts-modal.pts-lightbox{padding:58px 18px 26px;background:rgba(9,8,12,.88)}
/* The figure is the box, and it must be bounded on BOTH axes: nothing here scrolls, so an unbounded tall
   photo would simply run off the viewport. */
/* ⚠️ A DEFINITE `width`, not `max-width`. As a flex item with only a max-width this box shrink-wrapped its
   content, so it had no definite width for the stage's `width:100%` to resolve against — the percentage
   fell back to the photo's own natural width and every small upload got a small stage (the exact jumping
   this was meant to end). The photo is centred in it, which is what keeps a height-capped stage centred. */
.pts-lb-figure{margin:0;display:flex;flex-direction:column;align-items:center;gap:12px;
  width:min(1180px,100%);min-width:0}
/* 🔑 A FIXED STAGE, not a box that shrink-wraps each photo — every photo is scaled INTO the same frame.
   `width:auto` (what this shipped as) renders a replaced element at its NATURAL pixel size, and the
   pipeline caps uploads at app.image.max-size WITHOUT upscaling, so a 1200×800 upload filled the measure
   while a 700×467 one rendered 700px wide: same ratio, visibly different frame, and stepping through the
   gallery moved the frame, the caption and the arrows on every photo.
   ⚠️ The stage ratio IS THE RATIO THE EDITOR CROPS TO (`galleryParams.aspect = 1.5` in onlineBookingView —
   move one and move the other, which BookingHomeDesignsGalleryTest pins), so a photo that went through the
   uploader fills it exactly and the frame is the photo. An older or API-written row of some other ratio is
   letterboxed inside the SAME stage instead of resizing it — that is what `object-fit:contain` is for, and
   what the faint plate behind it reads as. CONTAIN, never cover: a preview that crops is just the cell.
   `aspect-ratio` also transfers the height cap into a narrower width on a short viewport (the same
   mechanism as --pts-cover-max on the banner), so the stage stays whole rather than overflowing; the cap
   leaves room for the overlay's own padding, the gap and the caption line.
   The explicit `width` is what defeats newbookingpage.css's bare `img{width:100%}` too (the leak that
   stretches any unpinned image in the header) — it wins on specificity, so the stage cannot be widened. */
.pts-lb-img{width:100%;aspect-ratio:3/2;max-height:calc(100vh - 152px);
  object-fit:contain;display:block;background:rgba(0,0,0,.45);
  border-radius:var(--pts-radius);box-shadow:0 30px 80px rgba(8,6,14,.5)}
/* Fixed white, like the cell's figcaption and for the same reason: this is text on a dark scrim, not on a
   themed surface, so it must not follow --pts-text (near-black in light appearance = invisible here). */
.pts-lb-meta{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:center;gap:10px;
  color:#fff;text-align:center}
.pts-lb-caption{font-size:14px;font-weight:800;line-height:1.4}
.pts-lb-count{font-size:12px;color:rgba(255,255,255,.66);font-variant-numeric:tabular-nums}
/* The same round control as the pill row's scroll arrows (accent-soft fill, accent ink, soft shadow):
   this page has one vocabulary for "step along a row" and that is it, so the merchant's theme carries here
   too. ⚠️ They must stay ABSOLUTE — as flex children of the overlay each would steal 44px from the photo.
   The font-size is on a .pt-home-shell-prefixed rule because the shell resets `button{font:inherit}`. */
.pt-home-shell .pts-lb-nav{position:absolute;top:50%;transform:translateY(-50%);
  width:44px;height:44px;display:flex;align-items:center;justify-content:center;padding:0;
  border:1px solid rgba(var(--pts-accent-rgb),.2);border-radius:999px;
  background:var(--pts-accent-soft);color:var(--pts-accent-ink);
  box-shadow:0 6px 18px rgba(8,6,14,.34);font-size:14px;line-height:1;cursor:pointer;z-index:2}
.pt-home-shell .pts-lb-nav:hover{background:var(--pts-accent-fill);
  border-color:rgba(var(--pts-accent-rgb),.34)}
.pts-lb-nav i:before{margin:0;width:auto;line-height:1}
.pts-lb-prev{left:16px}
.pts-lb-next{right:16px}
/* The close button belongs to the overlay, not to a head bar — there is no panel for it to sit in. */
.pt-home-shell .pts-lightbox .pts-modal-close{position:absolute;top:16px;right:16px;z-index:2}
/* One photo has nowhere to step and nothing to count. */
.pts-lightbox[data-pts-single="true"] .pts-lb-nav,
.pts-lightbox[data-pts-single="true"] .pts-lb-count{display:none}
/* ⚠️ Its narrow-screen trim lives in the overlay chrome's own @media(max-width:620px) block further down,
   NOT in a media query here. Two reasons: that block is where every other .pts-modal override sits, and a
   media query opened this early in the file cuts short the "everything before the first @media" base slice
   that BookingHomeDesignsMobileScaleTest / …FooterTest read (three of their assertions failed on rules
   they could no longer see). Keep plain rules here, breakpoints down there. */

/* ---------- video (the merchant's own clip, after the gallery) ----------------------------------------
   ⚠️ NO `[data-pts-design]` SELECTOR ANYWHERE IN THIS BLOCK — and that discipline is what made rolling
   the section out to ALL SEVEN designs a single deleted condition in buildVideo(). It shipped
   `directory`-only while the treatment was being settled; because neither this block nor the template had
   ever named a design, there was nothing else to find. Keep it that way.
   What makes these rules design-agnostic is that everything here is RELATIVE: the panel is
   `aspect-ratio:16/9` capped by a max-height, so it takes whatever measure its design hands it — the full
   1120px page in the five block designs, the ~752px content column in `split`/`splitwide` — and its corner
   is `var(--pts-radius)`, so it wears each design's own (8px in `directory`, 28px in `soft`). There is no
   fixed width and no hard-coded corner here to re-tune per layout, which is the whole reason there was no
   per-design work to do.

   🔑 THE VIDEO'S REAL POSTER FRAME UNDER A GLASS PLAY BUTTON, with the player created on the click
   (bindVideoEmbed). THREE shapes were built before this one settled, and each was rejected for a reason
   worth keeping written down:
     1. a surface drawn entirely in CSS — an accent-washed dark panel with a white "Play video" pill. Zero
        third-party requests, but it said nothing about what the visitor would be watching and read as an
        empty box with a button in it, i.e. like an image that failed to load.
     2. the real poster behind that same PILL — right picture, wrong control: a labelled pill dropped in
        the middle of somebody's thumbnail reads as a web form, not as a video.
     3. the provider's player embedded directly, with its own controls — honest, and it needs no CSS at
        all, but it hands the section's whole first impression to a black rectangle until the player
        boots, and it pays for a heavy cookie-setting frame on every page load rather than on a click.
   This is (2) with the control redrawn: no visible label, a round translucent button, so the panel reads
   as a piece of video. ⚠️ Do not put the label back — that is (2). The accessible name lives on the
   button's `aria-label`, which is where it belongs for a glyph-only control.

   16:9 by `aspect-ratio`, never a fixed height: the box scales with the column instead of freezing at a
   desktop number, exactly as .pts-cover and .pts-loc-map derive theirs.

   ⚠️ …but a derived height still needs a CAP, and the number is BORROWED, not chosen: 520px is what
   .pts-gallery-lead:only-child caps a single hero photo at, and this section's whole job is to sit in the
   same band as the gallery above it. Uncapped, 16:9 of the 1120px page measure is 630px — two thirds of a
   laptop viewport and the tallest single element anywhere on the page, which is exactly the "stands much
   taller than the section it sits in" failure the gallery's own height doctrine is written against.
   A `max-height` on an aspect-ratio box TRANSFERS into a max-width (520 x 16/9 = 924px), which is why
   `margin-inline:auto` is not optional — without it the capped box sits left with 196px of dead measure
   beside it. Same mechanism as --pts-cover-max, and the reason a definite `width` must never appear here:
   it would block the transfer and letterbox the player instead of scaling it.

   ⚠️ THE DARK GROUND STAYS, and it is doing three jobs: what shows while the poster loads, what stands in
   when there is no poster at all (a Vimeo video before its fetch lands, a YouTube poster whose fallback
   also failed), and the ground the loaded player sits on — the same colour, so pressing play flashes no
   colour change. It is also what any letterboxing lands on.

   ⚠️ NO `border` — a light `--pts-line` hairline around a dark panel reads as an outline drawn on it, and
   a dark box against this page's `--pts-bg` needs nothing to define its edge.

   `overflow:hidden` is here for the RADIUS alone (the poster and the player both have square corners of
   their own). It used to be load-bearing twice over — it also clipped a hover zoom on the poster — but
   that zoom is gone; see .pts-video-poster. */
.pts-video{position:relative;overflow:hidden;aspect-ratio:16/9;max-height:520px;margin-inline:auto;
  border-radius:var(--pts-radius);background:#0b0b0f;cursor:pointer;
  display:flex;align-items:center;justify-content:center}
/* The poster. `cover` is what makes ONE rule serve every source YouTube might hand back: the 1280x720
   frame fills it exactly, and the always-generated 480x360 fallback is 4:3 with pillarbox bars, which
   cover crops back to precisely its 480x270 picture. ⚠️ It also needs its own `width`/`height` — the
   booking page's newbookingpage.css leaks a bare `img{width:100%}`, so an unpinned image here would size
   itself and leave the box's own bottom showing.

   ⚠️ THE POSTER DOES NOT MOVE ON HOVER. It briefly carried a slow `scale(1.04)` zoom, on the reasoning
   that a picture breathing under the cursor says the panel is live — removed by request, and the page is
   better for it: the affordance is the play button, which is unmistakable on its own, and this section's
   content is somebody's photograph rather than a card. Nothing else on this page moves under the pointer
   either (see the catalogue-card hover rule, which bans exactly that), so the zoom was the odd one out.
   The `transition` went with it — there is nothing left to animate. */
.pts-video-poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
  border:0;background:none}
/* 🔑 THERE IS NO SCRIM OVER THE POSTER, and that is a decision, not an omission. The control does need a
   dark ground — it is white-on-ARBITRARY-PHOTOGRAPH otherwise, and a merchant's thumbnail is very often
   bright type on white (a product shot, a title card) — but a full-panel scrim is the wrong way to give it
   one. Measured against a plain light poster, a centre-weighted vignette (which is what shipped) paints a
   visible grey SMUDGE across the middle of the panel: invisible on a busy photograph, obvious the moment
   the thumbnail is flat, which is exactly the kind of thumbnail a small business uploads. It also dims the
   whole picture to solve a problem that exists in one 76px circle.
   So the ground travels WITH the button, as a soft dark halo in its own box-shadow (below). The merchant's
   picture is then shown clean and unmodified, which is the better look and the honest one.
   ⚠️ Do not reintroduce a `.pts-video-scrim` layer — the halo covers the contrast, and the template, the
   live-state hide list and the guard test were all cleaned up with it. */
/* 🔑 THE PLAY BUTTON: a white disc with the merchant's accent in the triangle — the control every video
   anyone has met wears, which is exactly why it works here and a labelled pill did not.
   It stays a real <button> because it is the keyboard and screen-reader path, and it is where the
   accessible name lives now that there is no visible text; the whole .pts-video is the hit area and this
   click just bubbles into the panel's handler, which is why it does NOT carry `pointer-events:none` like
   .pts-tile-cta (that CTA is a label over a link overlay — this one is the thing being pressed).

   ⚠️ WHITE IS THE RESTING STATE, NOT A HOVER STATE. It shipped the other way round — dark translucent
   glass at rest, inverting to white on hover — and the resting form was the weaker of the two on a
   customer-facing page: a play control's job is to be unmissable the moment the section is on screen, and
   a hover it never gets on a phone is not a state at all. So the inversion is gone; hover only tints and
   lifts (below).
   ⚠️ WHICH IS ALSO WHY THERE IS NO `backdrop-filter` HERE ANY MORE. An opaque disc has nothing showing
   through it, so a blur behind it is invisible — it was doing real work only while the disc was
   translucent. Don't re-add it "for the glass look": it would cost a paired-prefix rule
   (BookingHomeDesignsBackdropFilterTest) and buy nothing.

   🔑 THE HALO IS NOW THE ONLY THING SEPARATING THE DISC FROM THE POSTER, and it is what makes a white disc
   safe on ANY thumbnail. White reads instantly on a dark picture; on a light one — a title card, a product
   shot on white, exactly what a small business uploads — the disc would otherwise vanish, so the wide soft
   SPREAD shadow darkens the poster in a ring around it. Because it is a shadow on the disc it is always
   exactly where the control is and nowhere else, which is what let the full-panel scrim go (see
   .pts-video-poster).
   ⚠️ There is no white inset rim any more either — it was the dark disc's edge on a dark poster, and on a
   white disc it is white-on-white, i.e. nothing. A dark inset takes its place: invisible on a dark picture
   (where the disc needs no help) and just enough to close the disc's edge on a pale one.

   ⚠️ WHAT IS AND IS NOT GUARANTEED HERE, measured over forced pure-white and near-black posters — read
   this before "fixing" the light case:
     · the TRIANGLE is the load-bearing contrast, and it is comfortable everywhere: 5.9:1 against the disc.
       It is also the thing that identifies the control, so it is the number that matters;
     · the DISC'S OWN BOUNDARY is 20:1 on a dark poster (trivially) but only **2.24:1 on a pure-white one**,
       where a white disc has nothing but the halo to sit against. That is soft, and it is a deliberate
       stopping point, not an oversight: the alphas here were already raised once (rim .08 -> .16, halo
       .34 -> .42, which moved it from 1.92:1) and pushing further starts painting a visible grey donut
       around the button on ORDINARY mid-tone photographs — the same artefact the full-panel scrim was
       removed for, just smaller. A fully white full-frame thumbnail is also rare.
   So if this ever needs to be stronger, the move is a hairline that only shows on a light poster, NOT more
   halo.

   Hover TINTS AND LIFTS rather than inverting: `--pts-accent-soft` is an opaque light tint of the accent
   (the accent mixed into the page ground), so it warms the white disc towards the merchant's colour
   without going translucent over the poster, and it works on all ten themes — an accent FILL would not
   (measured against this panel, `darkblue` is 1.11:1 and `default` 1.18:1, so three swatches would vanish).
   ⚠️ The triangle reads `--pts-accent-ink`, never `--pts-accent-on`: the latter is ink for the accent FILL,
   i.e. white on nine themes — white on this white disc.

   ⚠️ IT DOES GET A TRIM ON A PHONE, and that is a real difference from `.pts-btn-small`, whose padding is
   deliberately kept OUT of the responsive blocks. That rule protects a control competing for ROOM in a
   row — shrink it and it reads as a lesser control. This one competes for no room at all (it fits with
   ~180px to spare even at 320px); what it competes with is the PICTURE. A circle in a box whose height is
   DERIVED takes a share of that height, and the share swings: 60px is 12% of the 520px desktop panel and
   would be **38% of the 158px one at 320px**, where it crowds the subject of the thumbnail — the very
   thing the poster exists to show. So the ≤470 block trims it to 48px (20-30% of the box there), with the
   glyph scaled to match. Sizes below are the desktop pair; keep the two in step.

   ⚠️ AND THE DISC ITSELF IS SIZED AGAINST THE PANEL, NOT PICKED. It shipped at 76px — 15% of the desktop
   panel's height and 48% of the phone one's — and read as an oversized button on a small picture. 60/48px
   is the pair that keeps it in the 12-30% band across every width. Two things move WITH it and are not
   independent taste: the glyph's box (kept at half the diameter, which holds its ink near 4.8% of the
   disc's area) and the halo's blur+spread, proportioned to the disc it surrounds. */
.pts-video-play{position:relative;z-index:2;width:60px;height:60px;padding:0;border:0;border-radius:50%;
  cursor:pointer;display:flex;align-items:center;justify-content:center;
  background:#fff;color:var(--pts-accent-ink);
  box-shadow:inset 0 0 0 1.5px rgba(12,12,16,.16),
             0 0 30px 13px rgba(6,6,10,.42),
             0 9px 22px rgba(0,0,0,.3);
  transition:transform .24s cubic-bezier(.2,.7,.3,1),background .24s ease,box-shadow .24s ease}
.pts-video:hover .pts-video-play{background:var(--pts-accent-soft);transform:scale(1.07);
  box-shadow:inset 0 0 0 1.5px rgba(12,12,16,.16),
             0 0 35px 15px rgba(6,6,10,.46),
             0 12px 28px rgba(0,0,0,.34)}
/* ⚠️ White ring, not --pts-accent-ink: it is drawn OUTSIDE the disc, on the poster, where a darkened
   accent is invisible — and the poster is darkest exactly there, under the halo. */
.pts-video-play:focus-visible{outline:2px solid #fff;outline-offset:4px}
/* 🔑 THE TRIANGLE IS AN INLINE SVG, and it is inline SVG for one reason: ROUNDED CORNERS. It was drawn
   with the CSS border trick (a zero-size box with one coloured border), which is compact and needs no
   markup — but a border triangle's corners are mathematically sharp and there is no way to soften them,
   and three needle points on an otherwise soft, round control looked like a different icon set. So it
   joins the review stars as this page's other deliberate non-fontello mark (fontello carries no play glyph
   at all; its only near miss, `icon-video-meeting-obp`, is a CAMERA this page already uses to mean "this
   location is online", so borrowing it would make one mark say two things).

   ⚠️ THE ROUNDING IS THE STROKE, NOT A RADIUS. `stroke-linejoin:round` with the path stroked in its own
   fill colour rounds every corner by exactly `stroke-width / 2` — 2px here — and there is no other way to
   round a polygon's corners in SVG. Consequences, both easy to get wrong:
     · the stroke EXPANDS the shape by `stroke-width / 2` on every side, so the path is drawn 4px smaller
       than the ink: `M11.33 8 L22.33 15 L11.33 22 Z` is 11x14 in user units and renders as 15x18;
     · `stroke-width` is in USER units, so it scales with the box — the phone's smaller svg gets a
       proportionally smaller radius (1.6px) for free, which is what you want.
   `fill` and `stroke` are both `currentColor`, which is what lets the button's hover carry the triangle.

   ⚠️ AND THERE IS NO OPTICAL NUDGE ANY MORE — the path carries it. A triangle's centroid sits a third of
   its width from the base while its bounding box centres at half, so a geometrically centred one reads as
   leaning back; the border version corrected that with a `margin-left` of width/6. Here the path is simply
   drawn with its centroid on the box centre ((2a+b)/3 = 15 of a 30-unit box), so flex centring is already
   optically right. If the path's x values ever change, keep that identity — do not reintroduce a margin.

   The box is HALF THE DISC's diameter (30px of 60, 24px of 48), which holds the ink at ~4.8% of the disc's
   area. It was 3.5% with the border version and read as a small icon inside a large circle. */
.pts-video-glyph{width:30px;height:30px;display:block;overflow:visible;
  fill:currentColor;stroke:currentColor;stroke-width:4;stroke-linejoin:round}
/* Loaded: the facade is HIDDEN, not removed (bindVideoEmbed appends the player rather than replacing the
   markup), so the loaded state is one reversible class. The ground is already the panel's own, so there is
   no colour change to flash. */
.pts-video.pts-video-live{cursor:default}
.pts-video-live .pts-video-poster,
.pts-video-live .pts-video-play{display:none}
.pts-video iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}

.pts-reviews{display:flex;flex-wrap:wrap;gap:15px;align-items:flex-start}
/* Average + distribution live in ONE panel (the classic's .rating-summary-card): loose in the flex line
   the bars stretched to ~880px and left their counts stranded at the page edge. Inside the panel the
   average sits behind a divider and the bars get a readable, bounded measure. */
.pts-review-summary{flex:1 1 100%;display:flex;flex-wrap:wrap;align-items:center;gap:24px;
  background:var(--pts-surface);border:1px solid var(--pts-line);border-radius:var(--pts-radius);
  padding:22px 26px;box-shadow:var(--pts-shadow)}
/* ⚠️ `align-self:center` is not redundant with the panel's `align-items:center`. The base
   `.pts-rating-card` rule sets `align-self:start` — correct when the card was a loose flex child, but it
   OUT-RANKS the container's alignment, so the average sat ~40px above the middle of the taller bar
   block. The divider's short line made it read as deliberate, which is why it survived a look. */
.pts-review-summary > .pts-rating-card{flex:0 0 auto;min-width:168px;align-self:center;background:none;
  border:0;border-right:1px solid var(--pts-line);border-radius:0;padding:0 26px 0 0;box-shadow:none}
/* The bars fill the rest of the panel, as they do in the classic's summary card and in this page's own
   reviews overlay — the panel's border is what keeps the counts from looking stranded, so a narrower cap
   only left dead space to its right. */
.pts-review-summary > .pts-bars{flex:1 1 260px}
/* Summary line = rating card + distribution, mirroring the reviews modal; the list then takes a line of
   its own. ⚠️ The list's flex-basis is what pushes it down — with `flex:1 1 auto` it shared the line with
   the card and there was nowhere for the bars to go without squeezing all three. Full width also gives a
   review's text a longer measure than the old ~880px column. */
/* ---------- reviews: stars, cards, filters, write form ----------
   The star is an SVG polygon (ported from bookingHomeView.renderStarSvg), not a text "★": only a shape
   can show a HALF star, and a 4.5 average has to look like 4.5. Colour comes from --pts-warning through
   `currentColor`, which is also what the half-star gradient samples. */
/* Sized per surface, as the classic sizes its .soft-star (18px average, 14px card and chip) — a flat
   15px everywhere read small next to this page's larger type. 15px stays the default for anything not
   listed below. */
.pts-star{width:15px;height:15px;flex:0 0 auto;color:var(--pts-warning)}
.pts-rating-card .pts-stars .pts-star{width:24px;height:24px}   /* under the 45px average */
.pts-review-score .pts-star{width:18px;height:18px}
.pts-chip-star .pts-star{width:14px;height:14px}
/* ⚠️ `:not(.shalf)` is load-bearing. A CSS `fill` OVERRIDES the presentation attribute on the polygon, so
   a blanket `.pts-star polygon{fill:currentColor}` silently beat the half star's inline
   `fill="url(#ptsHalf_…)"` — and the placeholder rule that tried to restore it pointed at a gradient that
   does not exist, which paints NOTHING (an invalid funciri has no fallback). Net effect: a 4.5 average
   rendered as four stars and a gap. Full and empty stars take their colour from CSS; the half star is
   painted entirely by its own gradient, so CSS must keep out of its fill. */
.pts-star:not(.shalf) polygon{fill:currentColor}
.pts-star.sempty{color:#D6D6D6}
.pts-stars{display:inline-flex;align-items:center;gap:2px;color:var(--pts-warning);line-height:1}
/* The 9px is the gap under the big average number — it used to live on the shared
   `.pts-stars` rule (a leftover from the text-star design), where it also pushed a review
   card's stars 9px down inside their score row: the row then measured 27px tall (18 + the
   margin) and centred the NUMBER in that, leaving it ~5px above the stars' centre. */
.pts-rating-card .pts-stars{gap:3px;margin-top:9px}

/* A distribution row doubles as a filter, as the classic's rating rows do.
   ⚠️ The affordance is scoped to `.pts-bar-pick` — the rows starBars() gave a `data-stars` to, i.e. the
   ratings that actually have reviews. An unscoped `cursor:pointer` also dressed up the 0-count rows,
   whose only possible destination is the "no reviews with this rating" empty state; the chips skip
   those ratings for the same reason. The row itself still renders at every count: a distribution that
   drops its zeros is not a distribution. */
.pts-bar-row{border-radius:8px;padding:2px 4px;transition:background .16s ease}
.pts-bar-pick{cursor:pointer}
.pts-bar-pick:hover{background:var(--pts-accent-soft)}

/* 🔑 The filter chips and the Leave Review button share ONE wrap context — `display:contents` on the
   chips box makes each chip an item of THIS row instead of the row holding one unsplittable box.
   ⚠️ As a box, `.pts-chips` is a single flex item whose flex-basis is `auto` = max-content = every chip
   on one line, and flex breaks the line on that hypothetical size BEFORE it wraps anything. So the
   button was pushed off the moment one chip would have wrapped, and only THEN did the chips wrap
   internally — leaving a hole beside the last chip that the button could never move into. Measured at
   375px: chips on 2 lines and the button opening a THIRD, with 295px free next to the last chip and the
   button only 113px wide (171px free at 320px). Same trap the identity card's social row documents.
   Two consequences, both deliberate: the chips' own `gap` no longer applies, so this row's 8px IS the
   chip rhythm now; and `justify-content:space-between` had to go — with every chip an item of this row
   it would have spread the chips across the full width. The button is right-anchored by its own auto
   margin instead, which resolves per LINE, so it sits right whether it shares the chips' last line or
   takes one of its own.
   ⚠️ Keep the plain `.pts-chips` rule below as the fallback: an engine without `display:contents` keeps
   the box and simply gets the layout that shipped before this, rather than a broken one. */
.pts-review-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;flex-basis:100%}
.pts-chips{display:flex;flex-wrap:wrap;gap:8px}
.pts-review-actions .pts-chips{display:contents}
/* Right-anchored by an auto margin rather than the row's `justify-content`, which now belongs to the
   chips. It resolves per LINE, so the button sits at the right edge whether it shares the chips' last
   line or takes one of its own. On the row's own child, not `#ptsLeaveReview` — the id is the view's
   handle, and layout here shouldn't depend on it. */
.pts-review-actions > .pts-btn{margin-left:auto}
/* The digit and its star are ONE item: .pts-chip is an inline-flex row with a gap, and every contiguous
   text run inside it becomes a separate flex item — so an unwrapped `5<svg>1` put the chip's gap between
   the digit and the star too ("5 ★ 1"). Grouped, the star sits against the digit and only the count is
   spaced off. */
.pts-chip-star{display:inline-flex;align-items:center;gap:1px}
.pt-home-shell .pts-chip{display:inline-flex;align-items:center;gap:5px;padding:7px 13px;font-size:13px;
  font-weight:600;color:var(--pts-text);background:var(--pts-surface);border:1px solid var(--pts-line);
  border-radius:999px;cursor:pointer}
.pt-home-shell .pts-chip:hover{border-color:var(--pts-accent)}
.pt-home-shell .pts-chip-on{background:var(--pts-accent-soft);border-color:var(--pts-accent);
  color:var(--pts-accent-ink)}

/* One card shape for the section list and the overlay — see reviewCardHtml. */
.pts-review-head{display:flex;align-items:flex-start;gap:12px}
.pts-review-avatar{flex:0 0 42px;width:42px;height:42px;border-radius:50%;overflow:hidden;
  display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;
  background:var(--pts-accent-soft);color:var(--pts-accent-ink)}
.pts-review-avatar img{width:100%;height:100%;object-fit:cover}
.pts-review-who{flex:1 1 auto;min-width:0}
.pts-review-who .pts-reviewer{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.pts-review-mine{padding:2px 9px;border-radius:999px;font-size:11px;font-weight:700;
  background:var(--pts-review-mine-bg);color:var(--pts-success)}
.pts-review-sub{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:2px}
.pt-home-shell .pts-review-edit{padding:0;font-size:12px;font-weight:600;color:var(--pts-accent-ink);
  background:none;border:0;text-decoration:underline;cursor:pointer}
.pts-review-score{display:inline-flex;align-items:center;gap:6px;flex:0 0 auto;line-height:1}
/* ⚠️ `line-height:1` is what actually aligns the number with the stars. Flex centres BOXES, and the
   number's line box carries the inherited leading — so its box was centred while its ink sat 5.3px above
   the stars' centre. Hugging the ink makes centring mean what it looks like. */
.pts-review-score b{font-size:13px;font-weight:700;line-height:1}
.pts-review-text{margin:10px 0 0}
/* Two-line fold. The clamp is CSS (so it is exact at any width); the Read More button is added by
   markClampedReviews() only to the comments this actually clips, and lives AFTER the paragraph — inside
   it, the clamp would hide the button along with the text it belongs to. The -webkit- trio is the form
   supported everywhere, including the Blink-based browsers this page has to serve. */
.pts-review-text.pts-clamp{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden}
.pt-home-shell .pts-review-more{display:inline-block;margin:6px 0 0;padding:0;font-size:13px;
  font-weight:600;color:var(--pts-accent-ink);background:none;border:0;cursor:pointer}
/* Your own review, marked the way the classic marks it (.review-card-mine: background #fbfaff, a
   lavender border) — a soft tinted block, not a heavy accent bar down the side. The negative margins let
   the tint bleed to just inside the list's own padding so it reads as a highlighted row rather than an
   indented one; the green "Your Review" pill and the pinned position do the rest of the work. */
/* Your own review gets NO fill of its own — it reads as one of the list, marked by the green "Your
   Review" pill, the Edit link and its pinned position at the top. `.pts-review-own` stays on the card as
   a state hook (the renderer and the tests use it), deliberately with no styling attached: an accent bar
   and a tinted backdrop were both tried here and both read as heavier than the card deserves. ⚠️ If a
   fill is ever wanted again, paint it with a `::before` backdrop — a margin/padding bleed moves the
   content box and puts this card's avatar, name and text 14px out of line with every other one. */

.pts-reviews-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:34px 18px;
  text-align:center}
.pts-reviews-empty .pts-star{width:34px;height:34px}
.pts-reviews-empty strong{font-size:15px}
.pts-reviews-empty p{margin:0;color:var(--pts-muted);font-size:13px;max-width:340px}

/* ---------- write / edit review dialog ----------
   Reuses the overlay chrome (.pts-modal) so there is one scrim, one close button style and one
   body-lock class on the page. It keeps the overlay's 520px measure, its 20px title and the page's
   16/780 primary — deliberately: this dialog is the page's own dialog language, not a smaller variant
   of it. (A compacted step — 480px panel / 18px title / 14/760 actions — was built, measured and
   rejected as too tight for what the form holds.) */
.pts-modal-panel-sm{max-width:520px}
/* Label above its own control (8px), a whole step between the two field groups (18px). They used to be
   14 and 14 with the body's own 6px on top, so the first group opened 20px under the head's rule while a
   label sat as close to the group ABOVE it as to the control it names — proximity grouped nothing. The
   `:first-child` reset is what lets the body state the top inset once. */
.pts-field-label{display:block;font-size:13px;font-weight:600;color:var(--pts-muted);margin:18px 0 8px}
.pts-field-label:first-child{margin-top:0}
/* 🔑 ONE star system on the page. These five used to be their own drawing — two data-URI SVGs filled
   `#FF8A1F` and `#D6D6D6` at 34px — so the picker rendered a different orange from --pts-warning AND
   half again the size of the biggest stars anywhere else (the 24px average in the summary card), which
   is what made the dialog read as pasted in from another product. They are now the view's own
   `starSvg` polygon (built once by ensureStarInput), coloured through `currentColor` by the SAME
   `.pts-star` / `.pts-star.sempty` rules as every other star here — so a colour change lands in one
   place and dark appearance follows for free. ⚠️ Don't put a fill back on the button: the ink is the
   SVG's, and the button is only the hit area. */
.pts-star-input{display:flex;align-items:center;gap:0}
.pt-home-shell .pts-star-pick{width:34px;height:34px;padding:0;background:none;border:0;cursor:pointer;
  display:flex;align-items:center;justify-content:center;line-height:1;
  transition:transform .12s ease}
/* The star ink. 26px keeps a comfortable touch target (the 34px button) without out-shouting the 24px
   average this dialog is opened from — the row's ink gaps come from the button's own padding, which is
   why the row sets gap:0 (contiguous hit areas are better for a thumb, too). ⚠️ Shrink the INK, never
   the hit area. */
.pt-home-shell .pts-star-pick .pts-star{width:26px;height:26px}
.pt-home-shell .pts-star-pick.hover{transform:scale(1.08)}
/* ⚠️ `line-height:1` is what aligns the number with the stars — the same trap `.pts-review-score b`
   documents: flex centres BOXES, and this span carried the inherited leading, so its 22.5px box was
   centred in the 34px row while its ink sat above the stars' centre. It also had `margin-left:6px` on
   top of the row's `gap:6px`, reading as a detached stat rather than this row's own value. */
#ptsStarInputValue{margin-left:12px;font-size:15px;font-weight:800;line-height:1;color:var(--pts-text)}
/* A field has to look like a field: on --pts-surface it was white-on-white, so only the hairline said
   it was writable. --pts-surface-2 is the same ground the close button and the chips already sit on. */
#ptsReviewComment{width:100%;min-height:112px;padding:12px 14px;font:inherit;font-size:14px;
  color:var(--pts-text);background:var(--pts-surface-2);border:1px solid var(--pts-line);
  border-radius:var(--pts-radius-sm);resize:vertical}
#ptsReviewComment::placeholder{color:var(--pts-muted)}
#ptsReviewComment:focus{outline:none;background:var(--pts-surface);border-color:var(--pts-accent);
  box-shadow:0 0 0 3px var(--pts-accent-soft)}
/* Plain numerals ("240 / 1000"), the same call as the lightbox's position counter: "N of 1000" would
   need a {token} key in all 39 bundles to say what two numerals already say. 12px = the caption tier. */
.pts-char-count{margin:6px 0 0;font-size:12px;color:var(--pts-muted);text-align:right;
  font-variant-numeric:tabular-nums}
.pts-form-error{margin:10px 0 0;font-size:13px;color:var(--pts-danger)}
#ptsReviewForm .pts-modal-body{gap:0;padding:20px 22px}
/* 🔑 The action row is a FOOTER, outside .pts-modal-body — which is the panel's only scroll container,
   so a submit button inside it scrolls away with the text on a short viewport (and on a phone, where the
   panel is full-screen, that is the whole point of the dialog disappearing off the bottom).
   The button is also auto-width and right-aligned now: stretched to 476px it was the loudest thing in
   the dialog — a pill the width of a banner, outweighing the modal's own title, with nothing balancing
   it. Cancel supplies that balance and gives the keyboard an obvious way out. */
.pts-modal-foot{display:flex;align-items:center;justify-content:flex-end;gap:10px;flex:0 0 auto;
  padding:14px 22px;border-top:1px solid var(--pts-line);background:var(--pts-surface)}
#ptsReviewForm .pts-btn{min-width:132px}
.pt-home-shell .pts-btn[disabled]{opacity:.45;cursor:not-allowed;transform:none}
.pts-reviews > .pts-review-list{flex:1 1 100%;min-width:0}
.pts-rating-card{background:var(--pts-surface);border:1px solid var(--pts-line);border-radius:var(--pts-radius);
  padding:25px;text-align:center;align-self:start}
.pts-rating-card strong{display:block;font-size:45px;line-height:1}
.pts-rating-card span{display:block;color:var(--pts-muted);font-size:12px;margin-top:6px}
.pts-review-list{background:var(--pts-surface);border:1px solid var(--pts-line);border-radius:var(--pts-radius);padding:0 22px}
.pts-review{padding:19px 0;border-bottom:1px solid var(--pts-line)}
.pts-review:last-child{border-bottom:0}
/* "View all (N)" closing the teaser list — see reviewsMoreHtml. It sits INSIDE the list card, as the
   row after the last review, so it reads as the end of THAT list rather than a stray control on the
   page; the last review therefore keeps its own hairline (it is no longer :last-child), which is the
   separator this row wants anyway. Full width so it is unmissable after four cards, but flat (no fill,
   no border) — a solid pill here would compete with "Leave Review" above it, which is the section's
   real action. ⚠ It is `.pts-review-all`, NOT `.pts-review-more` — that class is the Read More fold's
   button (above), whose delegated click.ptfold handler would fire on this one too. ⚠ And the selector
   MUST carry the `.pt-home-shell` prefix: `.pt-home-shell button{font:inherit}` is (0,1,1) and the
   shorthand sets font-size, so a bare `.pts-review-all` (0,1,0) loses and the row renders at body
   size. */
.pt-home-shell .pts-review-all{display:flex;align-items:center;justify-content:center;gap:5px;width:100%;
  padding:16px 0;border:0;background:none;font:inherit;font-size:14px;font-weight:800;
  color:var(--pts-accent-ink);cursor:pointer}
.pts-review-head{display:flex;justify-content:space-between;gap:14px}
.pts-reviewer{font-size:14px;font-weight:800}
.pts-review-date{font-size:11px;color:var(--pts-muted);white-space:nowrap}
.pts-review p{font-size:14px;color:var(--pts-muted);margin:7px 0 0}
/* The business's answer, in the homepage list as well as the modal (the classic shows it in both). Same
   soft-fill treatment as .pts-modal-reply, minus that block's 47px avatar indent — this list has no
   avatar column to align to. */
.pts-review-reply{margin:11px 0 0;padding:11px 13px;border-radius:var(--pts-radius-sm);
  background:var(--pts-surface-2)}
.pts-review-reply strong{font-size:12.5px;letter-spacing:.02em}
.pts-review-reply p{margin:4px 0 0;font-size:14px}
/* Equal columns. This was `1.15fr .85fr`, sized for the ONE arrangement the design was drawn with —
   a single location card beside a narrower hours card. But these cells are filled in DOM order by
   however many location cards the business has, and the ratio applies to whatever lands in them: with
   2 locations it silently made location A wider than location B, and with an odd count the hours card
   became the narrow one. Any fixed asymmetry mis-assigns widths as soon as the card count changes, so
   equal columns are the only ratio that is right for every count. minmax(0,1fr), not 1fr, so a long
   unbroken address or URL cannot push its column past its share. */
.pts-loc{display:flex;flex-wrap:wrap;gap:20px;align-items:flex-start;--pts-loc-basis:calc((100% - 20px) / 2)}
/* `0 1 basis` keeps both cards at exactly half the measure — the flex equivalent of the equal
   `repeat(2,minmax(0,1fr))` tracks. With `1 1` a lone card on the last row would stretch to full width,
   which is precisely the uneven sizing this replaced. min-width:0 stands in for the minmax(0,…). */
.pts-loc > .pts-loc-card{flex:0 1 var(--pts-loc-basis);min-width:0}
.pts-loc-card{background:var(--pts-surface);border:1px solid var(--pts-line);border-radius:var(--pts-radius);padding:23px}
.pts-loc-card h3{font-size:18px;margin-bottom:13px}
/* ⚠️ align-items:center is LOAD-BEARING — every icon+text row on this page states it (.pts-social-row,
   .pts-fact, .pts-hours-row) and this one was the single omission. Under the flex default `stretch` the
   icon keeps its explicit 25px height while the <span> stretches to the row, and text renders at the TOP
   of its box: the base line-height is the NUMBER 1.5, so against this row's 13px that is a 19.5px line box
   sitting (25 - 19.5) / 2 = 2.75px above the icon's centre. The glyph is centred inside its own tinted
   square either way, which is exactly why the defect reads as "the text is misaligned", not the icon.
   Row height is unchanged — max(25, 19.5) is 25 with or without this. */
.pts-contact-row{display:flex;align-items:center;gap:10px;padding:12px 0;border-bottom:1px solid var(--pts-line);font-size:13px}
.pts-contact-row:last-child{border-bottom:0}
.pt-home-shell .pts-contact-row a{color:var(--pts-accent-ink)}
/* Fontello icon box, same contract as .pts-fact-icon — the class comes from the template, the font is
   sized here, and fontello's `:before` margins are reset so the glyph sits on the box's centre. These
   four rows used to draw `◎ ☎ ✉ ↗` as HTML entities at 11px: four codepoints from four different system
   fonts, so they came out at four different sizes (and the phone/envelope fell back to an emoji face on
   some platforms). */
.pts-contact-icon{width:25px;height:25px;border-radius:8px;background:var(--pts-accent-soft);color:var(--pts-accent-ink);
  display:flex;align-items:center;justify-content:center;flex:0 0 auto;font-size:13px;line-height:1}
.pts-contact-icon:before{margin:0;width:auto;line-height:1}

/* ---------------------------------------------------------------------------------------------
   ONE physical branch → map panel + a single contact card ([data-pts-single], see
   buildSingleLocation). The equal-basis rule above is right for a VARIABLE number of cards; with
   exactly one branch there is only ever one address card, so the row becomes a fixed pair and can
   be sized deliberately: the map takes the larger share, the contact card the smaller.

   ⚠️ The pair keeps the row's child count at TWO, which is what makes the hours card land full
   width below it: #pts-hours is then child 3, and the :nth-child(odd) span further down IS the
   "alone on its line" test. (This is the third card type on this line — the parity note there says
   to re-check it, and this is that check.)

   ⚠️ `--pts-loc-basis` is re-pointed for the contact card rather than adding a second rule, so the
   existing `.pts-loc > .pts-loc-card{flex:0 1 var(--pts-loc-basis)}` keeps sizing it — and stays
   non-growable, which is the invariant that whole block exists to protect. The two bases sum to
   100% minus the 20px gap, so the pair fills the measure exactly.
   --------------------------------------------------------------------------------------------- */
/* [data-pts-branches] is the SAME pair, with a branch list in place of the contact card (two or more
   locations — buildBranchPicker), so it shares these bases: one rule, one mental model, and the four
   places that override them stay four one-line changes instead of eight. */
.pts-loc[data-pts-single],
.pts-loc[data-pts-branches]{--pts-loc-map-basis:calc(58% - 10px);--pts-loc-basis:calc(42% - 10px)}
/* 🔑 THE PICKER'S BLOCK STOPS GROWING — BUT THE MAP KEEPS ITS OWN HEIGHT.
   The branch list grows ~93px per branch against a panel whose height is derived from its own width
   (aspect-ratio, below — 400px at the 1120px page measure), so the section grew without limit: measured
   400 vs 460 (n=2), 400 vs 620 (n=4), 400 vs 898 (n=7), i.e. at seven locations the map covered 45% of
   the row's height and the rest was bare page. `--pts-branch-max` caps the ROWS instead (see
   .pts-branch-scroll), which bounds the whole thing: FOUR rows are visible, the rest scroll, and the
   block a five-branch business gets is the block a fifty-branch business gets.

   ⚠️ DO NOT give the map the LIST's height. `align-items:stretch` here (overriding `.pts-loc`'s
   flex-start) makes both columns take the flex line's cross size, which does square the pair off — it was
   built, measured and REJECTED: the panel is a 16:10 box by design, its height derived from its own width
   so it scales with the column instead of freezing at a desktop number (same reasoning as .pts-cover), and
   stretching it to the list turned it into a near-square 620-695px slab that dominated the section. The
   ground under a short map is deliberate, and the cap is what bounds it (~295px at worst, against 500 and
   rising before it).

   ⚠️ FOUR ROWS, and the fifth is not half-shown. 372px is 4 × the 93px row (a one-line address plus its
   "Get directions" line) — but a px cap can only be right for rows of ONE height, and they still vary: a
   wrapped address is taller (--pts-branch-min below pins the FLOOR only). So this value is the pre-measure
   FALLBACK (correct on first paint for the common case, and the whole story with JS off) and
   bindBranchScroll()'s fitBranchRows() then pins the box to the 4th row's real bottom edge, whatever that
   row happens to be. It clears the inline height below 921px, where the cap is released anyway (see the
   ≤920 block).
   ⚠️ Change BRANCH_VISIBLE_ROWS in the view when you change this number, or the two disagree for one
   frame — and keep both a WHOLE number of rows: a half row was tried and read as a broken row. */
.pts-loc[data-pts-branches]{--pts-branch-max:372px}
/* 🔑 EVERY ROW GETS THE SAME SPACING, physical or virtual. A branch with no actions line — a VIRTUAL one
   ("Online", and deliberately no directions link), or a physical one whose address the merchant left
   blank — is one LINE shorter than its neighbours, so it measured 68px against their 93px and read as a
   squeezed row wedged between two normal ones; loudest when it is the SELECTED row, whose fill states
   that height. This is the three-line row, stated: 13 top + 21 name + 2 gap + 18.75 note + 6 + 18.75 link
   + 13 bottom. Re-derive it if any of those change (the note/link line is 12.5px × the shell's 1.5).
   It is a FLOOR (see .pts-branch), so a wrapped address still grows past it and --pts-branch-max's
   4 × 93 fallback is now right for a list holding a virtual branch too.
   ⚠️ STATED ONCE, and it holds in every layout: no design block and no media query may re-point it or put
   a height/max-height back on a row (a wrapped address is 190.8px at 320px, a wrapped virtual name 109.8px
   — a height would clip both). Unlike --pts-branch-max, the floor is NOT released in the ≤920 collapse:
   "the same spacing" is not a desktop-only rule. Measured across all seven designs at 320-1920px. */
.pts-loc[data-pts-branches]{--pts-branch-min:92.5px}
/* No fixed height: `aspect-ratio` derives one from the width, so the panel scales with the column
   at every breakpoint instead of freezing at a desktop number (same reasoning as .pts-cover). */
.pts-loc > .pts-loc-map{flex:0 1 var(--pts-loc-map-basis);min-width:0;position:relative;overflow:hidden;
  aspect-ratio:16 / 10;border:1px solid var(--pts-line);border-radius:var(--pts-radius);
  background:var(--pts-surface);cursor:pointer}
/* Abstract map ART, drawn here in CSS: grid + two roads + a water body. It costs ZERO requests,
   which is the point — the real Google embed is a heavy third-party frame and is loaded only when
   the visitor asks for it (bindLocationMap), exactly like the classic page's map popup. It is
   deliberately abstract and unlabelled, and the panel carries a "View in Map" control plus the
   business's real name + address, so nothing here reads as a rendered map OF this address. */
.pts-map-art{position:absolute;inset:0;
  background:
    radial-gradient(115% 85% at 96% 112%,var(--pts-map-water) 0 44%,transparent 45%),
    repeating-linear-gradient(to right,var(--pts-line) 0 1px,transparent 1px 84px),
    repeating-linear-gradient(to bottom,var(--pts-line) 0 1px,transparent 1px 84px),
    var(--pts-surface)}
/* The two roads. Rotated bars, so they cross the panel diagonally like the streets on a real map;
   both are tinted from the merchant's accent so the art follows the booking-page theme. */
.pts-map-art:before,.pts-map-art:after{content:"";position:absolute;top:-60%;bottom:-60%;
  background:rgba(var(--pts-accent-rgb),.13)}
.pts-map-art:before{left:32%;width:26px;transform:rotate(19deg)}
.pts-map-art:after{left:46%;width:17px;transform:rotate(-58deg);
  background:rgba(var(--pts-accent-rgb),.09)}
/* Fontello, never a character — a unicode pin resolves to whatever system font carries it and comes
   out at an unpredictable size (see the trust strip). `icon-placeholder-obp` is the same glyph the
   address row below uses, and the only one of the four pin faces with no base ellipse, so it reads as
   a marker rather than a small illustration. Sized in px because it is a standalone mark.

   ⚠️ Coloured with --pts-accent-INK, like every other accent-hued glyph on this page
   (.pts-contact-icon, .pts-fact-icon): a glyph is painted with `color`, and the raw accent as a
   colour is banned outright (the yellow theme is 1.8:1 — see BookingHomeDesignsThemeColorTest). The
   shadow still mixes from the accent itself, which is a fill and may. */
.pts-map-pin{position:absolute;left:50%;top:50%;transform:translate(-50%,-64%);
  font-size:48px;line-height:1;color:var(--pts-accent-ink);
  filter:drop-shadow(0 8px 12px rgba(var(--pts-accent-rgb),.26))}
.pts-map-pin:before{margin:0;width:auto}
/* The callout: the panel's only real data, so it is what tells the visitor which place this is. */
.pts-map-note{position:absolute;left:18px;top:18px;max-width:calc(100% - 36px);
  display:flex;flex-direction:column;gap:2px;padding:11px 15px;border-radius:var(--pts-radius-sm);
  background:var(--pts-surface);box-shadow:0 10px 26px rgba(24,24,27,.13)}
.pts-map-note strong{font-size:13px;font-weight:800}
.pts-map-note span{font-size:12.5px;color:var(--pts-muted)}
/* Shell-prefixed for SPECIFICITY: `.pt-home-shell button{font:inherit}` is (0,1,1) and its `font`
   shorthand would reset a bare font-size on this control. */
.pt-home-shell .pts-map-open{position:absolute;right:16px;bottom:16px;display:inline-flex;align-items:center;
  gap:7px;padding:8px 14px;border:1px solid var(--pts-line);border-radius:999px;
  background:var(--pts-surface);color:var(--pts-accent-ink);font-size:12.5px;font-weight:700;
  box-shadow:0 8px 20px rgba(24,24,27,.1)}
/* ⚠️ THE WHOLE PANEL IS THE CLICK TARGET (`.pts-loc-map{cursor:pointer}` — the pill is the keyboard/AT
   path, not the only hit area), so the panel's hover has to drive the pill: over the CSS art the pointer
   was on something clickable that answered with nothing, and the one control that could answer only lit
   up in its own bottom-right corner. Same shape as `.pts-video:hover .pts-video-play`, and stated as one
   rule with the pill's own hover so the two can't drift. It is inert exactly where it should be — both
   `.pts-map-live` and `.pts-map-virtual` hide this pill, i.e. every state where the panel is not asking
   to be clicked. */
.pts-map-open:hover,
.pts-loc-map:hover .pts-map-open{border-color:var(--pts-accent);background:var(--pts-accent-soft)}
.pts-map-open i:before{margin:0;width:auto;line-height:1}
/* The VIRTUAL face: what the panel shows for a branch that is a meeting link rather than a place.
   Hidden by default and revealed by .pts-map-virtual, because a visitor switches between the two
   states as they pick branches — so both have to exist in the DOM at once (which is also why the
   view APPENDS the embed instead of replacing the panel's contents; see bindLocationMap).

   No map art behind it: art of a street grid under "Jitsi Meet" would be a picture of nowhere. The
   ground is the accent tint, opaque by construction (the accent mixed into the surface), so it reads
   as a deliberate panel rather than a box that failed to load. */
.pts-map-face{display:none}
.pts-loc-map.pts-map-virtual .pts-map-face{position:absolute;inset:0;z-index:2;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;
  padding:20px;text-align:center;background:var(--pts-accent-soft)}
/* Fontello, never a character — and `icon-video-meeting-obp` is the app's OWN virtual-location glyph
   (the classic mini-website's virtual card, locationsV3, approveBookings), not a new vocabulary.
   ⚠️ Painted with --pts-accent-INK like every other accent-hued glyph here: a glyph is `color`, and
   the raw accent as a colour is banned (see BookingHomeDesignsThemeColorTest). The `:before` reset
   undoes fontello's own width:1em + side margins, which would push it off the flex centre. */
.pts-map-face-icon{font-size:52px;line-height:1;color:var(--pts-accent-ink)}
.pts-map-face-icon:before{margin:0;width:auto}
/* The branch name, below the icon — the panel's only real data in this state, so it carries the
   weight the callout's name does elsewhere, one step up because it is the centrepiece. */
.pts-map-face strong{font-size:15px;font-weight:800;color:var(--pts-text)}
/* Loaded state: the embed fills the panel, covering the resting layers — which are HIDDEN here
   rather than removed, so a later pick of a virtual branch still has a face and a callout to show.
   ⚠️ (0,3,0) on purpose: `.pt-home-shell .pts-map-open` is (0,2,0) and would keep the pill on top of
   a loaded map. Same for the virtual state below, which additionally hides the frame — that rule
   out-ranks `.pts-loc-map iframe` (0,2,0) for the same reason. */
.pts-loc-map.pts-map-live .pts-map-art,
.pts-loc-map.pts-map-live .pts-map-note,
.pts-loc-map.pts-map-live .pts-map-open{display:none}
.pts-loc-map.pts-map-virtual .pts-map-art,
.pts-loc-map.pts-map-virtual .pts-map-note,
.pts-loc-map.pts-map-virtual .pts-map-open,
.pts-loc-map.pts-map-virtual iframe{display:none}
.pts-loc-map iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}
/* Nothing to click in either state: the map is already loaded, or there is no map to load. */
.pts-loc-map.pts-map-live,
.pts-loc-map.pts-map-virtual{cursor:default}
/* The follow row is pinned to the BOTTOM of the contact card, so the card's last line agrees with
   the map panel's bottom edge whenever the two happen to match in height. No border of its own:
   the contact row above it is no longer :last-child once this block exists, so it keeps its own
   hairline — adding a border-top here would draw that line twice. */
.pts-loc-contact{display:flex;flex-direction:column}
.pts-loc-follow{margin-top:auto;padding-top:17px}
.pts-loc-follow h4{font-size:14px;font-weight:800;margin-bottom:12px}
.pts-loc-socials{display:flex;flex-wrap:wrap;gap:9px}
/* Shell-prefixed for the same reason as .pts-map-open: these are <a>, and
   `.pt-home-shell a{color:inherit;font-size:inherit}` (0,1,1) outranks a bare class. */
.pt-home-shell .pts-loc-social{width:40px;height:40px;border-radius:var(--pts-radius-sm);
  border:1px solid var(--pts-line);background:var(--pts-surface);color:var(--pts-text);
  display:inline-flex;align-items:center;justify-content:center;font-size:15px;line-height:1}
.pt-home-shell .pts-loc-social:hover{border-color:var(--pts-accent);color:var(--pts-accent-ink);
  background:var(--pts-accent-soft)}
/* Same glyph-centring contract as .pts-social above: fontello ships `width:1em` + side margins and
   the faces differ in ascent, so the <i> is reset and centred by flex rather than by line-height. */
.pts-loc-social i{display:block;line-height:1;width:auto;margin:0}

/* ---------------------------------------------------------------------------------------------
   TWO OR MORE branches → the same map panel, driven by this list ([data-pts-branches], see
   buildBranchPicker). Replaces a row of per-branch cards, each carrying a name, an address and a
   directions link inside padding drawn for the far richer single-branch contact card — two
   locations rendered as two big, mostly empty panels.

   The card's own 23px padding is traded for per-row padding: the rows are the interactive things
   here, and a hover/selected fill has to reach the card's edges to read as a row rather than as an
   inset chip.

   ⚠️ Rows carry NO divider. They are separated by their own padding, and the selected one by its
   fill — the same call, for the same reason, as .pts-hours-row (see BookingHomeDesignsLocationGridTest).
   A hairline here needs suppression rules to go with it: on :last-child, and under the filled
   selected row, where a line cutting across the tint reads as a seam in the panel. That is why the
   border went rather than gaining exceptions.
   --------------------------------------------------------------------------------------------- */
.pts-loc-branches{padding:9px 11px;display:flex;flex-direction:column}
/* 🔑 The branch COUNT, above the rows. The list shows four and scrolls the rest behind a hidden
   scrollbar, so a seven-branch business read as "available in 4 locations" — this heading is what makes
   the window read as a window rather than as the whole list.
   ⚠️ Same type as `.pts-branch-contact h4` (the card's other heading) and the same 12px inset, so the two
   read as one kind of thing in one card; and NO bottom border — the rows below carry no dividers either
   (see the .pts-branch note above), and a rule here would also sit directly under the top fade the moment
   the list is scrolled. `flex:0 0 auto` so it can never be squeezed by the scroll box beside it. */
.pts-branch-head{flex:0 0 auto;padding:6px 12px 9px;font-size:14px;font-weight:800}
/* ⚠️ THE ROWS SCROLL; THE BUSINESS-CONTACT BLOCK DOES NOT — it is a sibling of this box, not inside it,
   so a visitor who scrolls to the last branch still has the phone number in front of them. That is the
   whole store-locator shape: a fixed panel, a scrolling result list, contacts pinned under it.

   `flex:0 1 auto` + `min-height:0`, never `1 1`: a growable box would eat the slack of a SHORT list and
   push the contact block to the card's bottom edge, away from the rows it follows. min-height:0 is the
   flex override that lets a scroll container actually shrink below its content.

   The scrollbar is hidden (`scrollbar-width` + the WebKit pseudo-element — both are needed, and this is
   the same treatment `.pts-nav-scroll` and `.pts-rail` already use, for the same reason: a bar drawn
   inside a bordered card reads as a broken panel). ⚠️ Unlike the pill row, this needs NO arrows: a
   VERTICAL list is scrolled by the wheel, the trackpad and the keyboard already — the arrows exist up
   there only because a horizontal row can strand a mouse. The affordance here is the fade plus the row
   it cuts through. Rows stay real <button>s, so tabbing to one below the fold scrolls it into view for
   free; bindBranchScroll() only has to handle the SELECTED row on first paint (the default is the first
   MAPPABLE branch, which is not necessarily the first row). */
/* ⚠️ `scroll-behavior:auto` UNDOES A LEAK, it is not a default — newbookingpage.css opens with a
   universal `*{…scroll-behavior:smooth}` (same class of leak as its bare `section{padding:12px 0}` and
   `a{font-size:16px}`), so every programmatic scroll of this box would ANIMATE: the list would visibly
   slide on first paint when bindBranchScroll() reveals the selected row, and the sync() right after it
   would read the pre-animation offset and paint the wrong fade. Wheel/trackpad scrolling is unaffected
   either way — `scroll-behavior` only governs programmatic and anchor scrolls. (The pill row opts INTO
   smooth deliberately, which is why it states it itself.) */
.pts-branch-scroll{flex:0 1 auto;min-height:0;max-height:var(--pts-branch-max);overflow-y:auto;
  scroll-behavior:auto;
  scrollbar-width:none;--pts-branch-fade-t:0px;--pts-branch-fade-b:0px;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 var(--pts-branch-fade-t),
    #000 calc(100% - var(--pts-branch-fade-b)),transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 var(--pts-branch-fade-t),
    #000 calc(100% - var(--pts-branch-fade-b)),transparent 100%)}
.pts-branch-scroll::-webkit-scrollbar{display:none}
/* Each edge fades only while there is something that way, exactly like the nav row's two fade tokens —
   an unconditional fade dims the first and last rows when the list does not scroll at all.
   ⚠️ The two are DELIBERATELY UNEQUAL, because the two edges cut differently. The top edge only exists
   once the list has been scrolled, so it always crosses a row's CONTENT: 28px there leaves the name
   legible while stating plainly that it is cut. The bottom edge, at rest, sits flush with the 4th row's
   own bottom padding (13px — see fitBranchRows), so a 28px fade would wash out that row's "Get
   directions" and make a WHOLE row look cut, which is the very thing showing four whole rows exists to
   avoid. 14px keeps the fade inside that padding at rest and still softens the edge mid-scroll. */
.pts-loc-branches.pts-can-up .pts-branch-scroll{--pts-branch-fade-t:28px}
.pts-loc-branches.pts-can-down .pts-branch-scroll{--pts-branch-fade-b:14px}
/* 🔑 One height for every row (--pts-branch-min above), and the COLUMN is what spends the slack: a
   two-line row centres in it, so its extra air is symmetric (~12px on top of the row's own 13px padding
   either side) instead of 25px of hollow space under the note, which reads as exactly the missing line it
   is. A three-line row's content IS the minimum, so nothing about a physical row moves. */
.pts-branch{border-radius:var(--pts-radius-sm);min-height:var(--pts-branch-min);
  display:flex;flex-direction:column;justify-content:center}
/* ⚠️ Hover and selection are painted on the ROW, never on the pick button. A branch is a button (name +
   address) PLUS an actions line — a link inside a button is invalid markup, so they have to be siblings
   — and a fill on the button alone stops at the address, cutting the row in half right above its own
   "Get directions" and reading as a stray highlighted chip rather than a selected row.
   The plain (unmappable) rows are excluded by class rather than by :has(), which is newer than the CSS
   Grid this stylesheet already refuses to rely on: a hover fill on a row that cannot be picked promises
   an interaction that does not exist. */
.pts-branch:not(.pts-branch-plain):hover{background:var(--pts-surface-2)}
/* Selected: an accent-tinted row plus a filled mark. --pts-accent is used as a FILL only (as text it
   is 1.8:1 on the yellow theme — see BookingHomeDesignsThemeColorTest); the copy keeps its own ink.
   Restated for :hover so the selection does not flip to the grey hover tint under the pointer. */
.pt-home-shell .pts-branch-on,
.pt-home-shell .pts-branch-on:hover{background:var(--pts-accent-soft)}
/* Shell-prefixed: `.pt-home-shell button{font:inherit}` is (0,1,1) and its `font` shorthand would
   reset a bare font-size here (same trap as .pts-map-open). `width:100%` + `text-align:left` undo
   the button defaults so the row reads as a row, not as a control with a label. */
.pt-home-shell .pts-branch-pick,
.pts-branch-row{display:flex;align-items:flex-start;gap:11px;width:100%;padding:13px 12px 6px;
  background:none;border:0;text-align:left}
.pt-home-shell .pts-branch-pick{cursor:pointer}
/* The 6px above leaves room for the actions line; a branch with no links (no address, no phone, no
   email) has none, so it pays the row's own bottom padding instead of ending 7px short. */
.pts-branch-pick:last-child,
.pts-branch-row:last-child{padding-bottom:13px}
/* The mark is the "which one is on screen" signal, so it must survive the row's own hover fill: a
   ring by default, the accent filled in when selected. Nudged down 3px to sit on the name's line. */
.pts-branch-mark{flex:0 0 auto;width:14px;height:14px;margin-top:3px;border-radius:50%;
  border:2px solid var(--pts-line);background:var(--pts-surface)}
.pts-branch-on .pts-branch-mark{border-color:var(--pts-accent);background:var(--pts-accent);
  box-shadow:inset 0 0 0 2.5px var(--pts-surface)}
.pts-branch-copy{min-width:0;display:flex;flex-direction:column;gap:2px}
.pts-branch-copy strong{font-size:14px}
.pts-branch-copy span{font-size:12.5px;color:var(--pts-muted)}
/* The actions line, indented to the copy column (12px row inset + 14px mark + 11px gap) so name,
   address and links read as one left-aligned block rather than three starts. */
.pts-branch-actions{display:flex;flex-wrap:wrap;gap:15px;padding:0 12px 13px 37px}
/* Shell-prefixed like every other link rule on this page: `.pt-home-shell a{color:inherit;
   font-size:inherit}` (0,1,1) out-ranks a bare class. */
.pt-home-shell .pts-branch-link{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;
  font-weight:600;color:var(--pts-accent-ink)}
.pts-branch-link:hover{text-decoration:underline}
/* Same glyph-centring contract as .pts-loc-social / .pts-fact-icon: fontello ships width:1em plus
   side margins, which throw a small inline icon off its label. */
.pts-branch-link i{line-height:1;width:auto;margin:0}
/* The business's contact details, closing the list card (phone/email/website + social icons). The rule
   above gives every branch a hairline, so this block is separated by its own top border and a little
   air rather than a second divider style. Its rows are the SHARED .pts-contact-row / .pts-contact-icon
   pair the single-branch card uses, so the two layouts print a phone number identically. */
.pts-branch-contact{margin-top:5px;padding:15px 12px 4px;border-top:1px solid var(--pts-line)}
.pts-branch-contact h4{font-size:14px;font-weight:800;margin-bottom:6px}
/* The icon row keeps the hairline of the contact row above it (that row is no longer :last-child once
   this block exists) — exactly how .pts-loc-follow sits under the single-branch card's rows. */
.pts-branch-contact .pts-loc-socials{margin-top:13px}

/* #pts-hours is a scroll target of its own (the "Working Hours" pill), separate from #pts-location's
   top — same 148px clearance as .pts-section/.pts-group so it lands below the sticky header+nav. */
#pts-hours{scroll-margin-top:148px}
/* The hours card is the LAST cell of .pts-loc, so it lands alone on its own row whenever the business
   has an even number of locations (2 locations fill row 1; hours starts row 2). Half a row of card
   beside an empty column reads as broken and needlessly cramps the day | hours | status rows, so in
   that case it takes the full measure. `:nth-child(odd)` IS the "alone on its row" test: the card is
   child n+1, odd exactly when the location count n is even — parity, not a guess at the count. It does
   assume this flex line holds only location cards plus this one, so re-check it if a third card type is
   ever added. When it DOES share a line (1 or 3 locations) it stays a normal half-width card. */
#pts-hours:nth-child(odd){flex-basis:100%}
.pts-hours{display:block}
.pts-hours-row + .pts-hours-row{margin-top:1px}
/* A full-width card USES the width instead of stretching into it. Across the whole page measure each of
   the 7 rows left a ~190px void between its range and its status pill, and the card read as tall and
   sparse — "wide with too little content". Two columns (Mon-Thu | Fri-Sun) halve the height and put every
   row back at a comfortable ~530px measure.
   CSS multi-column, not a flex row of two lists: a schedule is scanned vertically, so each column must
   read top-to-bottom (Mon-Thu then Fri-Sun), and `columns` is the only mechanism that flows block-level
   rows down one column and on to the next without splitting the week into two hand-built containers.
   (`flex-wrap` fills across, not down — it would zig-zag the week: Mon | Tue / Wed | Thu.)
   Scoped to :nth-child(odd) — the same "alone on its line" test as the flex-basis rule above — so a
   half-width card (odd number of locations) keeps its single column. */
#pts-hours:nth-child(odd) .pts-hours{columns:2;column-gap:34px}
/* A row must not be split across the column break. */
.pts-hours-row{break-inside:avoid;-webkit-column-break-inside:avoid}
/* day | range | status on one flex line, the pill pushed to the far edge by `margin-left:auto`, so the
   badges line up regardless of range-text length.
   NO row divider by design: the status pill already terminates every row, and in the two-column state the
   rules that managed the border (clear it on the last row, on today's filled row, and on the bottom row of
   the left column) outnumbered the border itself. Rows are separated by their own padding + the 1px
   spacing below, and today is marked by its fill. */
.pts-hours-row{display:flex;align-items:center;
  gap:12px;padding:11px 2px;font-size:13.5px;border-radius:12px}
.pts-hours-row strong{font-weight:700;flex:0 0 auto;min-width:84px}
/* The range takes the slack and the pill is pushed to the far edge, which is what the old
   `minmax(0,1fr) auto` pair did; min-width:0 keeps a long range from refusing to shrink. */
.pts-hours-row span:not(.pts-hours-badge){flex:1 1 auto;min-width:0}
.pts-hours-row .pts-hours-badge{flex:0 0 auto;margin-left:auto}
/* The status pill is ALSO a <span> in this row, so both of these must exclude it — otherwise the row
   wins on specificity (0-2-1 / 0-3-1 vs the pill's 0-1-0) and repaints the pill's text AND its
   currentColor dot: grey on a plain row, ACCENT PURPLE on today's. `:not()` fixes it by matching,
   not by specificity, so the pill's own semantic ink applies on every row. */
.pts-hours-row span:not(.pts-hours-badge){color:var(--pts-muted)}
/* Today gets a soft fill + ink text, not just bold — a weight change alone was easy to miss scanning
   the list. The negative margin bleeds the fill to the card's own edges (23px padding, well clear of
   the 10px inset here) without widening the row past its container. */
.pts-hours-row.pts-today{background:var(--pts-accent-soft);margin:0 -10px;padding-left:12px;padding-right:12px}
.pts-hours-row.pts-today strong,
.pts-hours-row.pts-today span:not(.pts-hours-badge){color:var(--pts-accent-ink);font-weight:800}
/* Status pill: soft same-hue fill + strong same-hue ink + a matching dot. Two deliberate details:
   · The fill is mixed with --pts-SURFACE, not with `transparent`. A translucent fill picks up
     whatever is behind it, so on the accent-filled `.pts-today` row the green/red tint blended with
     the purple and went muddy; mixing with the surface keeps every pill a flat, opaque pastel that
     reads the same on a plain row and on today's. It also keeps the pills theme-aware for free —
     under [data-pts-theme="dark"] the same mix yields a dark tint behind the bright ink, where a
     hard-coded pastel hex would have left light-on-light.
   · Sized to the row (12.5px), not to a standalone chip: pill type must stay under the row's own
     text or the status shouts louder than the hours it annotates. */
.pts-hours-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;
  font-size:12.5px;font-weight:800;white-space:nowrap}
.pts-hours-badge::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor;flex:0 0 auto}
.pts-badge-open{background:var(--pts-badge-open-bg);color:var(--pts-success)}
.pts-badge-closed{background:var(--pts-badge-closed-bg);color:var(--pts-danger)}
/* Today's row sits on --pts-accent-soft, so its pills carry a touch more tint to hold their own. */
.pts-hours-row.pts-today .pts-badge-open{background:var(--pts-badge-open-today-bg)}
.pts-hours-row.pts-today .pts-badge-closed{background:var(--pts-badge-closed-today-bg)}
.pts-empty{background:var(--pts-surface);border:1px dashed var(--pts-line);border-radius:var(--pts-radius);
  padding:26px;text-align:center;color:var(--pts-muted);font-size:14px}
/* ---------- FAQs ----------
   A ruled list, not a stack of cards: each question is a row separated by a hairline, the way a policies
   section reads on a page rather than in a widget. One <details> per question, so the browser owns the
   open/close state (no JS, keyboard-accessible, and the answers stay in the DOM for crawlers); the marker
   is drawn by us because the native triangle differs per browser and cannot be styled consistently. */
.pts-faqs{display:block;border-top:1px solid var(--pts-line)}
.pts-faq{border-bottom:1px solid var(--pts-line)}
.pts-faq summary{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;
  padding:22px 0;font-size:17px;font-weight:700;line-height:1.4;color:var(--pts-text);
  cursor:pointer;list-style:none}
.pts-faq summary::-webkit-details-marker{display:none}
/* An answer belongs to the question above it, so the space INSIDE a row must be clearly smaller than the
   row's own top/bottom air — otherwise proximity groups nothing and the answer reads as detached from its
   own question. The summary's padding is what separates the two, and symmetric 24px made the two gaps
   nearly equal (ink-to-ink 29.8px inside a row against 56.5px between rows). So an OPEN row hands its
   bottom air to the paragraph: 8px inside (13px ink-to-ink, the answer's leading adds the rest) against
   ~52px between rows. Closed rows keep the symmetric padding — there is nothing to group. */
.pts-faq[open] summary{padding-bottom:8px}
/* No hover colour on the question: the whole row is the control, and recolouring a 17px heading on
   pointer-over reads as a link into somewhere else. The pointer cursor is the affordance. */
/* A thin +/− in the row's own colour — deliberately quiet: the question is the thing being read. */
.pts-faq-mark{position:relative;flex:0 0 auto;width:15px;height:15px;margin-top:5px}
.pts-faq-mark::before,
.pts-faq-mark::after{content:"";position:absolute;top:50%;left:0;width:15px;height:1.5px;border-radius:2px;
  background:currentColor;opacity:.55;transform:translateY(-50%);transition:transform .18s ease,opacity .18s ease}
.pts-faq-mark::after{transform:translateY(-50%) rotate(90deg)}
.pts-faq[open] .pts-faq-mark::after{transform:translateY(-50%) rotate(0);opacity:0}
/* line-height 1.6, not 1.7: at 1.7 this was the loosest paragraph on the page (the shell's base is 1.5,
   and nothing else sets its own), so a two-line answer stood 3px taller per line than the same 15px copy
   everywhere else and the whole block read airier than its neighbours. 1.6 is the half-step a long-form
   answer earns over UI copy; going past it makes the list look loose again.
   ⚠️ AND THE ANSWER TAKES THE ROW'S FULL WIDTH — no reading measure of its own. It carried
   `max-width:78ch`, which computes to ~651px, so in the five block designs (page measure 1120px)
   every answer stopped at 58% of a row whose hairline and question span the whole width — a ruled
   list is read as rows, so an answer ending 470px short of its own rule reads as a broken cell, not
   as a considered measure. It is also the only `ch` value in this stylesheet: `.pts-panel p` is the
   page's other block of long-form merchant prose — same 15px/1.6/pre-line/muted treatment — and is
   bounded by its container, so the cap made one of two identical paragraph styles wrap early. If a
   measure is ever wanted back, it belongs on BOTH and it must be stated in px against the page
   measure, not in `ch` (which drifts with the rendered font — this page falls back to the generic
   sans-serif, see the type-scale notes). */
.pts-faq p{margin:0;padding:0 0 24px;color:var(--pts-muted);font-size:15px;line-height:1.6;
  white-space:pre-line}

/* ---------- rich footer ----------
   Brand block + link columns on one wrapping flex line, then the existing credit bar underneath. The
   brand block grows and the columns are fixed-basis, so they stay aligned as a group rather than each
   stretching to a different width.
   🔑 THE COLUMNS ARE A GROUP AT EVERY WIDTH — they never peel off one at a time. This desktop row holds
   only while all four items fit on one line (≥942px); below that the brand takes a line of its own and
   the columns become equal thirds under it (≤941 block), then one per line on a phone (≤470). A wrapped
   column here would land a BRAND-HEIGHT below the ones that stayed — the brand block is four or five
   rows tall — so "one at a time" put a stray heading under a finished block. See the ≤941 block. */
.pts-footer-rich .pts-footer-top{display:flex;flex-wrap:wrap;gap:34px;
  width:min(var(--pts-page),calc(100% - 36px));margin:0 auto;padding:54px 0 40px}
/* A COLUMN, not a wrapping row: the description belongs under the mark, and in a row it would not stay
   there — a paragraph with a `max-width` has that cap as its flex basis, so `flex-basis:100%` stops
   reserving a line and the text sits beside the logo (the same clamp that once put the split rail on the
   banner's line). The mark and the name are a lockup so they still sit side by side. */
.pts-footer-brand{flex:1 1 300px;min-width:0;display:flex;flex-direction:column;align-items:flex-start;
  gap:14px}
/* Same gap as the header's .pts-brand — see --pts-brand-gap. It is the same logo twice on one page. */
.pts-footer-lockup{display:inline-flex;align-items:center;gap:var(--pts-brand-gap)}
.pts-footer-mark{flex:0 0 auto;width:38px;height:38px;border-radius:6px;overflow:hidden;
  display:flex;align-items:center;justify-content:center;background:var(--pts-surface)}
.pts-footer-mark img{width:100%;height:100%;object-fit:cover}
.pts-footer-name{font-size:17px;font-weight:800}
.pts-footer-brand p{margin:0;max-width:380px;font-size:14px;color:var(--pts-muted)}
.pts-footer-brand .pts-lang{background:var(--pts-surface-2);border:1px solid var(--pts-line)}
.pts-footer-brand .pts-lang:hover{border-color:var(--pts-accent);color:var(--pts-text)}
/* The classic footer's product credit, in this block instead of the bottom bar: the heart line, then the
   two product lines. Sizes sit on this page's content scale (13 primary / 12 caption) and the links MUST
   carry the .pt-home-shell prefix — newbookingpage.css leaks a bare `a{font-size:16px}` that the shell
   only neutralises with `.pt-home-shell a{font-size:inherit}` (0,1,1), so an unprefixed rule loses. */
/* ⚠️ align-items:flex-start, not the flex default `stretch`: every line here is a LINK, and stretched
   items all take the widest line's width — so "Made with ♥ by Picktime" carried ~88px of invisible
   clickable space to the right of its own text, where a click still navigated off-page. */
/* The top margin is ON PURPOSE and belongs here, not on the block's `gap`: the brand block runs on a 14px
   rhythm (lockup → description → socials) and the credit is a separate, quieter cluster on a 5px rhythm of
   its own, so at a flat 14px it read as a fourth row of the same list. 28px more (42px clear of the
   socials) sets it apart without needing a rule line; raising the block's gap instead would loosen all
   three rows with it. */
.pts-footer-credit{display:flex;flex-direction:column;align-items:flex-start;gap:5px;margin-top:28px}
.pt-home-shell .pts-footer-credit a{font-size:12px;color:var(--pts-muted)}
.pt-home-shell .pts-footer-credit a.pts-footer-made{font-size:13px;color:var(--pts-text)}
.pt-home-shell .pts-footer-credit a:hover,
.pt-home-shell .pts-footer-credit a.pts-footer-made:hover{color:var(--pts-accent-ink)}
.pts-footer-credit strong{font-weight:700}
/* ⚠️ Picktime's heart is a FIXED brand red (--pts-heart, derived from the classic's #fb3957 — see the
   token), never the account accent: the mark is the product's, not the merchant's. Sized in em so it
   tracks whatever line hosts it, with fontello's 1em box + .2em side margins reset to 3px. A solid glyph
   must sit BELOW the cap height around it (.76em → ink 8.25px against a 9.5px cap) and be centred on it
   (vertical-align:.13em → 0.12px off at 4× device scale); the two numbers are one calibration. */
.pts-footer-heart{color:var(--pts-heart)}
.pts-footer-heart:before{width:auto;margin:0 3px;font-size:.76em;vertical-align:.13em}
.pts-footer-col{flex:0 0 var(--pts-footer-col,168px);display:flex;flex-direction:column;gap:11px}
.pts-footer-col h3{margin:0 0 3px;font-size:14px;font-weight:800}
.pt-home-shell .pts-footer-col a{font-size:14px;color:var(--pts-muted)}
.pt-home-shell .pts-footer-col a:hover{color:var(--pts-accent-ink)}
.pts-footer-rich .pts-footer-inner{border-top:1px solid var(--pts-line);padding-top:20px}
.pts-footer-legal{display:inline-flex;flex-wrap:wrap;align-items:center;gap:5px}
/* ⚠️ The bottom padding is the FLOATING PILL'S RESTING PLACE, and it is DERIVED, not chosen. At the end of
   the page the fixed .pts-scroll-cta lands in the strip below .pts-footer-inner's hairline, so that strip
   has to be the pill plus its own offset on BOTH sides, or the pill reads as stuck to the hairline:
        strip  = hairline (1) + footer-inner padding-top (20) + copyright line (18) = 39, + this padding
        wanted = --pts-cta-h + 2 x --pts-cta-gap
   Hence the calc: it stays correct when either token moves, which is the point — hard-coded numbers here
   have been wrong twice (74px → 49px of air above the pill and 11px below, the page reading as refusing to
   end; 30px → 5px above, the pill touching the line; 44px → 18px above, from measuring the pill without
   its `pts-btn-small` button). Anything added to the RIGHT of .pts-footer-inner would sit under the pill.
   🔑 A WHITE-LABELED page has no bottom bar at all (the template drops .pts-footer-inner when the model
   sends no copyright line — the bar's whole content is Picktime's), so that 39px strip is not there. This
   padding is deliberately NOT re-derived for that case: .pts-footer-top's own 40px bottom padding stands
   in for it almost exactly — measured at 1440px with the real markup and the full stylesheet set, ~12px of
   air above the pill and ~11px below, against 11px/11px on a branded page. So the 40px in .pts-footer-top
   above is load-bearing twice; drop it and the white-labeled page's pill lands on the footer columns.
   ⚠️ padding-TOP is 0 on purpose: .pts-footer-top owns the space above the rich block (54px), and stacking
   the band's own 30px on top of it put 85px above the first line against 40px below the last — the band
   looked top-heavy and hollow. The non-rich fallback below keeps that case from hugging the border. */
.pts-footer{background:var(--pts-surface);border-top:1px solid var(--pts-line);
  padding:0 0 calc(var(--pts-cta-h) + var(--pts-cta-gap) * 2 - 39px)}
.pts-footer:not(.pts-footer-rich){padding-top:30px}
.pts-footer-inner{width:min(var(--pts-page),calc(100% - 36px));margin:auto;display:flex;flex-wrap:wrap;
  justify-content:space-between;gap:20px;font-size:12px;color:var(--pts-muted)}
.pts-footer-inner strong{color:var(--pts-text)}
.pts-scroll-cta{position:fixed;right:12px;bottom:var(--pts-cta-gap);z-index:76;display:flex;align-items:center;gap:13px;
  padding:8px 8px 8px 15px;border:1px solid var(--pts-line);border-radius:999px;
  background:rgba(var(--pts-surface-rgb),.92);box-shadow:0 15px 44px rgba(20,15,30,.2);
  -webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);
  opacity:0;transform:translateY(16px);pointer-events:none;transition:.22s ease}
.pts-scroll-cta.pts-visible{opacity:1;transform:none;pointer-events:auto}
.pts-scroll-cta strong{display:block;font-size:13px}
.pts-scroll-cta span{display:block;color:var(--pts-muted);font-size:11px}
.pts-mobile-cta{display:none;position:fixed;z-index:75;left:10px;right:10px;bottom:10px;padding:9px;
  border:1px solid var(--pts-line);border-radius:8px;background:rgba(var(--pts-surface-rgb),.94);
  box-shadow:0 14px 40px rgba(0,0,0,.2);
  -webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);
  align-items:center;justify-content:space-between;gap:12px}
/* The copy block gives way, never the pill: the bar is the page's last-resort CTA, so "Book now" must keep
   its whole label (`.pts-btn` is already nowrap) and the business name is what truncates. Without
   `min-width:0` a flex item refuses to shrink below its content, so a long business name wrapped to two
   lines and grew this fixed bar from 59px to 82px at every phone width — 23px of a 740px screen, permanently,
   for a name the identity card states in full a screen above. */
.pts-mobile-cta > div{min-width:0}
.pts-mobile-cta strong{display:block;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pts-mobile-cta span{display:block;font-size:11px;color:var(--pts-muted)}
.pts-mobile-cta .pts-btn{flex:0 0 auto}

/* --------------------------------------------------------------------------
   Cookie consent notice.

   Same corner as every other Picktime booking surface: fixed BOTTOM-LEFT, which is where
   newbookingpage.css puts the classic one (`.cookie-consent-section > div`: bottom 70px, left 10px,
   max-width 350px). That is the opposite corner from the floating "Ready when you are" pill, so the
   two can never meet on a desktop; the phone case is handled in the ≤720 block, where the full-width
   .pts-mobile-cta owns the bottom edge.

   ⚠️ Radius and shadow are stated LITERALLY rather than taken from --pts-radius/--pts-shadow, for the
   same reason .pts-mobile-cta states its own: this is floating chrome laid over arbitrary page
   content and it has to keep its separation in EVERY design — `directory` sets --pts-shadow:none and
   an 8px radius, which would leave the notice reading as a slab of page text rather than an overlay.

   Hidden until bindCookieNotice() has read the `ptconsent` cookie, so a visitor who already consented
   never sees a flash of it; dismissing REMOVES the node rather than fading it — a fixed box that is
   invisible but still in layout is what gave this page a phantom horizontal scrollbar once already
   (see .pts-share-menu). */
.pts-cookie{display:none;position:fixed;z-index:74;left:12px;bottom:var(--pts-cta-gap);
  width:min(360px,calc(100vw - 24px));align-items:flex-start;gap:10px;padding:10px 11px;
  border:1px solid var(--pts-line);border-radius:6px;
  background:rgba(var(--pts-surface-rgb),.94);
  box-shadow:0 14px 40px rgba(0,0,0,.16);
  -webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}
.pts-cookie.pts-visible{display:flex}
.pts-cookie p{margin:0;font-size:12px;line-height:1.5;color:var(--pts-muted)}
/* The shell resets links to `color:inherit` at (0,1,1) — see the .pts-rating note — so the accent has
   to be stated at (0,2,1) or the policy link renders as body text with an underline. */
.pt-home-shell .pts-cookie a{color:var(--pts-accent-ink)}
.pt-home-shell .pts-cookie-close{flex:0 0 auto;display:flex;align-items:center;justify-content:center;
  width:13px;height:16px;padding:0;border: none;
  background:var(--pts-surface);color:#000000;font-size:9px;font-weight: 700;line-height:1;cursor:pointer}
/* fontello ships every glyph with width:1em + .2em side margins, which push it off the flex centre. */
.pt-home-shell .pts-cookie-close:before{width:auto;margin:0}
.pt-home-shell .pts-cookie-close:hover{color:var(--pts-text);border-color:var(--pts-text)}

/* ==========================================================================
   DESIGN VARIANTS
   ========================================================================== */

/* ---------- showcase: large visual hero, glassy identity card, 2-up + wide lead tile ---------- */
.pt-home-shell[data-pts-design="showcase"]{--pts-radius:12px;--pts-radius-sm:12px;--pts-section:78px;
  --pts-overlap:-40px;--pts-tiles:2;--pts-shadow:0 24px 70px rgba(36,21,52,.12)}
.pt-home-shell[data-pts-design="showcase"] .pts-header{background:rgba(var(--pts-surface-rgb),.9);
  box-shadow:0 8px 28px rgba(25,19,34,.05)}
.pt-home-shell[data-pts-design="showcase"] .pts-cover::after{
  background:linear-gradient(90deg,rgba(8,7,12,.68),rgba(8,7,12,.08) 70%)}
.pt-home-shell[data-pts-design="showcase"] .pts-hero-msg{display:block}
.pt-home-shell[data-pts-design="showcase"] .pts-card{padding:25px;
  -webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);
  background:rgba(var(--pts-surface-rgb),.89)}
.pt-home-shell[data-pts-design="showcase"] .pts-tile{--pts-tile-h:230px;padding:24px}
.pt-home-shell[data-pts-design="showcase"] .pts-tiles .pts-tile:nth-child(3n+1){flex-basis:100%}
/* ⚠️ The lead/pair rhythm has to CLOSE ITSELF, because a categorised catalogue gives each category its own
   `.pts-tiles` and `nth-child` restarts inside it — so the count is now arbitrary rather than the whole
   group's. A category of `3n+2` items ends on a single half-width tile beside dead space (2 items = a
   full-width lead plus one lonely half, which reads as a mistake rather than as a grid gap, since the tile
   above it spans). The last tile in that position spans instead. This equally fixes an uncategorised group
   whose total is 3n+2 — the same orphan was always there, just rarer. Reset in the responsive block below,
   where the lead tile stops spanning and every tile is an ordinary column again. */
.pt-home-shell[data-pts-design="showcase"] .pts-tiles .pts-tile:last-child:nth-child(3n+2){flex-basis:100%}

/* ---------- directory: dense, list-first, minimal radii and shadows ---------- */
.pt-home-shell[data-pts-design="directory"]{--pts-radius:12px;--pts-radius-sm:12px;--pts-section:48px;
  --pts-overlap:-38px;--pts-tiles:1;--pts-rows:1;--pts-shadow:none;
  /* Category rhythm for a dense one-column listing (this design lists categories too — see
     categorisesGroups). Every item here is a full-width bordered row, so the header's hairline can read as
     just one more row edge; the fix is rhythm, not a fill — a filled band above bordered rows reads as one
     more row. MORE air above the header (34 vs 24) separates it from the previous category's last row, and
     LESS below (10 vs 14) binds it to the items it introduces: at the shared 14px the header sat exactly
     one card-gap from its first row, i.e. equidistant from both categories. Tokens, never restated
     margins — see the note beside their defaults. */
  --pts-cat-gap:34px;--pts-cat-lead:10px}
/* ⚠️ The `none` reset must be stated in BOTH forms too. `.pts-header` sets -webkit- and unprefixed blur,
   so resetting only the unprefixed one leaves this design's header still blurred on every Safari below 18
   — i.e. adding the prefix up there would have QUIETLY BROKEN the one design that opts out of the glass. */
.pt-home-shell[data-pts-design="directory"] .pts-header{background:var(--pts-surface);
  -webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:none}
.pt-home-shell[data-pts-design="directory"] .pts-card{padding:15px 17px;box-shadow:0 5px 20px rgba(0,0,0,.06)}
.pt-home-shell[data-pts-design="directory"] .pts-logo{width:76px;height:76px;border-radius:10px;font-size:21px}
.pt-home-shell[data-pts-design="directory"] .pts-title{font-size:27px}
.pt-home-shell[data-pts-design="directory"] .pts-facts{box-shadow:none}
/* tiles collapse into horizontal rows */
.pt-home-shell[data-pts-design="directory"] .pts-tile{--pts-tile-h:94px;display:flex;flex-direction:row;padding:13px;
  gap:13px;align-items:center}
.pt-home-shell[data-pts-design="directory"] .pts-tile > *{flex:0 0 auto}
/* 🔑 `flex:1 1 0`, never `1 1 auto` — the same zero-basis rule as `.pts-row`'s text column above, and it
   matters here from 720px DOWN (the ≤720 block wraps this tile so its meta and CTA take their own indented
   lines): `auto` = max-content = the nowrap name on one line, so a long service/resource name dropped the
   whole text block below the icon while a short one sat beside it. */
.pt-home-shell[data-pts-design="directory"] .pts-tile > div:not(.pts-tile-icon):not(.pts-tile-meta){flex:1 1 0;min-width:0}
.pt-home-shell[data-pts-design="directory"] .pts-tile-icon{margin:0;width:58px;height:58px}
.pt-home-shell[data-pts-design="directory"] .pts-desc-row{margin:3px 0 0}
/* Same reason as the CTA's reset below: the base fold button's `margin-bottom` is the air before the
   COLUMN tile's meta row, and in this centred row layout it only pushes the text block off centre. */
.pt-home-shell[data-pts-design="directory"] .pts-tile-more{margin:3px 0 0}
.pt-home-shell[data-pts-design="directory"] .pts-tile-meta{border:0;padding:0;margin:0;display:block;text-align:right}
.pt-home-shell[data-pts-design="directory"] .pts-price{display:block;margin:0}
/* Book now — the dense row keeps its CTA, as its OWN column at the end (icon | text | meta | cta), so
   showing it costs no row height. Both were `display:none` here for density, which left every service,
   class and resource in this design with the invisible .pts-tile-link overlay as their only affordance:
   a row carrying a duration and a price but no CTA reads as a listing rather than something bookable,
   and it was the one design where the page never said "Book now" at the item level.
   ⚠️ The tile CTA's `margin-top:12px` must be reset — it is the air above the label in the COLUMN tile
   every other design renders; in this row layout it pushes the label off the row's centre line. */
.pt-home-shell[data-pts-design="directory"] .pts-tile-cta{margin-top:0;white-space:nowrap}

/* ---------- soft: rounded and spacious, hero inside a rounded frame ---------- */
.pt-home-shell[data-pts-design="soft"]{--pts-radius:12px;--pts-radius-sm:12px;--pts-section:72px;
  --pts-overlap:-40px;--pts-tiles:2;--pts-shadow:0 20px 56px rgba(68,47,91,.09)}
.pt-home-shell[data-pts-design="soft"] .pts-header{background:var(--pts-header-tint);
  border-bottom-color:var(--pts-edge-12)}
/* max-width, not width: a definite width blocks the ratio's height→width transfer, so on a short
   window the box would keep this width, overshoot --pts-cover-max and letterbox the banner instead
   of scaling it down. Leaving the width auto lets it narrow (and the auto margins re-centre it). */
.pt-home-shell[data-pts-design="soft"] .pts-cover{max-width:min(calc(var(--pts-page) + 90px),calc(100% - 28px));
  margin:12px auto 0;border-radius:12px}
.pt-home-shell[data-pts-design="soft"] .pts-card{padding:25px;
  border-color:var(--pts-edge-14)}
.pt-home-shell[data-pts-design="soft"] .pts-facts{border:0;padding:5px}
.pt-home-shell[data-pts-design="soft"] .pts-fact{border:0;border-radius:12px}
.pt-home-shell[data-pts-design="soft"] .pts-fact:hover{background:var(--pts-surface-2)}
.pt-home-shell[data-pts-design="soft"] .pts-nav{border:0}
/* …and with it the at-rest line: `border:0` was the whole of soft's statement when the bar owned the
   only hairline, so without this the one design that deliberately has no rule under its pills grows
   one at rest and loses it again on stick. */
.pt-home-shell[data-pts-design="soft"] .pts-nav::after{display:none}
.pt-home-shell[data-pts-design="soft"] .pts-tile,
.pt-home-shell[data-pts-design="soft"] .pts-panel{padding:23px}
.pt-home-shell[data-pts-design="soft"] .pts-tile{--pts-tile-h:228px}

/* ---------- spotlight: hero-led, flat identity row, 4-up tiles ---------- */
.pt-home-shell[data-pts-design="spotlight"]{--pts-radius:12px;--pts-radius-sm:12px;--pts-section:58px;
  --pts-overlap:0px;--pts-tiles:4;--pts-shadow:0 16px 45px rgba(24,24,27,.08)}
.pt-home-shell[data-pts-design="spotlight"] .pts-header{background:var(--pts-surface);
  box-shadow:0 7px 25px rgba(20,16,28,.05)}
.pt-home-shell[data-pts-design="spotlight"] .pts-hero-msg{display:block;max-width:520px}
.pt-home-shell[data-pts-design="spotlight"] .pts-hero-title{font-size:54px}
.pt-home-shell[data-pts-design="spotlight"] .pts-rail{padding-top:24px}
.pt-home-shell[data-pts-design="spotlight"] .pts-card{box-shadow:none;border-width:0 0 1px;border-radius:0;
  padding:18px 0;background:transparent}
.pt-home-shell[data-pts-design="spotlight"] .pts-facts{box-shadow:none;border-radius:12px}
.pt-home-shell[data-pts-design="spotlight"] .pts-tile{--pts-tile-h:218px}

/* ---------- split + splitwide: sticky identity rail beside a scrolling content column ----------
   THE SHELL IS FLEX, wrapping into lines: header · page-wide banner · (rail + content column) · footer.
   Each full-width band is `flex:0 0 100%`; the rail and the content column then share one line, centred
   by justify-content (the leftover space is the page gutter). The content column is a real element —
   `.pts-col-main` in the template — because the sticky pill row has to sit OVER main, and two separate
   flex items on the same line cannot stack; wrapping them also gives the sticky nav the tall containing
   block it needs. Everything except the placement + tokens below is shared between the two designs via
   [data-pts-design^="split"].
   ⚠️ The rail and the nav share a TOP EDGE here, so they must share a sticky offset: both stick at
   --pts-header-h + --pts-stuck-gap. Any difference looks right at rest and drifts apart by exactly that
   amount the moment the page scrolls. --pts-stuck-gap is deliberate breathing room under the header, not
   flush (flush was tried and rejected), so .pts-nav::before paints that strip and travels with the bar. */
/* --pts-col-bottom is where BOTH columns of the line end: main's bottom padding and the rail's bottom
   margin. ONE token because the two have to agree — `.pts-col-side` is align-self:stretch, so it is as
   tall as `.pts-col-main` INCLUDING that padding, and the sticky rail's travel is bounded by its
   containing block: with no margin the rail slid the full --pts-col-bottom past main's last card and
   finished hard against the footer while the content column still had its breathing room.
   50px in BOTH split designs. They started apart (72 / 26): 72 left a visibly empty band above the
   footer, and matching the pair on one value is deliberate — the two layouts are the same shell, so a
   visitor comparing them should not see the page end differently. Keep them equal. */
.pt-home-shell[data-pts-design="split"]{
  --pts-section:62px;--pts-shadow:0 14px 40px rgba(24,24,27,.07);
  --pts-rail-w:340px;--pts-content-w:780px;--pts-top:12px;--pts-nav-h:56px;
  --pts-stuck-gap:12px;--pts-col-bottom:50px;
  --pts-tiles:2;--pts-rows:1;--pts-team:2}
.pt-home-shell[data-pts-design^="split"]{display:flex;flex-wrap:wrap;justify-content:center;align-items:flex-start}
.pt-home-shell[data-pts-design^="split"] .pts-header,
.pt-home-shell[data-pts-design^="split"] .pts-footer,
.pt-home-shell[data-pts-design^="split"] .pts-cover{flex:0 0 100%}
/* ⚠️ `flex:0 0 100%` does NOT guarantee a whole line once a max-width is involved. Lines are collected
   from each item's HYPOTHETICAL main size — the basis clamped by min/max-width — so the banner's
   hypothetical size is its 1120px cap, not 100%, and from ~1460px up (1120 + the 340px rail) the rail
   climbed onto the banner's line. Order groups + a zero-height breaker force the split explicitly:
   header/cover (group 0) · breaker · rail + content column (group 2) · footer (group 3). The header and
   the footer have no cap, so their 100% basis really does fill a line and they need no breaker of their
   own. `order` is visual only — DOM/reading order is unchanged. */
.pt-home-shell[data-pts-design^="split"]::before{content:"";display:block;order:1;flex:0 0 100%;height:0}
.pt-home-shell[data-pts-design^="split"] .pts-rail,
.pt-home-shell[data-pts-design^="split"] .pts-col-main{order:2}
.pt-home-shell[data-pts-design^="split"] .pts-footer{order:3}
.pt-home-shell[data-pts-design^="split"] .pts-header{
  background:rgba(var(--pts-surface-rgb),.94);box-shadow:0 6px 22px rgba(20,16,28,.04)}
/* Banner geometry is the ONE thing the two split designs do NOT share: `splitwide` spans it across the
   page measure (below), `split` boxes it into the rail column above the identity card (further below).
   Both are rounded and inset — an edge-to-edge bleed was tried and rejected. */
.pt-home-shell[data-pts-design^="split"] .pts-cover{border-radius:var(--pts-radius);overflow:hidden}
.pt-home-shell[data-pts-design="split"] .pts-cover::after{
  background:linear-gradient(180deg,rgba(7,7,10,.44),rgba(7,7,10,.06) 55%,rgba(7,7,10,.34))}
/* Rail sits BELOW the banner (its own line) and sticks once scrolled. `flex:0 0 var(--pts-rail-w)` is the
   flex spelling of the old minmax(0,var(--pts-rail-w)) track: it never grows and never shrinks, so the
   column width is the same fixed number the grid version resolved to. */
/* ---- split only: banner + identity card as ONE column ----
   `.pts-col-side` (emitted by the template only for this design — see m.sideBanner) is the left column;
   inside it the banner and the card simply stack in normal flow. Two things are load-bearing:
   · `align-self:stretch` — the rail's sticky travel is bounded by its containing block, which is now
     this wrapper rather than the shell. Stretched, it is as tall as the content column, so the card
     sticks for the whole page exactly as it did when it shared a grid row with main; left at the
     shell's `flex-start` it would be only as tall as banner + card and unstick almost immediately.
   · `max-width:none` on the banner — it has to undo the page-measure cap the two designs used to share,
     or the banner reaches past its column. Its height still comes from --pts-cover-ar, so a 340px
     column gives a 136px band. */
.pt-home-shell[data-pts-design^="split"] .pts-col-side{flex:0 0 var(--pts-rail-w);order:2;
  align-self:stretch;min-width:0}
.pt-home-shell[data-pts-design="split"] .pts-cover{max-width:none;margin:var(--pts-top) 0 0}
.pt-home-shell[data-pts-design^="split"] .pts-rail{flex:0 0 var(--pts-rail-w);position:sticky;
  top:calc(var(--pts-header-h) + var(--pts-stuck-gap));z-index:20;width:auto;margin:var(--pts-top) 0 0;
  max-height:calc(100vh - var(--pts-header-h) - var(--pts-stuck-gap) - 18px);
  overflow:auto;overscroll-behavior:contain;
  /* Scrollable, but with NO visible bar — a thin bar down the identity card reads as a broken panel
     rather than an affordance, and the rail only scrolls at all on a short viewport. Both declarations
     are needed (Firefox honours scrollbar-width, Blink/WebKit the pseudo-element); same pair the nav
     strip uses. Wheel/trackpad/keyboard scrolling is unaffected. */
  scrollbar-width:none}
.pt-home-shell[data-pts-design^="split"] .pts-rail::-webkit-scrollbar{display:none}
/* ⚠️ `flex:1 1 0` + max-width, NOT `flex:0 1 var(--pts-content-w)`. Wrapping is decided from each item's
   FLEX-BASIS before anything shrinks, so a basis of --pts-content-w meant rail (340) + content (780) =
   1120px had to fit or the whole content column dropped onto its own line — the rail then stood alone,
   centred, with the sticky card floating over the content beneath it. That broke every viewport from
   920px (where the block collapse takes over) up to ~1135px (1120 + the scrollbar). A zero basis can
   never overflow the line, so the column always sits beside the rail and simply takes whatever space is
   left; max-width caps it at the design's measure so a wide screen still centres the pair (that cap is
   what the old grid's 1fr gutter tracks did). min-width:0 is the flex counterpart of minmax(0,…):
   without it a long unbreakable string makes the column refuse to shrink and pushes the rail off. */
.pt-home-shell[data-pts-design^="split"] .pts-col-main{flex:1 1 0;max-width:var(--pts-content-w);min-width:0}
/* Inside the side column the rail is a block, so the flex/width parts of the rule above are inert — but
   its margin is not: this must follow it (equal specificity, so source order decides) or the card sits a
   full --pts-top below the banner instead of the 14px gap the design uses. */
.pt-home-shell[data-pts-design="split"] .pts-rail{margin-top:14px}
.pt-home-shell[data-pts-design^="split"] .pts-nav{height:var(--pts-nav-h);box-sizing:border-box;
  margin-top:var(--pts-top);padding-left:var(--pts-gap);
  top:calc(var(--pts-header-h) + var(--pts-stuck-gap))}
/* The gap is real space, so page content would scroll through it behind the bar. This band travels
   with the sticky bar and paints the page background over that strip — the gap stays visible, the
   content behind it does not. (At rest the strip is already page background, so it is invisible.) */
.pt-home-shell[data-pts-design^="split"] .pts-nav::before{content:"";position:absolute;
  left:0;right:0;bottom:100%;height:var(--pts-stuck-gap);background:var(--pts-bg)}
/* The nav is in normal flow above main now, so main only needs its own breathing room — no
   nav-height clearance (that was the overlap the shared grid area required). */
.pt-home-shell[data-pts-design^="split"] .pts-main{width:auto;margin:0;
  padding:10px 0 var(--pts-col-bottom) var(--pts-gap)}
.pt-home-shell[data-pts-design^="split"] .pts-card{flex-direction:column;align-items:flex-start;gap:15px;padding:20px}
.pt-home-shell[data-pts-design^="split"] .pts-logo{width:76px;height:76px;border-radius:13px;font-size:21px}
.pt-home-shell[data-pts-design^="split"] .pts-title{font-size:27px}
.pt-home-shell[data-pts-design^="split"] .pts-meta{gap:5px 12px;margin-top:7px}
.pt-home-shell[data-pts-design^="split"] .pts-rail-actions{align-items:stretch;gap:9px}
.pt-home-shell[data-pts-design^="split"] .pts-rail-actions .pts-btn{width:100%}
.pt-home-shell[data-pts-design^="split"] .pts-note{text-align:center}
.pt-home-shell[data-pts-design^="split"] .pts-facts{--pts-fact-basis:100%;margin-top:12px}
.pt-home-shell[data-pts-design^="split"] .pts-fact{border-right:0;border-bottom:1px solid var(--pts-line);padding:13px 16px}
.pt-home-shell[data-pts-design^="split"] .pts-fact:last-child{border-bottom:0}
.pt-home-shell[data-pts-design^="split"] .pts-fact-copy strong{white-space:normal}
/* No radius on either split nav: the bar starts at the rail gutter and runs to the page edge, so rounded
   corners cut a notch out of a band that has no panel behind them to belong to (they read as a clipped
   glass bar, which is what the screenshot showed). Square in both designs, at every width. */
.pt-home-shell[data-pts-design^="split"] .pts-nav-inner{width:auto;margin:0}
.pt-home-shell[data-pts-design^="split"] .pts-section-head h2{font-size:25px}
.pt-home-shell[data-pts-design^="split"] .pts-group{scroll-margin-top:148px}
.pt-home-shell[data-pts-design^="split"] .pts-tile{--pts-tile-h:218px}
.pt-home-shell[data-pts-design^="split"] .pts-panel h2{font-size:25px}
.pt-home-shell[data-pts-design^="split"] .pts-review-summary{padding:18px 20px;gap:18px}

/* ---------- all-reviews overlay ----------
   Lives outside every section and is position:fixed, so it contributes ZERO page height — the reviews
   section stays a fixed teaser whether the business has 2 reviews or 2000. Scrolling happens inside
   .pts-modal-body, and body.pts-modal-lock stops the page behind from scrolling with it. */
.pts-modal{position:fixed;inset:0;z-index:200;display:none;padding:24px;
  background:rgba(12,10,16,.52);
  -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);overflow:auto}
/* 🔑 The write/edit form is the TOP layer, because it is opened FROM the all-reviews overlay (the Edit
   link on your own card, or Leave Review) and both stay open — `closeReviewForm` deliberately keeps the
   body lock while `#ptsReviewsModal` is still open, so the visitor lands back on the list afterwards.
   ⚠️ Without this the two tie at `.pts-modal`'s z-index:200 and, being positioned siblings, SOURCE ORDER
   decides — and the form is written first in the template (it precedes #ptsReviewsModal), so clicking
   Edit opened it *behind* the list, over a scrim, looking like the click did nothing. Raising the form
   is the fix rather than reordering the markup: the stacking intent then lives in one line here instead
   of depending on which `<div>` a future edit happens to move. */
#ptsReviewForm{z-index:210}
.pts-modal.pts-modal-open{display:flex;align-items:center;justify-content:center}
/* The lock must out-specify the page-scoped html,body{overflow:visible} sticky reset. */
body.pts-modal-lock{overflow:hidden !important}
.pts-modal-panel{width:min(760px,100%);max-height:min(86vh,880px);display:flex;flex-direction:column;
  background:var(--pts-surface);color:var(--pts-text);border-radius:var(--pts-radius);
  box-shadow:0 30px 80px rgba(8,6,14,.34);overflow:hidden}
/* The write/edit review dialog corners tighter than the reviews overlay: 12px against --pts-radius's 18.
   ⚠️ Two things about where this line sits. It has to come AFTER `.pts-modal-panel` — the two selectors
   tie at (0,1,0), so source order decides, and stating it up in the form's own block (where
   `.pts-modal-panel-sm` sets its max-width) silently loses to the rule above. And it must NOT be an id
   (`#ptsReviewForm .pts-modal-panel`), which would also out-rank the ≤620 `border-radius:0` that squares
   the panel off when it goes full-screen on a phone. A literal, not `--pts-radius-sm`: that token is
   re-pointed per design (7px on directory, 17 on showcase, 19 on soft), and this is floating chrome —
   the same call `.pts-mobile-cta` and `.pts-cookie` make. */
.pts-modal-panel-sm{border-radius:12px}
/* 🔑 THE REVIEWS OVERLAY IS A CONSTANT-HEIGHT DIALOG — `height`, not just the `max-height` above.
   Its content is SWAPPED IN PLACE (the star-filter chips, and Show-more paging), so a shrink-wrapped
   panel resized itself on every click: picking "3★ 2" after "All" dropped the panel from the 86vh
   ceiling to its content, and because .pts-modal-open CENTRES it that delta is split across both
   edges — the title, the summary and the chip row the visitor had just clicked all slid ~60px down
   under the cursor, then jumped back on "All". Same doctrine (and same reason) as the gallery
   lightbox's fixed stage: one frame size per viewport, so nothing moves as you step through.
   ⚠️ Scoped with :not(.pts-modal-panel-sm) because the write/edit review dialog is the other
   .pts-modal-panel on this page and MUST keep shrink-wrapping — a 86vh form is a text field adrift
   in an empty sheet. A class, deliberately not `#ptsReviewsModal`: an id would also out-rank the
   ≤620 block's overrides, which is the trap .pts-modal-panel-sm's own comment documents above.
   The body is the panel's only scroll container, so a fixed height costs nothing when the list is
   long (it scrolls, as before) and leaves plain surface below the last card when it is short. */
.pts-modal-panel:not(.pts-modal-panel-sm){height:min(86vh,880px)}
/* …and the scroll container takes the slack, so the leftover space belongs to the list (its
   scrollbar spans the whole region) instead of being dead panel below a `0 1 auto` body. */
.pts-modal-panel:not(.pts-modal-panel-sm) .pts-modal-body{flex:1 1 auto}
.pts-modal-head{display:flex;align-items:center;justify-content:space-between;gap:16px;
  padding:18px 22px;border-bottom:1px solid var(--pts-line)}
.pts-modal-head h2{font-size:20px}
/* The × is fontello's own icon-cancel (the class is on the button, so the glyph is its `:before`) — the
   classic booking pages close with the same icon. 14px in a 34px box: the entity `&times;` needed 20px
   to look right, which is glyph metrics, not a size the icon font shares. */
.pt-home-shell .pts-modal-close{width:34px;height:34px;flex:0 0 auto;border:1px solid var(--pts-line);
  border-radius:10px;background:var(--pts-surface-2);color:var(--pts-muted);font-size:14px;line-height:1;
  display:flex;align-items:center;justify-content:center}
.pt-home-shell .pts-modal-close:before{margin:0;width:auto;line-height:1}
.pt-home-shell .pts-modal-close:hover{color:var(--pts-text)}
/* Same composition as the reviews SECTION's summary (.pts-review-summary): the average is a plain block
   behind a divider, not a bordered card sitting inside the already-bordered panel. The band itself is the
   panel here (it has the head's rule above and the chips' below), so the average drops the base
   .pts-rating-card's surface, border, radius and padding and keeps only the divider.
   ⚠️ `align-self:center` is NOT redundant with the band's `align-items:center` — the base
   .pts-rating-card rule sets `align-self:start`, which out-ranks the container, and the average is the
   shorter of the two halves (the same trap the section's summary documents). */
.pts-modal-summary{display:flex;flex-wrap:wrap;align-items:center;gap:24px;
  padding:18px 22px;border-bottom:1px solid var(--pts-line)}
.pts-modal-summary > .pts-rating-card{flex:0 0 auto;min-width:168px;align-self:center;background:none;
  border:0;border-right:1px solid var(--pts-line);border-radius:0;padding:0 26px 0 0;box-shadow:none}
.pts-bars{display:flex;flex-direction:column;gap:6px;justify-content:center;flex:1 1 auto;min-width:0}
.pts-bar-row{display:flex;align-items:center;gap:9px;
  font-size:13px;color:var(--pts-muted)}
.pts-bar-row > span:first-child{flex:0 0 26px}
.pts-bar-row > .pts-bar-count{flex:0 0 34px}
.pts-bar{flex:1 1 auto;min-width:0;height:7px;border-radius:999px;background:var(--pts-surface-2);overflow:hidden}
.pts-bar i{display:block;height:100%;border-radius:999px;background:var(--pts-warning)}
.pts-bar-count{text-align:right;font-variant-numeric:tabular-nums}
.pts-modal-chips{display:flex;flex-wrap:wrap;gap:7px;padding:14px 22px;border-bottom:1px solid var(--pts-line)}
.pt-home-shell .pts-chip{padding:6px 12px;border:1px solid var(--pts-line);border-radius:999px;
  background:var(--pts-surface);color:var(--pts-muted);font-size:13px;font-weight:700}
.pt-home-shell .pts-chip-on{border-color:transparent;background:var(--pts-accent-soft);color:var(--pts-accent-ink)}
/* The only scroll container in the overlay. */
.pts-modal-body{padding:6px 22px 22px;overflow:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:2px}
#ptsModalMore{margin:16px auto 0}
/* A .pts-link that is a <button>: strip the UA button chrome. */
.pt-home-shell .pts-link-btn{border:0;background:none;padding:0;font:inherit;color:var(--pts-accent-ink);
  font-weight:800;cursor:pointer}

@media(max-width:620px){
  .pts-modal{padding:0}
  .pts-modal-panel{width:100%;max-height:100vh;border-radius:0}
  /* ⚠️ Restated at the base rule's own (0,2,0) — a media query adds NO specificity, so a bare
     `.pts-modal-panel{height:100vh}` here silently loses to the :not() rule above and the phone
     sheet would come up 86vh tall with a strip of scrim under it. Equal specificity, later in the
     file, so this wins. */
  .pts-modal-panel:not(.pts-modal-panel-sm){height:100vh}
  /* Two 132px buttons + the gap + the row's inset want 328px, which does not fit a 320px phone — so
     down here they stop being auto-width chips and SHARE the row instead (`flex:1 1 0`, never `1 1 auto`,
     or the longer label takes more of it and the pair reads as misaligned). ⚠️ The min-width has to be
     undone explicitly: it is id-scoped and a media query adds no specificity of its own. */
  .pts-modal-foot{padding:12px 16px}
  #ptsReviewForm .pts-btn{min-width:0;flex:1 1 0}
  /* The gallery preview's narrow trim. ⚠️ Same (0,2,0) / .pt-home-shell-prefixed shapes as its base block
     above — the plain `.pts-modal{padding:0}` on the line above is exactly what those overrides have to
     beat, and a media query adds no specificity of its own. */
  .pts-modal.pts-lightbox{padding:52px 12px 20px}
  .pts-lb-img{max-height:calc(100vh - 136px)}
  .pts-lb-caption{font-size:13px}
  /* Smaller, and pulled to the very edge: at this width the photo fills the measure, so the arrows float
     over its sides — 44px chips at a 16px inset covered too much of it. */
  .pt-home-shell .pts-lb-nav{width:36px;height:36px;font-size:12px}
  .pts-lb-prev{left:6px}
  .pts-lb-next{right:6px}
  .pt-home-shell .pts-lightbox .pts-modal-close{top:10px;right:10px}
  /* ⚠️ BOTH summaries stay SIDE BY SIDE at every width — the average never stacks above the bars.
     Stacking (which both surfaces used to do, the overlay below 620 and the section below 720) turned a
     ~180px band into a ~315px one: on a phone the whole first screen of the reviews overlay was the score
     and five bars, and the first review card sat below the fold. Nothing here needs a whole line — the
     average is one number, and a bar row is a label, a track and a count.
     So the pair is SHRUNK to fit instead: the card gives up its 168px floor and half its divider padding,
     the gap closes, and the two fixed columns of a bar row (the "5★" label and the count) narrow. That
     leaves ~90px of track at 320px, which still reads as a distribution.
     The card keeps its right-border divider precisely because it is still a column — the stacked rule this
     replaced had to swap that for a bottom rule, since a right border on a full-width block draws a stray
     line down the panel's edge. */
  /* ⚠️ The section's panel is anchored on [data-pts-design] purely for SPECIFICITY: the split designs set
     their own `.pt-home-shell[data-pts-design^="split"] .pts-review-summary{padding:18px 20px;gap:18px}`
     at (0,3,0) OUTSIDE any media query, and a media query adds none — a bare `.pts-review-summary` here
     loses, which left both split designs on the desktop gap and inset (a 43px bar track at 320px against
     65px in the other five). Matching that shape and landing later in the file is what makes it win. */
  .pts-modal-summary{gap:14px}
  .pt-home-shell[data-pts-design] .pts-review-summary{padding:18px 16px;gap:14px}
  .pts-modal-summary > .pts-rating-card,
  .pts-review-summary > .pts-rating-card{flex:0 1 auto;min-width:0;padding-right:14px}
  /* ⚠️ Flex wraps on an item's flex-BASIS, before it shrinks anything — so the section's desktop
     `flex:1 1 260px` dropped the bars onto their own line below ~470px however much they could have
     shrunk (card 111 + gap 14 + basis 260 = 385 against 264px of content measure at 320px). A zero basis
     can never overflow the line, so the bars always sit beside the average and simply take what is left.
     The overlay's `.pts-bars{flex:1 1 auto}` never had the bug: a bar row's own max-content is tiny (its
     track is `flex:1 1 auto;min-width:0`, i.e. no intrinsic width), so `auto` resolves small. */
  .pts-review-summary > .pts-bars{flex:1 1 0}
  /* The average block's own type: the 5-star row is what makes it wide (5×24 + gaps = 132px), and the
     count line is the widest thing left once the stars come down. Both shrink so the card settles at
     ~110px and the bars keep a usable track. The number itself is already handled by the heading scale
     (36px ≤720, 30px ≤470) — don't restate it here or the two sources drift. */
  .pts-rating-card .pts-stars .pts-star{width:16px;height:16px}
  .pts-rating-card span{font-size:11px}
  .pts-bar-row{gap:7px;font-size:12px}
  .pts-bar-row > span:first-child{flex:0 0 22px}
  /* Stays a FIXED column, just a narrower one: sized to content the tracks would end at a different x on
     every row (1 vs 128 reviews) and the distribution would read as ragged. 26px carries three digits at
     12px tabular-nums. */
  .pts-bar-row > .pts-bar-count{flex:0 0 26px}
  .pts-modal-reply{margin-left:0}
}

/* ---------- splitwide: split rail, wider page measure ----------
   Shares the whole flex shell above (see the split block) — only its tokens, its cover overlay and its
   tighter main padding differ. It used to be the one design whose banner spanned the page measure; the
   flex rewrite gave both split designs that banner, since one shared markup cannot both box the cover
   into the rail column and span it across two columns. */
.pt-home-shell[data-pts-design="splitwide"]{
  --pts-section:62px;--pts-shadow:0 14px 40px rgba(24,24,27,.07);
  --pts-rail-w:352px;--pts-content-w:770px;--pts-nav-h:56px;--pts-top:12px;
  --pts-stuck-gap:12px;--pts-col-bottom:50px;
  --pts-tiles:2;--pts-rows:1;--pts-team:2}
/* Spans the PAGE measure — the rail plus the content column, i.e. exactly the width of the two columns
   beneath it, not the viewport. `margin-inline:auto` is what centres it over the pair. */
.pt-home-shell[data-pts-design="splitwide"] .pts-cover{
  max-width:calc(var(--pts-rail-w) + var(--pts-content-w));margin:var(--pts-top) auto 0}
.pt-home-shell[data-pts-design="splitwide"] .pts-cover::after{
  background:linear-gradient(180deg,rgba(7,7,10,.34),rgba(7,7,10,.05) 46%,rgba(7,7,10,.46))}
/* splitwide's column end is the --pts-col-bottom token in its block above (50px, same as split) —
   restating it here as a padding would give main a bottom the rail's margin does not follow, which is
   the whole point of the token. */
.pt-home-shell[data-pts-design="split"] .pts-loc{--pts-loc-basis:100%}
/* Both split designs lay the location row out inside the narrow CONTENT column, not across the page
   measure, so the single-branch pair stacks there instead of sitting side by side — 42% of that
   column is not a card, it is a column of wrapped words. Prefixed with ^="split" (the documented
   shared hook) and anchored on [data-pts-single] so it outranks the pair's own bases. */
.pt-home-shell[data-pts-design^="split"] .pts-loc[data-pts-single],
.pt-home-shell[data-pts-design^="split"] .pts-loc[data-pts-branches]{
  --pts-loc-map-basis:100%;--pts-loc-basis:100%}

/* ==========================================================================
   DARK APPEARANCE — independent of design (opt-in via data-pts-theme="dark")
   ========================================================================== */
.pt-home-shell[data-pts-theme="dark"]{
  --pts-bg:#111014;--pts-surface:#1b1920;--pts-surface-2:#24212a;--pts-text:#f8f7fb;--pts-muted:#aaa4b5;
  /* Both were hand-picked purples (#312545 / #9f7aea) — i.e. the light theme's accent baked in, which
     a themed account would contradict. Derived instead: the tint mixes into the dark surface rather
     than white, and the ink LIGHTENS (readable text on dark is a lighter accent, not a darker one).
     The mixes land within a shade of the two values they replace. */
  --pts-line:#37323f;
  --pts-surface-rgb:27,25,32;--pts-bg-rgb:17,16,20;--pts-line-rgb:55,50,63;
  --pts-accent-soft:#2d1c49;--pts-accent-ink:#a47ae7;
  --pts-accent-fill:#281b3e;--pts-header-tint:#211a2e;
  --pts-edge-12:#3d3151;--pts-edge-14:#3f3155;--pts-edge-18:#41305b;--pts-edge-30:#472f6d;--pts-edge-35:#4a2f75;
  --pts-map-water:#1e2a38;--pts-review-mine-bg:#1e342c;
  --pts-badge-open-bg:#1f372d;--pts-badge-open-today-bg:#204132;
  --pts-badge-closed-bg:#3b2328;--pts-badge-closed-today-bg:#45262b;
  --pts-success:#3ddc84;--pts-danger:#ff8177;
  /* The raw brand red — on a dark surface it reads deep red without help, which is why the classic's
     dark footers paint this value straight. */
  --pts-heart:#fb3957;
  --pts-shadow:0 20px 60px rgba(0,0,0,.3);color-scheme:dark}
/* Per-theme DARK derivations. GENERATED by scripts/gen-accent-tokens.py — the same mixes the light
   blocks state, but into the dark surface/line, and the ink LIGHTENS. Specificity (0,3,0) so these
   beat both the light accent blocks and the dark base block above. */
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="default"]{--pts-accent-soft:#1c1a20;--pts-accent-ink:#747474;
  --pts-accent-fill:#1b1a20;--pts-header-tint:#1b1920;
  --pts-edge-12:#34303b;--pts-edge-14:#342f3a;--pts-edge-18:#332e39;--pts-edge-30:#302c35;--pts-edge-35:#2e2b33}
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="teal"]{--pts-accent-soft:#153035;--pts-accent-ink:#61b0b0;
  --pts-accent-fill:#17292f;--pts-header-tint:#192127;
  --pts-edge-12:#303b47;--pts-edge-14:#2f3d48;--pts-edge-18:#2d404b;--pts-edge-30:#274953;--pts-edge-35:#244d56}
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="violet"]{--pts-accent-soft:#301d43;--pts-accent-ink:#ad7cd7;
  --pts-accent-fill:#2a1c39;--pts-header-tint:#221a2c;
  --pts-edge-12:#3f314e;--pts-edge-14:#413151;--pts-edge-18:#433156;--pts-edge-30:#4b3065;--pts-edge-35:#4f306c}
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="pink"]{--pts-accent-soft:#462735;--pts-accent-ink:#ec97b0;
  --pts-accent-fill:#3b232f;--pts-header-tint:#2a1e27;
  --pts-edge-12:#4b3647;--pts-edge-14:#4f3748;--pts-edge-18:#55394b;--pts-edge-30:#6a3d53;--pts-edge-35:#723f56}
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="green"]{--pts-accent-soft:#23392e;--pts-accent-ink:#89cc9b;
  --pts-accent-fill:#21312a;--pts-header-tint:#1e2425;
  --pts-edge-12:#384143;--pts-edge-14:#384343;--pts-edge-18:#394845;--pts-edge-30:#3a5748;--pts-edge-35:#3b5d4a}
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="orange"]{--pts-accent-soft:#482c2b;--pts-accent-ink:#f0a693;
  --pts-accent-fill:#3c2728;--pts-header-tint:#2a1f24;
  --pts-edge-12:#4c3941;--pts-edge-14:#503b42;--pts-edge-18:#573d42;--pts-edge-30:#6c4444;--pts-edge-35:#754745}
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="blue"]{--pts-accent-soft:#1c3143;--pts-accent-ink:#75b5d7;
  --pts-accent-fill:#1c2b39;--pts-header-tint:#1b212c;
  --pts-edge-12:#343c4e;--pts-edge-14:#343e51;--pts-edge-18:#334156;--pts-edge-30:#304c65;--pts-edge-35:#2f506c}
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="darkblue"]{--pts-accent-soft:#16142e;--pts-accent-ink:#63639b;
  --pts-accent-fill:#17162a;--pts-header-tint:#191725;
  --pts-edge-12:#312c43;--pts-edge-14:#302c43;--pts-edge-18:#2e2a45;--pts-edge-30:#272448;--pts-edge-35:#25224a}
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="yellow"]{--pts-accent-soft:#493c1b;--pts-accent-ink:#f4d468;
  --pts-accent-fill:#3d331d;--pts-header-tint:#2b251e;
  --pts-edge-12:#4d4239;--pts-edge-14:#514538;--pts-edge-18:#584a36;--pts-edge-30:#6e5b2f;--pts-edge-35:#77622d}
.pt-home-shell[data-pts-theme="dark"][data-pts-accent="red"]{--pts-accent-soft:#431319;--pts-accent-ink:#e26161;
  --pts-accent-fill:#38151b;--pts-header-tint:#29171e;
  --pts-edge-12:#492c37;--pts-edge-14:#4c2b36;--pts-edge-18:#532934;--pts-edge-30:#65232c;--pts-edge-35:#6d2029}
.pt-home-shell[data-pts-theme="dark"] .pts-header{background:rgba(var(--pts-surface-rgb),.91);
  border-bottom-color:var(--pts-line)}
/* ⚠️ NO dark-appearance override for .pts-logo. It used to repaint the plate #2a2632, which is the same
   literal-instead-of-token bug as the two dark tokens above: it discarded the merchant's theme in dark
   mode. The accent is theme-independent (a fill, not a surface), so the plate follows the appearance
   for free — exactly as
   .pts-brand-mark and the Book-now button do. */
.pt-home-shell[data-pts-theme="dark"][data-pts-design="soft"] .pts-header{
  background:var(--pts-header-tint)}
.pt-home-shell[data-pts-theme="dark"][data-pts-design="spotlight"] .pts-header,
.pt-home-shell[data-pts-theme="dark"][data-pts-design="directory"] .pts-header{background:var(--pts-surface)}
.pt-home-shell[data-pts-theme="dark"][data-pts-design="showcase"] .pts-card{
  background:rgba(var(--pts-surface-rgb),.88)}
.pt-home-shell[data-pts-theme="dark"][data-pts-design="split"] .pts-nav{
  background:rgba(var(--pts-bg-rgb),.82)}

/* The share popover lives inside .pts-rail, a stacking context at z-index 5 — so the sticky .pts-nav (60)
   painted straight through the open menu, cutting it in half. Raising the popover cannot fix that; the
   ANCESTOR has to clear the nav, so bindShare() adds this class to the rail while the menu is open.
   65 is deliberate: above the nav (60), still below the header (70), which should stay the topmost chrome.
   Shell-prefixed and placed after the design blocks because `split`/`splitwide` set their own
   `.pts-rail{z-index:20}` at (0,3,0) — a bare `.pts-rail.pts-share-lift` (0,2,0) would lose to them. */
.pt-home-shell .pts-rail.pts-share-lift{z-index:65}

/* ==========================================================================
   RESPONSIVE — shared first, then per-design normalisation
   ========================================================================== */
/* ---- the two-column split layout (>920px) ----
   Only up here does the Share pill ride the Book now CTA's line: the rail is just --pts-rail-w wide, so a
   pill alone on a row wastes it. Below 921px the rail spans the page, the card has room again and the CTA
   row is hidden outright at <=720px, so the view moves the pill back under the identity meta — the
   placement every other design uses (see placeShare()). Keeping these rules out of the base block is what
   makes that fallback exact rather than approximately-the-old-look. */
@media(min-width:921px){
  /* The in-page pill row's scroll arrows — the ONLY place they are shown. This is the one layout where
     a mouse can be stranded: the two-column shell leaves the row ~752px against the ~797px a full
     section list needs, the scrollbar is hidden, and a vertical wheel scrolls the page rather than the
     row, so without these the last pill is unreachable. Below 921px the row is swiped and the fade
     alone is the hint; the five block designs get the whole 1120px measure and never overflow. Each
     arrow still needs its direction class, so at either end of the row it goes away. */
  .pt-home-shell[data-pts-design^="split"] .pts-nav.pts-can-left .pts-nav-prev,
  .pt-home-shell[data-pts-design^="split"] .pts-nav.pts-can-right .pts-nav-next{display:flex}
  /* ...and only here does the fade have to clear a BUTTON rather than hint at a swipe, so it doubles.
     At the base 34px the dissolve was exactly as wide as the 32px button: a pill label ran fully opaque
     right up to the button's edge and the two read as colliding. 64px puts the button's near edge at
     roughly half opacity, so the row visibly dissolves INTO it. Keep this in step with revealNavPill's
     `pad` in bookingHomeDesignsView.js — that is what stops the scroll-spy parking a pill under here. */
  .pt-home-shell[data-pts-design^="split"] .pts-nav.pts-can-left .pts-nav-scroll{--pts-nav-fade-l:64px}
  .pt-home-shell[data-pts-design^="split"] .pts-nav.pts-can-right .pts-nav-scroll{--pts-nav-fade-r:64px}
  .pt-home-shell[data-pts-design^="split"] .pts-rail-actions{flex-direction:row;flex-wrap:wrap;
    align-items:center}
  .pt-home-shell[data-pts-design^="split"] .pts-rail-actions .pts-btn{flex:1 1 auto;width:auto}
  .pt-home-shell[data-pts-design^="split"] .pts-rail-actions .pts-share{flex:0 0 auto;margin-top:0}
  .pt-home-shell[data-pts-design^="split"] .pts-rail-actions .pts-note{flex-basis:100%;text-align:left}
  /* Anchored to the pill's RIGHT edge: it sits at the end of the row, and a left-anchored menu
     (min-width 196px) would hang past the rail column. It still opens downward — `.pts-rail` is a scroll
     container (max-height + overflow:auto) an absolutely positioned popover cannot escape, and downward
     it lands inside the rail's box (measured 246..581 of a 571px rail, the last few px reachable by the
     rail's own scroll), whereas flipping it upward overshoots the card's top edge by ~139px. */
  .pt-home-shell[data-pts-design^="split"] .pts-rail-actions .pts-share-menu{left:auto;right:0}
  /* ⚠️ NO panel shadow in the two-column rail — it cannot read as a shadow here, only as a fill.
     `.pts-rail` is `overflow:auto` at this width (the short-viewport scroll), and overflow clips
     descendant ink to the rail's own box: the whole OUTER halo is cut away (measured #f6f6f8 right
     beside the card's edge and below the facts card), so the only part of the shadow that survives is
     the part that falls INSIDE the rail — the 14px gap between the identity card and the facts card,
     where split's `0 14px 40px rgba(24,24,27,.07)` lands at full strength because of its 14px downward
     offset. That painted the gap a flat #e9e9eb, edge to edge, with no falloff visible at either end:
     it read as a grey plate deliberately placed behind the aside, not as depth. Dropping it costs
     nothing that was ever visible up here — both panels keep their own 1px --pts-line border — and it
     is scoped to this breakpoint because below 921px the rail is `overflow:visible`, where the halo
     does wrap the panels and the same shadow reads correctly. */
  .pt-home-shell[data-pts-design^="split"] .pts-card,
  .pt-home-shell[data-pts-design^="split"] .pts-facts{box-shadow:none}
  /* Both columns of the line have to END together. main carries --pts-col-bottom as PADDING, and
     `.pts-col-side` is align-self:stretch — so it is as tall as main including that padding, and a sticky
     element travels to the bottom of its containing block MINUS its own bottom margin. With no margin the
     rail kept sliding after main's last card had stopped and finished flush against the footer, so the two
     columns ended --pts-col-bottom apart (50px in both designs) and the page bottom looked lopsided.
     Same token on both sides, so retuning one column's end moves the other with it.
     ⚠️ MARGIN, not padding: the rail is a border-box scroll container with a max-height, so padding-bottom
     would be spent INSIDE it (eating visible card height and scrolling away) instead of ending the column.
     ⚠️ And >920px only: in the collapse the rail stacks ABOVE the nav, where this would just wedge
     --pts-col-bottom of dead space between the identity card and the pill row. */
  .pt-home-shell[data-pts-design^="split"] .pts-rail{margin-bottom:var(--pts-col-bottom)}
}

/* ---------- sweetalert: put the shared confirm back in the middle ----------
   🔑 The one selector in this sheet that is NOT under `.pt-home-shell`, and it cannot be: sweetalert
   appends `.sweet-alert` to <body>, outside the shell entirely. It is page-scoped by the FILE — this
   stylesheet is loaded by bookingHomeDesigns.html only.
   ⚠️ `.sweet-alert` centres itself with a HARD-CODED negative margin: `width:478px;padding:17px;
   left:50%;margin-left:-256px`, and -256 is half of 478+34, i.e. the number is only half the box under
   `box-sizing:content-box`. `newbookingpage.css` opens with a universal `*{box-sizing:border-box}` and
   loads AFTER sweetalert.css (here and in the classic pages' bundled CSS alike), so the rendered box is
   478px wide while the margin still shifts it by 256 — measured 17px LEFT of centre, which is visible
   precisely because the dialog it covers (`.pts-modal-panel`) IS centred, so the two disagree.
   Centred here with auto margins against `left:0;right:0` rather than by correcting the number to
   -239px: it needs no literal, and it stays right if the vendored lib's width or padding ever change.
   ⚠️ NOT `transform:translateX(-50%)` — the pop-in animates `transform:scale()`, and an animation
   out-ranks an author-normal declaration, so the box would slide 239px across during those 0.3s.
   ⚠️ Scoped to the exact complement of sweetalert's own `@media (max-width:540px)` block, which
   deliberately drops to `left:15px;right:15px;width:auto;margin-left:0` — this sheet loads later, so an
   unscoped rule would beat that and break the phone layout.
   The remaining ~8px ABOVE centre is the library's own arithmetic and is left alone: it writes
   `margin-top` inline from `-(clientHeight + paddingTop)/2`, which overshoots by half the padding at
   every box-sizing, and an inline style can only be beaten with `!important` — which here means either
   a magic height or killing the pop animation. Every classic booking page sits 8px high too.
   ⚠️ And it lives DOWN HERE with the other breakpoints, never up beside the modal chrome it belongs to:
   a media query opened before the ~line-2300 mark cuts short the "everything before the first @media"
   base slice that BookingHomeDesignsReviewsModalSizeTest (and …MobileScaleTest / …FooterTest) read — two
   of their assertions failed on rules they could no longer see. Same trap the lightbox comment
   documents: plain rules up there, breakpoints down here. */
@media all and (min-width:541px){
  .sweet-alert{left:0;right:0;margin-left:auto;margin-right:auto}
}

/* ---- the HEADER's own collapse point, well above the layout's 920px ----
   The header row is brand + nav + actions and NOTHING in it can give way: a pill's label cannot compress,
   and the actions (language chip · signed-in name · Book now) are the page's controls. Measured, the full
   row wants ~1020px (~1075px in the split designs) against an inner measure of `100% - 36px`, so between
   921px and ~1075px the actions were pushed straight off the right edge — up to 82px of horizontal page
   scroll on a page that otherwise never scrolls sideways, in ALL SEVEN designs, right across iPad-landscape
   widths (1024px). Above 920px is exactly where the pill row and the layout look their most desktop-like,
   which is why the overflow read as a rendering fault rather than a too-narrow window.
   The NAV is what yields, for the same reason the signed-in name yields to the brand text further down: the
   in-page pill row lists every one of these entries (and more, plus arrows), so nothing is lost — while the
   brand, the account chip and Book now all have to stay whole.
   1140px is measured, not round: at 1101px the row fit only by ellipsising the "Picktime" wordmark by 3px
   (the page measure is `min(--pts-page, 100% - 36px)`, so the available width grows with the viewport until
   it caps at 1120px) — and this page's standing rule is that the brand text stays whole and something else
   yields. 1140 leaves ~36px of slack, which also buys a longer locale's labels some room before the base
   rule's `overflow:hidden` has to catch them. */
@media(max-width:1140px){
  .pts-header-nav{display:none}
  .pt-home-shell[data-pts-design] .pts-brand{max-width:none}
  /* 10 photos is the ONE count whose cells run out of width before the collapse below: five across is
     213px at the full page measure but 166px by 921px — under the ~170px this page refuses to show a room
     at, and reached ~220px earlier than any other count. So it borrows its neighbours' quarters here and
     lays out 4 + 4 + 2 with the short line centred: 265px at 1140px, 211px at 921px, never below its
     siblings. Five across is kept where it is comfortable (≥1141px), because it fills both of its lines
     exactly and stands two rows instead of three. */
  .pts-gallery[data-pts-gal="10"]{
    --pts-gal-lead:var(--pts-gal-quarter);--pts-gal-tile:var(--pts-gal-quarter)}
}

/* 🔑 THE FOOTER'S LINK COLUMNS STAY TOGETHER — the brand block takes a line of its OWN the moment all
   three no longer fit beside it, and the columns then lay out as equal thirds beneath it.
   941px is DERIVED, not chosen: brand basis 300 + 3 x 168px columns + 3 x 34px gaps + the 36px gutters
   is 942px, so 942 is the narrowest width where the four sit on one line and 941 is the first where flex
   has to wrap. Below it the columns used to peel off ONE AT A TIME — right when a wrapped item lands
   beside its siblings, wrong here: the brand block is four or five rows tall (lockup, description,
   socials, credit, language chip), so the wrapped column landed a BRAND-HEIGHT below the two that
   stayed. Measured before: at 768px "Booking" sat alone at the far left, 388px down, with the whole
   right half of its line empty, and at 721px "Explore" joined it — a stray heading under a finished
   block rather than the third column of a group, for every width from 721px to 941px, iPad-portrait
   768px included.
   ⚠️ `flex-basis` only — never `flex:1 1`. The columns must stay NON-growable or the one column on a
   short last line stretches across the whole measure (the trap the card tracks and the location row
   both document), and a growable basis would also re-stretch them per line as the count changes.
   Measured after: three columns on one row under the brand at every width in the band — 279px each at
   941px, 221px at 768px, 206px at 721px — no overflow, no wrapped label. */
@media(max-width:941px){
  .pts-footer-brand{flex-basis:100%}
  .pts-footer-col{flex-basis:calc((100% - 68px) / 3)}
}

@media(max-width:920px){
  /* ⚠️ Anchored on [data-pts-design] for SPECIFICITY: every design sets these counts itself at (0,2,0)
     and a media query adds none, so a bare .pt-home-shell rule collapses `clean` alone — spotlight kept
     4 tiles down to 320px. Same reasoning as the heading/logo overrides in the blocks below. */
  .pt-home-shell[data-pts-design]{--pts-tiles:2;--pts-rows:1;--pts-team:2}
  /* The chip leaves the HEADER here — the row has to fit brand + account + Book-now and the nav has been
     gone since 1140px — but it does NOT leave the page: placeLang() moves the one chip node into the
     footer's brand block below this width, so a phone can still reach all 39 locales (the `?lang=` POST
     switcher is the only way to). The classic hides its own chip under 1023px and offers nothing on a
     phone at all, which is the gap this closes rather than mirrors.
     ⚠️ SCOPED TO THE HEADER (`.pts-header-actions`), or this rule follows the chip down and hides it in
     the footer too — the hide and the move would then cancel out and the fix would look like a no-op. It
     is still needed for the pre-JS paint, where the chip is in the header markup at 375px.
     ⚠️ Shell-prefixed: the base rule is `.pt-home-shell .pts-lang` (0-2-0) and a media query adds no
     specificity, so a bare `.pts-lang{display:none}` here loses and the chip stays visible on phones. */
  .pt-home-shell .pts-header-actions .pts-lang{display:none}
  /* ⚠️ The CONTACT SHEET collapses HERE, one step ahead of the mosaic (whose own collapse is the ≤720
     block — do not read that block's "narrow" comment as this page's only gallery breakpoint, which is
     the mistake this rule was added to fix). Quarters and fifths are the two narrowest cells in the set
     and they run out first: measured, 4-across is 207px at 920px and 163px by 721px, and 5-across —
     count 10 — is 163px at 920px and just 127px by 721px, a thumbnail of a room rather than a photo of
     one. Thirds hold every count at 223-280px across this whole band. The mosaic has no such problem
     here (its widest cell is a half) and keeps its own shapes down to 720. */
  .pts-gallery[data-pts-gal="7"],.pts-gallery[data-pts-gal="8"],.pts-gallery[data-pts-gal="9"],
  .pts-gallery[data-pts-gal="10"],.pts-gallery[data-pts-gal="11"],.pts-gallery[data-pts-gal="12"]{
    --pts-gal-lead:var(--pts-gal-third);--pts-gal-tile:var(--pts-gal-third)}
  .pts-facts{--pts-fact-basis:50%}
  .pts-fact{border-right:1px solid var(--pts-line)}
  .pts-fact:nth-child(2n){border-right:0}
  .pts-fact:nth-child(-n+2){border-bottom:1px solid var(--pts-line)}
  /* Odd fact counts stack in this band — see the block at the END of this media query, which has to sit
     after the split rules below to win on source order. */
  .pts-loc{--pts-loc-basis:100%}
  /* ⚠️ Anchored on [data-pts-single] for SPECIFICITY, same trap as the counts above: the pair states
     its own bases at (0,2,0) and a media query adds none, so the bare rule on the line above loses
     and a single-branch business kept the 58/42 split all the way down to 320px. */
  .pts-loc[data-pts-single],
  .pts-loc[data-pts-branches]{--pts-loc-map-basis:100%;--pts-loc-basis:100%}
  /* ⚠️ THE ROW CAP IS A TWO-COLUMN CONSTRAINT — release it in the collapse, same doctrine as
     --pts-content-w. Stacked, the card is full-bleed with nothing beside it to balance, so the cap buys
     nothing and costs plenty: a scroll region nested in a page that also scrolls traps the touch
     gesture, which is the phone's single worst list interaction. Every branch simply lists, and the
     page scrolls. Anchored on the attribute for the specificity reason stated above. */
  .pts-loc[data-pts-branches]{--pts-branch-max:none}
  /* Every card is full-bleed here, so the hours card still matches :nth-child(odd) — but it is now narrow,
     where two day-columns would be ~160px each. Back to one column (same selector shape as the base rule,
     which carries an id, so it can win). */
  #pts-hours:nth-child(odd) .pts-hours{columns:1;column-gap:0}
  /* ⚠️ THE FLOATING PILL STAYS — its handover to the full-width bar happens in ONE block, the ≤720 one.
     It used to be hidden HERE while .pts-mobile-cta only appears at ≤720, so 721-920px had NEITHER bottom
     CTA: measured at 721/760/800/900/920px, `.pts-scroll-cta` and `.pts-mobile-cta` both computed
     `display:none`, leaving the page's last-resort Book-now to the sticky header alone across the whole
     tablet band. The two breakpoints have to be the same number or a band falls through the gap — that is
     what BookingHomeDesignsFooterTest#everyWidthKeepsABottomBookNowCta pins. The pill fits down there with
     room to spare (293px wide against a 721px viewport, and 29px clear of the bottom-left cookie notice at
     the narrowest width it now serves), and the band keeps the derived footer strip from the base rule —
     no `padding-bottom` override here, or the pill lands on the copyright hairline. */
  /* Hero headline: 46px (54 in spotlight) was sized for a 1120px measure. This is the LAST
     breakpoint where it is visible at all — ≤720px hides .pts-hero-msg outright. Anchored on
     [data-pts-design] to out-rank spotlight's own 54px (see the ≤470 block's note). */
  .pt-home-shell[data-pts-design] .pts-hero-title{font-size:34px}
  .pt-home-shell[data-pts-design="showcase"] .pts-tiles .pts-tile:nth-child(3n+1),
  .pt-home-shell[data-pts-design="showcase"] .pts-tiles .pts-tile:last-child:nth-child(3n+2){flex-basis:var(--pts-tile-basis)}
  .pt-home-shell[data-pts-design="directory"]{--pts-tiles:1}
  /* 🔑 ONE CATALOGUE CARD FOR ALL THREE GROUPS, EVERYWHERE BELOW THE DESKTOP LAYOUT. Services and
     resources render as `.pts-tile`, classes as `.pts-row`, and in THIS design they are the same object —
     badge | text | meta | CTA on one centred line, 58px badge, full width at every count. So once the
     shell has collapsed to a single column the three lists must read as one system. They did not:
     measured at 560px and again at 834px, a service/resource card stood **94px** tall on **13px** padding
     with a **16px** name and a **12px**-radius badge, against a class card at **104px** on **16px**
     padding with a **15px** name and a **14px** badge — six differences, on two lists one section apart
     on the same page.
     None of that is a designed distinction. The desktop `directory` tile is deliberately dense (its 94px
     is pinned by BookingHomeDesignsTileFoldTest as part of "dense, list-first"), but nothing ever decided
     that the COLLAPSED state should differ from the class row beside it; the two simply drifted apart.
     The row is the reference — it is the one that already looked right — so the tile restates the row's
     geometry verbatim: padding 16, gap 15, height 104, badge radius 14, name 15px.
     ⚠️ The name's TRACKING moves with its size: 15px is -.015em on the tracking ladder (see the CONTENT
     TYPE SCALE block). Left on the 16px rung's -.02em it would be the one thing still set differently
     from the class name it now matches.
     ⚠️ Every rule anchored on [data-pts-design="directory"] (0-3-0) — the desktop directory rules are
     0-3-0 and a media query adds NO specificity, so a bare `.pts-tile{padding}` here loses to them. That
     anchor is also what keeps this off the other six designs, whose tiles are COLUMN cards and share none
     of this shape.
     ⚠️ This block, not ≤720: the mismatch is visible at every width the shell is one column, and 921px is
     where the desktop layout (and directory's guarded dense row) begins. Above it the tile is 94px and the
     row 104px — the same drift, one deliberate value away from being fixable here. */
  .pt-home-shell[data-pts-design="directory"] .pts-tile{--pts-tile-h:104px;padding:16px;gap:15px}
  .pt-home-shell[data-pts-design="directory"] .pts-tile-icon{border-radius:14px}
  .pt-home-shell[data-pts-design="directory"] .pts-tile h4{font-size:15px}
  /* split collapses to the shared single-column stack. The cover goes full-bleed here, so its
     height is purely aspect-driven and the rail's --pts-cover-h offset is overridden below. */
  .pt-home-shell[data-pts-design^="split"]{display:block;--pts-section:62px}
  /* Block layout again, so the auto margins are wanted back (see the base .pts-cover rule). */
  .pt-home-shell[data-pts-design^="split"] .pts-cover{margin:0 auto;border-radius:0}
  /* position:relative (not static) keeps z-index alive so the card is not clipped by the cover;
     top:auto stops the sticky offset from re-applying and shifting the card down. */
  /* ⚠️ Drop the two-column measure. `--pts-content-w` caps the content column so the rail + content pair
     stays centred on a wide screen, but the cap is a plain max-width — it keeps applying once the shell
     collapses to a single column, where nothing balances it: the column froze at 780px and left 55-125px
     of dead space down the right edge at every width from 781px to 920px. Full width is the whole point
     of the collapse. */
  .pt-home-shell[data-pts-design^="split"] .pts-col-main{max-width:none}
  .pt-home-shell[data-pts-design^="split"] .pts-rail{position:relative;top:auto;max-height:none;overflow:visible;
    width:min(var(--pts-page),calc(100% - 36px));margin:-46px auto 0}
  .pt-home-shell[data-pts-design^="split"] .pts-card{flex-direction:row;align-items:center;padding:22px}
  .pt-home-shell[data-pts-design^="split"] .pts-rail-actions{align-items:flex-end}
  .pt-home-shell[data-pts-design^="split"] .pts-rail-actions .pts-btn{width:auto}
  .pt-home-shell[data-pts-design^="split"] .pts-note{text-align:right}
  .pt-home-shell[data-pts-design^="split"] .pts-facts{--pts-fact-basis:50%;margin-top:14px}
  .pt-home-shell[data-pts-design^="split"] .pts-fact{border-right:1px solid var(--pts-line);border-bottom:0;padding:15px 17px}
  .pt-home-shell[data-pts-design^="split"] .pts-fact:nth-child(2n){border-right:0}
  .pt-home-shell[data-pts-design^="split"] .pts-fact:nth-child(-n+2){border-bottom:1px solid var(--pts-line)}
  .pt-home-shell[data-pts-design^="split"] .pts-nav{height:auto;margin-top:18px;padding-left:0}
  .pt-home-shell[data-pts-design^="split"] .pts-nav-inner{width:min(var(--pts-page),calc(100% - 36px));margin:auto}
  .pt-home-shell[data-pts-design^="split"] .pts-main{width:min(var(--pts-page),calc(100% - 36px));margin:auto;
    padding:44px 0 96px}

  /* 🔑 ODD FACT COUNTS STACK IN THIS BAND — an odd count cannot fill a two-up row, and 3 facts as 2+1
     leaves the same empty half the desktop quarter-void showed (measured 432px at 900px). Stacking is
     what the ≤470 block already does for every count; this brings the two odd counts forward to the band
     where they break. It applies to BOTH families: split's rules above also set a 50% basis up here.

     ⚠️ POSITION IS LOAD-BEARING, twice over. It must come AFTER the split rules above — its border
     selectors tie with theirs at (0,4,0), so only source order decides, and written up beside the
     `.pts-facts` basis it lost to `[data-pts-design^="split"] .pts-fact:nth-child(2n)` and drew a divider
     after a full-width tile. And the dividers must travel WITH the basis: the 2-up `nth-child` pair
     earlier in this block would otherwise put a bottom border under the first two stacked tiles only. */
  .pt-home-shell[data-pts-design] .pts-facts[data-pts-facts="1"],
  .pt-home-shell[data-pts-design] .pts-facts[data-pts-facts="3"]{--pts-fact-basis:100%}
  .pt-home-shell[data-pts-design] .pts-facts[data-pts-facts="1"] .pts-fact,
  .pt-home-shell[data-pts-design] .pts-facts[data-pts-facts="3"] .pts-fact{
    border-right:0;border-bottom:1px solid var(--pts-line)}
  .pt-home-shell[data-pts-design] .pts-facts[data-pts-facts="1"] .pts-fact:last-child,
  .pt-home-shell[data-pts-design] .pts-facts[data-pts-facts="3"] .pts-fact:last-child{border-bottom:0}
}

/* Signed-in name caps. Its own breakpoints (768 / 425) rather than the layout ones, because what
   constrains it is the header running out of room, not the page switching layout: the name is the one
   header item that can give way, so the Picktime wordmark, the logo mark and the Book-now pill all stay
   whole. It ellipsises (…) rather than wrapping, so a long name still shows its first characters.
   ORDER MATTERS: media queries add no specificity, so these must stay in descending width order —
   768 here, 425 after the ≤470 block — or the wider cap wins at the narrower width. */
@media(max-width:768px){
  .pts-user-name{max-width:60px}
}

@media(max-width:720px){
  .pt-home-shell[data-pts-design]{--pts-tiles:1;--pts-rows:1;--pts-team:2;--pts-overlap:-10px}
  /* …except spotlight, which declares 0 for itself at every width and MEANS it: its identity card is a
     bare text block (background:transparent, no shadow, no radius), so a negative overlap does not lift a
     panel over the banner — it drops the business name onto the photograph, half-hidden behind the
     banner's bottom edge. The generic rule above is (0,2,0) and this must out-rank it, hence the design
     name; it also has to sit AFTER it, since equal specificity is decided by source order. */
  .pt-home-shell[data-pts-design="spotlight"]{--pts-overlap:0px}
  .pts-rail,.pts-main,.pts-nav-inner,.pts-header-inner,.pts-footer-inner{width:min(var(--pts-page),calc(100% - 24px))}
  /* 🔑 THE RICH FOOTER'S BLOCK TAKES THE SAME GUTTERS as the line above it — it was the one band left on
     the base 36px while every other band (the list above, .pts-footer-inner included) moved to 24px, so
     the hairline + copyright bar UNDERNEATH it ran 12px wider than the block it closes and the brand
     lockup sat 6px right of the copyright and of the header's own brand. Measured at 720px and below:
     block left 18px against bar left 12px, now 12/12 at every width.
     ⚠️ It needs the descendant form (0,2,0) to beat its own base rule — a bare `.pts-footer-top` is
     (0,1,0) and loses — and it is a rule of its OWN rather than a sixth selector in the list above,
     because that list is pinned verbatim by BookingHomeDesignsNavScrollTest#theRowMeasureLivesOnTheArrowAnchor. */
  .pts-footer-rich .pts-footer-top{width:min(var(--pts-page),calc(100% - 24px))}
  /* ⚠️ NOTHING IN THE HEADER'S ACTIONS IS HIDDEN ON A PHONE — not the signed-in name, and not the
     LOG IN button, which used to carry `display:none` here. The two are the same slot (the template
     renders one or the other, `m.user ? .pts-user : .pts-login`), so hiding only the signed-out half was
     the page telling a returning customer they were anonymous while telling a signed-in one exactly who
     they were: below 720px there was no way to reach Booking History at all, and the only route back was
     the login prompt buried in the booking flow. It also costs the header LESS than the state that
     already fits — the pill is one short label where the chip is avatar + name + caret (measured at
     320px: 76px against 98px), so the width that carries the signed-in header carries this one with room
     to spare. The name likewise stays visible — it is how a customer confirms WHICH account they are
     booking under, and an avatar-only button reads as anonymous; it truncates (max-width + ellipsis)
     and the narrow-screen trims below keep the row on one line. */
  .pts-hero-msg{display:none !important}
  /* Gallery, narrow: the FEATURED counts give up their two-column shape — the featured photo takes the
     width and its column sits two-up underneath, since a 50/25 split leaves those photos ~160px wide,
     too small to show a room. The EVEN counts keep their grid and only drop to halves, which is what
     preserves the equality that is the whole point of those arrangements (a 6-photo grid becomes 2 × 3,
     a 4-photo grid stays 2 × 2, a pair stays side by side).
     ⚠️ Tokens, not flex-basis, and anchored on [data-pts-gal] — the per-count blocks above are (0,2,0)
     and a media query adds NO specificity, so a bare `.pts-gallery-lead{flex-basis:100%}` here would
     lose to the count that set the token. Every MOSAIC count still lands on complete lines here: the
     featured columns hold 2 or 4 photos and the grids hold an even number, so nothing has to grow. The
     contact-sheet counts below can come out short, which is what the base rule's centring is for. */
  .pts-gallery[data-pts-gal="3"],.pts-gallery[data-pts-gal="5"]{
    --pts-gal-lead:100%;--pts-gal-rest:100%;--pts-gal-item:var(--pts-gal-half)}
  .pts-gallery[data-pts-gal="6"]{--pts-gal-lead:var(--pts-gal-half);--pts-gal-tile:var(--pts-gal-half)}
  /* The CONTACT-SHEET counts take their SECOND step here: thirds (set in the ≤920 block) would be 164px
     by 560px, the same too-small cell that block moved them off quarters to avoid, so they drop to HALVES
     — the width the 6-photo grid above drops to, measured 333px at 720px and 209px at 471px. It costs
     rows (twelve photos become six of them), which is the trade this page makes at every narrow width: a
     legible photo of a room and a longer scroll, never a strip of textures.
     ⚠️ 9 keeps its 2:1 (it has no override), exactly as count 4 keeps its own at every width — a cell this
     narrow is already short, and re-deriving a per-count ratio per breakpoint is how one starts to drift.
     A count that comes out short on its last line (7, 9, 11 at halves) is centred by the base rule. */
  .pts-gallery[data-pts-gal="7"],.pts-gallery[data-pts-gal="8"],.pts-gallery[data-pts-gal="9"],
  .pts-gallery[data-pts-gal="10"],.pts-gallery[data-pts-gal="11"],.pts-gallery[data-pts-gal="12"]{
    --pts-gal-lead:var(--pts-gal-half);--pts-gal-tile:var(--pts-gal-half)}
  .pts-card{padding:15px}
  .pts-logo{width:72px;height:72px;border-radius:12px;font-size:19px}
  /* ---- heading scale, tablet/large phone ----
     The whole scale has to move together: shrinking only the h1 (business name) would leave the
     28px section headings LARGER than it, which reads as a broken hierarchy rather than a small
     screen. Order kept: title ≥ section/panel h2 > group h3 > card h3 > tile h4.
     Anchored for specificity where a design sets its own size (`directory`/`split` title 27px,
     `split` section/panel h2 25px) — a bare selector here would lose to those. See the ≤470 note. */
  .pt-home-shell[data-pts-design] .pts-title{font-size:22px}
  .pt-home-shell[data-pts-design] .pts-section-head h2,
  .pt-home-shell[data-pts-design] .pts-panel h2{font-size:21px}
  .pts-group-head h3{font-size:18px}
  .pts-loc-card h3{font-size:16px}
  .pts-tile h4{font-size:15px}
  .pts-modal-head h2{font-size:18px}
  .pts-rating-card strong{font-size:36px}
  /* The sticky mobile CTA takes over here, so the rail's own Book now goes away. The split designs' Share
     pill is unaffected: below 921px placeShare() has already put it back inside the identity card. */
  .pts-footer-rich .pts-footer-top{gap:26px;padding:38px 0 30px}
  /* The thirds are RE-STATED because the GAP changed — 2 x 26px between the three columns, not 2 x 34px.
     🔑 And they hold all the way to the phone stack at ≤470: a TWO-UP rung was built here and REMOVED,
     because with three columns it always leaves one alone on a second row — and it lands under the
     tallest column, which is the same stranded heading this whole ladder exists to avoid, just 300px
     further down the page (measured at 620px: two-up 692px tall with "Booking" adrift, three-up 578px
     with all three on one row). Thirds stay legible instead: 215px per column at 720px, 181px at 620px,
     132px at 471px, and measured with the longest real translations a bundle carries
     ("Gruppenbuchungen", "Öffnungszeiten", "Buchungsverlauf") not one label wraps at any width in the
     band. Below 470 they stack one per line — see that block. */
  .pts-footer-col{flex-basis:calc((100% - 52px) / 3)}
  .pts-rail-actions{display:none}
  .pts-nav{margin-top:12px}
  .pts-nav-scroll{padding:7px 0}
  /* ⚠️ The head stays a ROW at every width — do NOT stack it. Only two heads carry a link (the
     catalogue's "View all" and the reviews overlay's), and stacking moved that link to the LEFT edge
     directly under the heading, where it reads as a stray line of copy rather than the section's
     action. The row is affordable because nothing in it is fixed: the text block shrinks (min-width:0,
     the heading wraps) while the link stays whole — same "the copy gives way, never the action"
     doctrine as the sticky mobile CTA. */
  .pts-section-head{gap:14px}
  .pts-section-head > div{min-width:0}
  /* NOTE: the section's summary does NOT stack here (and the overlay's does not below 620) — see the
     side-by-side block in the ≤620 media query. Between 621 and 720 the panel still has ~545px of
     content measure, so the desktop sizing fits as it is and this block needs no rule at all. */
  /* 🔑 THE HANDOVER — one block, both halves. The compact right-hand pill gives way to the full-width bar
     exactly here, so every width from 320px up has one bottom CTA and only one. Splitting these two rules
     across breakpoints is what left 721-920px with none of either (see the ≤920 block's note), and stating
     them together is what keeps a future move of one from silently reopening that gap. */
  .pts-scroll-cta{display:none}
  .pts-mobile-cta{display:flex}
  /* ⚠️ The bottom edge now belongs to the full-width .pts-mobile-cta (10px offset + its measured 59px
     bar), so the notice has to sit ABOVE it — at the desktop offset it would cover the page's
     last-resort Book-now bar until the visitor dismissed it. It also goes full-bleed on the CTA's own
     10px gutters, so the two read as one stack rather than a card floating over a bar. */
  .pts-cookie{left:10px;right:10px;width:auto;bottom:79px}
  .pts-footer{padding-bottom:86px}
  .pt-home-shell[data-pts-design="soft"] .pts-cover{max-width:none;margin:0 auto;border-radius:0}
  .pt-home-shell[data-pts-design="spotlight"] .pts-rail{padding-top:0}
  /* 🔑 THE DIRECTORY TILE DOES **NOT** WRAP HERE — it wraps with `.pts-row` at ≤470, one breakpoint for
     the whole catalogue (its geometry is unified with the row's in the ≤920 block above). This is where
     that wrap used to live, and from 471px to 720px it made every service and resource stand as a
     3-line 151px block — the name alone on the top line with the right half of the card empty, its
     duration/price and "Book now" stacked underneath — directly beside a class row still tidy on one
     line. Measured across this band the unwrapped tile leaves the name 180px+ of measure at 471px (it
     ellipsises, which is its own doctrine), so there was nothing to buy by wrapping 250px early. */
  .pt-home-shell[data-pts-design^="split"] .pts-rail{width:min(var(--pts-page),calc(100% - 24px));margin-top:-28px}
  .pt-home-shell[data-pts-design^="split"] .pts-card{padding:15px}
  .pt-home-shell[data-pts-design^="split"] .pts-main{padding:34px 0 72px}
}

@media(max-width:470px){
  .pt-home-shell[data-pts-design]{--pts-team:1;--pts-overlap:6px}
  /* One column per line: two 50% columns leave ~140px each at 375px, which wraps every second label. */
  .pts-footer-col{flex-basis:100%}
  /* One photo per line, whatever the count arranged. At 375px a two-up gallery is ~170px wide — a room
     photo at that size is a texture, not a picture, and the caption wraps to three lines over it. That
     holds for the contact-sheet counts too, so a 12-photo gallery is a genuinely long scroll on a phone —
     the merchant chose twelve, and twelve legible photos beat twelve thumbnails of a room. (`loading=lazy`
     on every cell but the first is what keeps the cost of that off the initial paint.)
     ⚠️ Every token, the LEAD included: it is a cell of the grid at the even counts, so leaving it out
     left the first photo at its desktop half/third width beside full-width ones. And ⚠️ tokens anchored
     on [data-pts-gal] rather than bare `flex-basis` declarations — the per-count blocks are (0,2,0) and
     a media query adds no specificity. */
  .pts-gallery[data-pts-gal]{--pts-gal-lead:100%;--pts-gal-rest:100%;--pts-gal-item:100%;--pts-gal-tile:100%}
  /* Banner and identity card sit APART on a phone: 6px of breathing room instead of the -28px the wider
     layouts use to lift the card over the banner. At this width the card is full-bleed and the banner is
     short, so the overlap just crowded the two. ⚠️ --pts-overlap is anchored on [data-pts-design] (0-2-0)
     because each design sets its own value at that specificity and a media query adds none; the split
     designs don't read the token at all (their rail sets margin directly), hence the explicit override
     further down this block. */
  /* NOTE: the Picktime wordmark stays visible at every width — it is the product's brand text. The
     header's shortfall on small screens is paid for by the signed-in name instead; its caps live in the
     ≤768px / ≤425px blocks below. */
  /* The logo is the business's identity and must survive the narrowest layout — it used to be
     display:none here, which left an anonymous name-only card on every phone. Keep the two-column
     card (single-column would drop the mark onto its own row and cost a whole line of height) and
     shrink the mark plus the gap instead, so the name still has usable measure at 320px.
     ⚠️ Anchored on [data-pts-design] purely for SPECIFICITY: `directory` and `split` size the logo
     and the card themselves (`.pt-home-shell[data-pts-design…] .pts-logo`, 0-3-0), which a bare
     `.pts-logo` here cannot out-rank — a media query adds none. Matching that shape and landing
     later in the file is what makes these win in all seven designs. */
  .pt-home-shell[data-pts-design] .pts-logo{width:54px;height:54px;border-radius:9px;font-size:16px}
  .pt-home-shell[data-pts-design] .pts-card{gap:12px}
  /* ---- heading scale, phone ---- (same reasoning as the ≤720 block: the scale moves as a unit) */
  .pt-home-shell[data-pts-design] .pts-title{font-size:19px}
  .pt-home-shell[data-pts-design] .pts-section-head h2,
  .pt-home-shell[data-pts-design] .pts-panel h2{font-size:18px}
  .pts-group-head h3{font-size:16px}
  .pts-loc-card h3{font-size:15px}
  .pts-tile h4{font-size:14px}
  .pts-team-card h3{font-size:14px}
  .pts-modal-head h2{font-size:17px}
  .pts-rating-card strong{font-size:30px}
  /* The last few px of bar track at phone width come out of the panel's own inset, not out of the bars —
     the section's summary is the tighter of the two surfaces (the overlay is full-bleed here, while this
     one sits inside the page gutter, and in the split designs inside the content column as well).
     ⚠️ Anchored on [data-pts-design] for the same reason as the ≤620 rule: the split designs' own
     padding/gap rule is (0,3,0) and outside any media query. */
  .pt-home-shell[data-pts-design] .pts-review-summary{padding:16px 12px;gap:12px}
  .pts-facts{--pts-fact-basis:100%}
  /* ⚠️ …and the split designs need it stated on THEIR anchor, the same trap as the count tokens and the
     heading overrides: the ≤920 block sets `[data-pts-design^="split"] .pts-facts{--pts-fact-basis:50%}`
     at (0,3,0) — correct up there, where the rail spans the page — and a media query adds no specificity,
     so the bare rule above lost and both split designs kept a TWO-UP strip down to 320px. At 320 that is a
     147px cell: "Services · Classes · Resources" broke over three lines and the confirmation tile over
     four, against 70px-tall single-column tiles in the other five designs. */
  .pt-home-shell[data-pts-design^="split"] .pts-facts{--pts-fact-basis:100%}
  /* ⚠️ …and on the COUNT anchor, for the third statement of the same trap: a phone gets one tile per line
     whatever the count, so the per-count bases (0,2,0) and the ≤920 odd-count rule (0,4,0) both have to be
     undone here. Written without this, a 2-fact strip held its 50% basis down to 320px (a ~140px cell) and
     a 3-fact strip its 33% (a 116px cell — a tile narrower than its own title). One rule for every count,
     so a count added later is covered. */
  .pt-home-shell[data-pts-design] .pts-facts[data-pts-facts]{--pts-fact-basis:100%}
  .pts-fact{border-right:0 !important;border-bottom:1px solid var(--pts-line) !important}
  .pts-fact:last-child{border-bottom:0 !important}
  .pts-row{flex-wrap:wrap;height:auto;min-height:var(--pts-row-h)}
  /* Both drop under the text and line up with it — so the indent IS the badge (58px) plus the row's 15px
     gap = **73px**, measured against the text column's own left edge, not a chosen number. It read 67 (the
     sum was slipped), which left the wrapped price and CTA 6px to the LEFT of the name above them: a ragged
     left edge on exactly the rows this block reshapes. The indent comes out of the basis: `100%` plus the
     margin overflowed the row by the margin. */
  .pts-row-price{flex-basis:calc(100% - 73px);margin-left:73px;text-align:left}
  .pts-row-cta{flex-basis:calc(100% - 73px);margin-left:73px}
  /* 🔑 …and the DIRECTORY TILE takes the same move at the same width, because it is the same object: the
     services/resources tile in this design is `badge | text | meta | CTA` on one centred line with a 58px
     badge, i.e. a class row (the ≤720 block is where it is given the row's geometry outright). So it
     wraps HERE and not there — one breakpoint for the whole catalogue, or a page shows a wrapped tile
     beside an unwrapped row.
     ⚠️ The indent is the SAME 73px as the row's above, and for the same derivation: the badge (58px) plus
     the card's gap — which below 720px is the row's 15px, since that block hands the tile the row's
     spacing. So the three groups' wrapped lines share one left edge, not merely a tidy one each.
     It read **55px**, which was neither this tile's sum nor any other rule's, and left both wrapped lines
     **16px to the LEFT of the name above them**: a ragged left edge on exactly the cards this block
     reshapes, and the more visible half of what made these read as broken beside the classes. (Its
     comment derived 55 from a 42px icon — the icon has been 58px for some time; the guard test
     `BookingHomeDesignsFlexLayoutTest#theWrappedPriceAndCtaLineUpWithTheTextColumn` reads the operands
     out of this sheet rather than restating the sum, and had been failing on it.) The indent comes out of
     the basis for the same reason as the row's: `100%` plus the margin overflows the tile by the margin.
     ⚠️ Anchored on [data-pts-design="directory"] (0-3-0) — the desktop directory rules are 0-3-0 and a
     media query adds no specificity, so a bare `.pts-tile-meta`/`.pts-tile-cta` here would lose. */
  .pt-home-shell[data-pts-design="directory"] .pts-tile{flex-wrap:wrap;height:auto;min-height:var(--pts-tile-h)}
  .pt-home-shell[data-pts-design="directory"] .pts-tile-meta{flex-basis:calc(100% - 73px);margin-left:73px;
    text-align:left;display:flex}
  .pt-home-shell[data-pts-design="directory"] .pts-tile-cta{flex-basis:calc(100% - 73px);margin-left:73px}
  /* Same move for the review head, and for the same reason. `.pts-review-score` is `flex:0 0 auto` — it
     never gives up its ~105px (five stars plus the number) — so at 320px the name column was left 58px
     against a 42px avatar. A one-word name fits that; a real one does not, and `.pts-reviewer` has no
     clipping, so "Chandrika Venkatasubramanian" simply painted its second word straight over the stars.
     Dropping the score onto its own line hands the name the whole column (208px at 320px). The indent is
     the avatar (42px) plus the head's 14px gap, and it comes out of the basis for the reason above. */
  .pts-review-head{flex-wrap:wrap}
  .pts-review-score{flex:0 0 calc(100% - 56px);margin-left:56px}
  /* The split designs' rail sets its margin directly, so the --pts-overlap change at the top of this
     block misses them — same 6px gap, stated explicitly. */
  .pt-home-shell[data-pts-design^="split"] .pts-rail{margin-top:6px}

  /* ⚠️ The play button is trimmed HERE and only here — see the reasoning on .pts-video-play: this is the
     one control on the page that competes with a picture rather than for room, and a circle takes a share
     of a DERIVED height (60px would be 38% of the panel at 320px against 12% on the desktop one).
     ⚠️ The glyph's BOX moves with it and nothing else does: the path, its stroke width and therefore its
     corner radius are all in the svg's own user units, so they scale with the box. That is the whole
     benefit of having moved off the border triangle, which needed a matching pair of border widths and a
     re-derived margin at every size. */
  .pts-video-play{width:48px;height:48px}
  .pts-video-glyph{width:24px;height:24px}
}

/* Narrowest phones: the name yields further so the brand text and the CTA stay intact (see the ≤768
   note above). 38px shows only the first few characters plus the ellipsis — deliberate, since the
   full name is in the menu this button opens. */
@media(max-width:425px){
  .pts-user-name{max-width:38px}
  /* At 320px the 38px cap alone still left the header ~20px short, and flex takes that out of the one
     shrinkable item — the brand text, ellipsised to "Pickt…". These trims buy the space back out of
     chrome (the header inset, the actions gap, the mark) so the wordmark stays whole. Values are tight,
     not arbitrary: each was measured back to a header that fits at 320px.
     ⚠️ The small pill's own padding is deliberately NOT trimmed here — `.pts-btn-small` keeps its base
     `0 14px` at every width, so the Book-now button reads identically above and below 425px (it is the
     page's primary CTA, and a narrower pill on a phone made it look like a different, lesser control).
     That hands ~8px back to the header, which flex pays for out of the brand text: at the very narrowest
     phones the wordmark may ellipsise rather than the CTA shrinking. Buy space elsewhere (the name cap,
     the mark) if that ever needs fixing — not out of this padding. */
  .pts-header-inner{width:calc(100% - 14px)}
  .pts-header-actions{gap:6px}
  /* No brand-gap trim here any more: --pts-brand-gap is 6px everywhere, which is 1px tighter than the
     7px this block used to force, so the 320px fit is unchanged (strictly better). Re-adding a value
     here would WIDEN the phone gap and split the header lockup from the footer's again. */
  .pts-brand-picktime,.pts-brand-logo,.pts-brand-mark{width:30px;height:30px}
  /* Same trim for `clean`'s credit badge, which stands in for that 34px mark: the plate is the brand
     here, so it gives back the same few px of chrome rather than letting the wordmark ellipsise. */
  .pts-brand-badge{padding:6px 10px}
  .pts-brand-badge-name{font-size:14px}
  .pts-user-btn{gap:6px;padding:4px 6px 4px 4px}
  .pts-avatar{width:26px;height:26px}
}

@media (prefers-reduced-motion: reduce){
  .pt-home-shell *,.pt-home-shell *::before,.pt-home-shell *::after{
    transition-duration:.01ms !important;animation-duration:.01ms !important}
}
