/* floors.css: owner rulings a skin cannot go under. Loaded after skin.css on purpose. */
/* Photographs take the site's own media radius (--cx-radius-media, 6px unless the skin says otherwise). The
   6px floor of 2026-08-27 was retired on 2026-09-21 (owner decision 4): a square shape language is a valid
   choice when buttons and cards share it. A photo that bleeds to the viewport edge is marked cx-bleed and
   stays square. */
main img:not(.cx-bleed):not(.r-brand-logo):not([class*="logo"]):not([class*="icon"]) { border-radius: var(--cx-radius-media, 6px); }
.cx-bleed img, .cx-bleed { border-radius: 0; }
/* The hamburger's position is NOT here, on purpose. A first version pinned it with margin-left:auto
   and order:2; on a skin that had already placed the button correctly that moved it from the corner
   to 99px away on every page. Position is layout, and layout cannot be floored by a rule that does
   not know the skin's header. The mobile gate names the selector and the gap; that repair stays a
   targeted model edit. */
/* The phone action bar is chassis furniture: hidden above the chassis breakpoint whatever a skin says.
   A visibility rule on an element the skin does not own cannot move skin content. */
@media (min-width: 901px) { .cx-bar { display: none; } }
/* The sticky header and the strip above it are chassis furniture, and their HEIGHT is the chassis's
   business, not a band: every in-page anchor on the site is offset by --cx-head-h, and whatever they
   take comes off the first screen on every page at once. Their inner wraps set the height (.r-hdr-in
   has min-height: var(--cx-head-h), .r-util-in 42px), so a skin that wants a taller header still says
   so there; what is ruled out is vertical padding on the outer element, which is where a section band
   lands when it leaks. The normalizer's theme-is-a-palette correction removes the usual cause; this is
   the same ruling for a selector it does not read. Same shape as the .cx-bar rulings above: a rule on
   furniture the skin does not own cannot move skin content. */
.r-hdr.r-hdr, .r-hdr2.r-hdr2, .r-mast.r-mast, .r-util.r-util, .r-topbar.r-topbar { padding-block: 0; }
/* The stacked header's two rows are designed to 88px on a desktop with 6px above and below them. A skin's
   ".cx-head .cx-wrap { padding-block: .5rem }", written for a one-row header, outranked that by coming later
   and took it to 99px (layout stress set, 2026-11-04). Its inner padding is furniture too. On a desktop row
   that does not wrap, a wordmark that leaves no room for the number and the button may take a second line
   instead of pushing them off the row, but only once the links are behind the menu button (cx-nav-tight):
   before that the links give first, and a name that shrank alongside them wrapped to three lines in a header
   whose links still fit (a 36-letter name in a heavy face at 1,024). A logo keeps its box. */
@media (min-width: 901px) {
  .r-hdr2 .r-hdr2-in.r-hdr2-in { padding-block: 6px; }
  html.cx-nav-tight .cx-head [class*="-brand"]:not(:has(img)) { min-width: 0; flex-shrink: 1; }
}
/* A flush section draws its own ground edge to edge and pads itself inside (the split-paper hero, the zigzag
   rows); r-section--flush says so. A skin that gave the class a band's padding (Test B: clamp(52px, 7vw,
   100px)) opened a strip of the section's white ground between the header and a dark full-bleed hero on
   every page. Same ruling as the header's: a rule on furniture the skin does not own. */
.r-section.r-section--flush { padding-block: 0; }
/* The service picker is drawn by the site, not the browser (chassis.css): Safari otherwise draws it at
   half the height of the other fields. A skin that sets a field's ground with the background shorthand
   would erase the arrow with it, so the arrow and the appearance are furniture here. */
html .cx-form select:not([multiple]) {
  -webkit-appearance: none; appearance: none; padding-right: 2.4em; line-height: inherit;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 1.2em) 55%, calc(100% - .85em) 55%; background-size: .35em .35em, .35em .35em; background-repeat: no-repeat;
}
/* On a phone the header is ONE row: the brand and the menu button, nothing else (owner ruling R-0826a,
   audit F38; Test A 2026-09-22). The header's own call to action is hidden under the chassis breakpoint
   by every variant, because the fixed action bar carries the ask there; a skin repair once put it back,
   the brand, the button and "Menu" no longer fit 390px, and the button wrapped onto a second row of a
   121px sticky header. Hidden again here whatever the skin says, and the brand may shrink instead of
   pushing the menu down. The open menu is the one thing that takes its own row, and it still does. */
@media (max-width: 900px) {
  .cx-head .r-btn:not(.cx-nav .r-btn), .cx-head [class*="-cta"]:not(.cx-nav [class*="-cta"]) { display: none; }
  /* A zero basis, not "0 1 auto": on a row that wraps, an item moves to a new line before it shrinks, so an
     auto basis let the name keep its full width and sent the button down. The masthead learned this on
     Test D (below); the layout stress set (2026-11-04) found the other three headers had the same bug at
     a 20-character name, every skin, on every phone. */
  .cx-head [class*="-brand"] { min-width: 0; flex: 1 1 0; }
  .cx-head [class*="-brand"] img { max-width: min(62vw, 220px); height: auto; max-height: 48px; }
  /* A brand inside a lockup (the masthead's name over its descriptor) is not the row's flex item; the lockup
     is, and it kept min-width:auto, so the rule above never engaged. And on a row that wraps, a flex item
     moves to a new line before it shrinks. Test D (2026-10-29): "Thistle & Comb Salon" left 9px beside the
     "Menu" button at 390 on a Mac, Linux Chromium sets text a few pixels wider, and the button wrapped onto
     a second row (119px) on every page of the cloud build. A zero basis puts the lockup on the button's line
     first and lets the name take what is left; the ellipsis rule below then holds it to one line. */
  .cx-head [class*="-lockup"] { flex: 1 1 0; min-width: 0; }
  .cx-head [class*="-lockup"] > [class*="-brand"] { max-width: 100%; }
}
/* A brand with no logo is its NAME set as text, and the rule above bounded only an image. Test C
   (Trueline Plumbing, no logo, 2026-09-27): "Trueline Plumbing" at the skin's display size was 237px,
   and beside the menu button it wrapped the header to two rows at 320px on every page. The name now
   scales with the screen on a phone and stays on one line, and at 360px and below the button's word
   "Menu" is kept for screen readers but not drawn, which is what makes room on the smallest phones.
   The line height is set here too, because overflow: hidden (what the ellipsis needs) clips whatever
   leaves the line box: utility-sticky's brand column sets line-height .86 for stacked lockups, and Test C
   attempt 9 (2026-10-28) cut 4px off "Trueline Plumbing" on every page, a finding no skin edit could
   clear because this rule caused it. */
@media (max-width: 420px) {
  /* display: block, because the masthead's and the topbar's names are inline-flex and a browser draws no
     ellipsis on a flex box: a long name was cut mid-letter with nothing to say so (layout stress set, 2026-11-04).
     align-content: center keeps the line in the middle of the masthead's 44px target, as the flex box did. */
  .cx-head [class*="-brand"]:not(:has(img)) { display: block; align-content: center; font-size: clamp(1rem, 6vw, 1.625rem); line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* A lockup's name sits beside the labeled "Menu" button with nothing else to give: at 6vw a twenty-letter
     name had 9px to spare at 390 and Linux text metrics took it, so the ellipsis would have eaten the end of
     "Thistle & Comb Salon" (Test D, 2026-10-29). A step smaller keeps a name that long whole on Linux too;
     the ellipsis stays as the last resort for a longer one. */
  .cx-head [class*="-lockup"] > [class*="-brand"]:not(:has(img)) { font-size: clamp(1rem, 5.5vw, 1.625rem); }
  /* The 404 and privacy pages carry the number beside the name instead of a menu button, and the line
     above cut the name to "Trueline Plum..." there. The name wraps inside its own column instead: still
     one row of controls, and nobody's business name loses its end. */
  .cx-util-head .cx-brand:not(:has(img)) { white-space: normal; overflow: visible; text-overflow: clip; }
}
@media (max-width: 360px) {
  .cx-navbtn-label { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}
/* An eyebrow belongs to its heading (audit F44): the gap is the eyebrow's own bottom margin, 8 to 14px,
   and the heading adds nothing. A skin's prose rule once gave every h2 a 70px top margin, eyebrow or not,
   and a hero h1 with a tight line box rode 3px up into its eyebrow once the eyebrow's margin was gone.
   The eyebrow carrying the gap holds in both cases; loaded after the skin, at a specificity a section
   rule does not reach. */
main :is([class*="eyebrow"], [class*="kicker"]):has(+ :is(h1, h2, h3)) { margin-bottom: var(--sp-eyebrow, clamp(8px, 1vw, 14px)); }
main :is([class*="eyebrow"], [class*="kicker"]) + :is(h1, h2, h3) { margin-top: 0; }
/* Caption children stay in the caption's flow. A skin once wrote .r-tiles [class*="-title"] { position: absolute; bottom: 0 } beside the same rule for -cap, so every tile's title sat on top of its line. Position the caption box, never its children. Two attribute selectors, so this ties the skin's .r-tiles [class*="-title"] on specificity and wins by coming later. */
[class*="-cap"] > [class*="-title"], [class*="-cap"] > [class*="-line"], [class*="-cap"] > [class*="-name"], [class*="-cap"] > [class*="-text"] { position: static; inset: auto; padding: 0; background: none; }
/* Text over a photograph stays readable whatever the skin did to the caption's gradient: a shadow travels with the letters, so a caption that lands on a bright sky still reads. A skin may make it stronger, never absent. */
[class*="-cap"]:not(.r-heritage-cap), .r-tile2-cap, .r-tile-cap { text-shadow: 0 1px 2px rgba(0,0,0,.7), 0 0 14px rgba(0,0,0,.55); }
/* ...and only there: the heritage numeral's caption ("Open since" under the year) is set on a plain ground,
   where the same shadow read as a dark smudge around the words (Test D, 2026-11-05). */
/* A card the renderer draws as a BOX stays a box. A skin may restyle the border, thicken it, colour it
   or take it off every edge; it may not take off ONE edge and leave three. A last-child rule that
   zeroes the right border is a single-row idiom - in one row that edge is the section's own edge, so the
   strip closes itself - and the moment the row count is anything else the last card is a three-sided
   box with its right side open. The owner saw it in a second on a build that had passed 145 checks.
   Restored from the renderer's own token, which is what draws the other three sides. */
.r-step:last-child, .r-metro-card:last-child, .r-svc-card:last-child, .r-tile2:last-child {
  border-right: 1px solid var(--r-line);
}
/* A phone link outside running prose is a real target (audit F47): min-height does nothing on an inline
   anchor, so the footer and contact numbers measured 22px tall. inline-flex gives the 44px its box. A
   link inside a sentence stays inline, where a box would break the line. */
address a[href^="tel:"], address a[href^="mailto:"], .cx-foot a[href^="tel:"], footer a[href^="tel:"], footer a[href^="mailto:"] { display: inline-flex; align-items: center; min-height: 44px; }
/* A hero photograph that fails to load leaves its box, never its alt text printed over the headline
   (audit F48): the words are for a screen reader and still reach one. */
main > section:first-child img, [class*="hero"] img { color: transparent; }
/* An outline button keeps its outline (WCAG 1.4.11; Test B's look): beside a filled button, a ghost button
   with a transparent border reads as loose text, not as the second action. Its boundary is its own text
   color, so it holds on any ground. */
.r-btn.r-btn--ghost { border-style: solid; border-width: max(1.5px, 0.09em); border-color: currentColor; }
/* Tap targets: 44px on everything a thumb lands on. */
.r-btn, .r-chip, .r-ledger-link, .r-textlink, .cx-nav a, .cx-bar a, a[href^="tel:"], .cx-form button, .cx-navbtn { min-height: 44px; }
/* The phone bar's labels keep room: padding inside each link and a gap between the two. A skin restyled the bar with neither and the labels touched on every page. */
.cx-bar { gap: 8px; } .cx-bar a { padding-inline: 12px; }
/* The bar's type is the chassis's too. Two labels share 390px; at 15px bold with .1em tracking "GET A FREE ESTIMATE" is wider than its half and runs off the screen, and two repair runs restyled the bar's type in one round and re-broke it in the next. The same specificity as a skin's .cx-bar a, .cx-bar .r-btn rule, loaded after it. */
.cx-bar a, .cx-bar .r-btn { font-size: 0.875rem; letter-spacing: .03em; line-height: 1.15; white-space: normal; text-transform: var(--r-case-label, uppercase); }
.r-chip, .r-ledger-link, .r-textlink { display: inline-flex; align-items: center; }
/* A live mount is empty until the platform fills it. The snippet stamps data-skyhq-state on it and
   the markup carries an honest line beside it; the line goes the moment real content lands. Without
   this the reviews section rendered a heading over 300px of nothing, which the eye is told to ignore
   because it cannot tell an empty mount from a broken one. */
[data-skyhq-state="ready"] + .r-mount-empty { display: none; }
/* The split card is a panel laid over the photograph, meant as the OPPOSITE of its section. It took
   that from the skin's inverse tokens, and every one of three generated skins resolved them so the
   card's fill and its words were both dark inside a dark section: "Priced before we dig" at 1.02:1.
   The section's own text and background are the one pair every skin must keep legible (the page's
   body copy is judged on them), so the card is that pair, swapped - the same construction as a
   solid button. Borders and radius stay the skin's; only fill and ink are ruled, at 0-3-0. */
.r-split .r-split-card.r-split-card { background: var(--r-fg); color: var(--r-bg); }
.r-split .r-split-card.r-split-card strong { color: inherit; }
.r-split .r-split-card.r-split-card span { color: color-mix(in srgb, var(--r-bg) 82%, var(--r-fg)); }
/* NO floor on the service card's scrim or words, deliberately. One was added (076fb9c) for skins
   that darken the scrim and forget the words, and the sixth Cloud Run build showed why it was wrong:
   that skin had redesigned the card with the photograph BESIDE the text, the floor laid a dark
   scrim under text meant for a light card, and three repair rounds fought it (a colour, a darker
   colour, then !important) while the page stayed failed and no re-look could run. A floor that
   rules half of something a skin has deliberately redesigned is the button mistake again. The
   over-photograph check names the element either way, and the repair loop can read the skin's
   intent; a floor cannot. */
/* Colour the caption box; its children INHERIT. The sibling ruling above says position the box and
   never its children, and colour turns out to have the same shape. A skin repainted a tile caption
   with a black gradient and cream words, which is right, and the section's own rule for the TITLE
   inside it still said var(--r-inverse-fg): that skin reads inverse as a cream panel with near-black
   type, so every service name on the page came out near-black on black at 1.02:1 and simply was not
   there. A child that re-declares its colour defeats whatever correction the box just made, so the
   children stop re-declaring it. The accent line keeps its tint by mixing toward the colour it
   inherits, which is the one colour known to work against that backdrop. */
.r-tile2-cap > *, .r-tile-cap > *, .r-carousel-cap > * { color: inherit; }
.r-tile2-cap .r-tile2-line { color: color-mix(in srgb, var(--r-inverse-accent-text, currentColor) 45%, currentColor); }
/* A photo placeholder is the one piece of copy on the page addressed to the OWNER: it says which
   shot is still missing, and it is the reason a site can ship honestly before every photograph
   exists. A skin set it in 12px uppercase at .18em, and at 75 characters that is an instruction
   nobody reads, so the gap it is meant to advertise stays invisible. It is read, not scanned, so it
   takes the reading floor. */
.r-placeholder, .r-placeholder span { font-size: max(0.875rem, var(--cx-body-s, 0.875rem)); letter-spacing: normal; text-transform: none; }
/* The placeholder paints its OWN background and carries its own words, both renderer furniture, so
   both are ruled together and cannot disagree: stripes from the section's own background tones, the
   words in the section's own foreground. The sixth Cloud Run build's skin striped it near-black and
   left the words the chassis's near-black, 1.03:1, on the instruction that tells the owner which
   photograph to take. Ruling the pair, not half of it, is what keeps this from being the button
   mistake: nothing a skin does can leave the two unmatched. */
.r-placeholder.r-placeholder { background: repeating-linear-gradient(135deg, var(--r-surface, var(--r-bg)) 0 12px, var(--r-bg) 12px 24px); color: var(--r-fg); }
.r-placeholder.r-placeholder span { color: inherit; }
/* The reviews note is a SENTENCE ("We do not screen, edit or reorder them, and nobody is paid for
   one...", 117 characters and up), and its class name reads like a label: two of three generated
   skins set it in 12px tracked capitals, which fails the reading floor and the capitals-prose rule
   and says the one honest thing on the reviews page in the least readable type on the site. */
.r-reviews-note { font-size: max(0.875rem, var(--cx-body-s, 0.875rem)); letter-spacing: normal; text-transform: none; line-height: 1.55; }
/* A caption sits ON the photograph, so the photograph has to be big enough to survive it. At two
   tiles to a 390px row each picture is about 131px tall and a two-line caption covers 63% of it,
   which the third acceptance build shipped at 89%: a gallery that had stopped showing the work. The
   section stylesheet says one column on a phone and a skin outranked it by loading later with no
   media query at all. Only when the tiles CARRY captions: an uncaptioned grid has nothing to cover
   and keeps whatever columns the skin chose. */
@media (max-width: 640px) {
  .r-gallery-grid:has(.r-tile-cap), .r-gallery-grid:has([class*="-cap"]) { grid-template-columns: 1fr; }
}
/* A marquee's track is wider than the screen on purpose: the second copy of its words scrolls in
   behind the first. A skin that caps it (max-width on the track or its spans) drops that copy onto a
   second line and the ticker prints itself twice, which is what shipped. */
.r-marquee-track { width: max-content; flex-wrap: nowrap; }
.r-marquee-track > * { max-width: none; }
/* A section is a full-width band. Its gutter is its wrap's width and its measure is inside it; a skin that
   narrows the section element itself (a .r-prose max-width meant for the text: third acceptance build,
   every desktop service page 685px wide and flush left) narrows the band, and the normalizer records the
   removal. This floor is the same ruling for a selector the normalizer does not read, at equal specificity. */
main > section, .r-section { width: auto; max-width: none; }
/* The phone bar's clearance under a first screen sized to the viewport, once. Only the anchored hero between
   641 and 900px is viewport-sized with the bar showing; under 640px it is content-sized, and padding below
   content that already runs past the first screen is a dark band (164px on the third acceptance build, when
   the base sheet reserved the bar on the section AND its wrap). After the skin, so a skin's hero padding
   cannot remove it; the [class] attribute lifts it over a two-class skin selector. Only WITHOUT script now:
   with script the bar stays hidden until the first section has scrolled away (the 2026-08-28 ruling,
   finally implemented in the chassis), so there is nothing at rest for the hero to clear. */
@media (min-width: 641px) and (max-width: 900px) { html:not(.js) main > section.r-hero--anchored:first-child[class] { padding-bottom: calc(var(--cx-bar-h) + var(--cx-gap)); } }
