/* Alpha Appliance Service - site styles
   Extracted from index.html on 2026-10-11 so that additional pages can share
   one stylesheet instead of carrying their own copy.
   NOTE: url() paths here are relative to THIS file, i.e. assets/css/ - so an
   image in assets/img is ../img/name.png, not ./assets/img/name.png. */
:root {
    --bg: #2c0735;
    --panel: #4e0b5e;
    --text: #f8fafc;
    --muted: #cbd5e1;
    --accent: #e51837;
    --gold: #b7884c;
    --border: rgba(255, 255, 255, .12);
    --radius: 16px;

    /* THREE WIDTHS, and the difference between them matters.
       --max         the full page width: the header bar's contents, the footer.
       --content-max how wide the content COLUMN gets. Steps up twice in the
                     media queries at the bottom of this file - change it there.
       --gutter      the breathing room between content and the screen edge.
                     ONE source for it, because two things need to agree: the
                     .container's side padding, and the inset on the module
                     bubbles. Those bubbles ARE their .container, so unlike the
                     hero's panels they get no gutter for free and have to build
                     one - see the module-box rule. When they disagreed, the
                     bubbles sat 2x gutter wider than the hero panels AND ran
                     flush to the screen edge on a phone. */
    --max: 1100px;
    --content-max: 720px;
    --gutter: 18px;
    /* Inner gradient switched off — it shaded the top of every panel and
       button too heavily. Left as a transparent no-op rather than deleted,
       so the rules that reference it stay intact and it can be restored by
       putting the two inset layers back here. */
    --shadow-inset: 0 0 0 rgba(0, 0, 0, 0);
    --shadow-card: 0 12px 30px rgba(0, 0, 0, .6);

    /* 1970s appliance palette for the Services icons. Averaged from the four
       photos in "File Analysis Folder\green appliances" - the samples ran
       #6D785A to #A6AA7C depending on the light each was shot in, so the set
       uses one green rather than four. Retune the whole set from right here. */
    --avocado: #93995f;
    --avocado-dark: #6d7848;
    --chrome: #dfe3e8;

    /* JADE, his colour, added 2026-10-15 as a third accent. It exists because
       gold was doing two jobs - headings AND small print - and because the
       header is full of teal that nothing else on the page picked up.
       #04c2bb WAS TRIED THE SAME DAY AND REVERTED - he did not like it. Both
       are kept in the table below because the comparison is the useful part:
       the two colours want OPPOSITE type on a fill. Measured:
                               #0a8268   #04c2bb
         as text on the page     3.71      7.94   both better, now comfortably
         as text on a review card 3.08      6.59   so the small type passes too
         WHITE on it as a fill    4.56      2.13   <- this is what broke
         page-purple on it        3.71      7.94
       So the three pills - the two hours badges and the voicemail line - carry
       WHITE text, which is right for #0a8268 and wrong for #04c2bb. If this
       value is ever brightened again, those three have to flip to var(--bg) in
       the same commit. The fills and the text pull opposite ways. */
    --jade: #0a8268;
    /* THE SECOND TONE, 2026-10-17. --jade darkened 30%, used as the bottom of a
       two-tone fill on everything that is a filled jade bubble: the two hours
       pills, the voicemail line and the cat-gallery switch. Light from above, so
       the gradient runs base at the top to this at the bottom.
       ON THE THREE PILLS THIS IS FREE CONTRAST - their text is white, which goes
       from 4.77:1 at the top of the bubble to 7.96:1 at the bottom.
       ON THE CAT BUTTON IT COSTS: that text is the page purple at Kevin's ask,
       and purple over this tone is 2.22:1 against 3.71:1 over the base - the
       gradient was widened from 18% to 30% on 2026-10-17 at his ask and that
       took the button with it. White text there would read 4.77-7.96 like the
       pills do, and would make all four match. Recommended twice, his call. */
    --jade-dark: #075c4a;

    /* THE GOLD SHEEN, in one place. Used by the gallery tiles, the enlargement
       bubble and (desktop only) every button and service bubble - four rule
       blocks that must stay the same gold, which is why it is a variable rather
       than four copies. The .52 in the middle is the brightness dial: that is
       the peak of the highlight, the rest of the stops are its falloff.

       RESATURATED 2026-10-15: Kevin saw the sweep as "more silver" on some
       buttons and "golder" on others. The gradient was always the same - what
       differed was what sits UNDERNEATH it. A translucent wash takes on its
       backdrop, so the old pale cream at .40 read gold over the dark purple and
       pinkish-white over the red buttons. The gold is stronger and less washed
       out now, so the band carries its own colour instead of borrowing the
       button's. Some of the backdrop always shows through - that is what
       translucent means - but this is the lever that controls how much. */
    --sheen-grad: linear-gradient(90deg,
            rgba(183, 136, 76, 0) 0%,
            rgba(198, 152, 92, .20) 30%,
            rgba(226, 180, 110, .52) 50%,
            rgba(198, 152, 92, .20) 70%,
            rgba(183, 136, 76, 0) 100%);

    /* SHEEN SPEED, read by site.js. --sheen-speed is pixels per second: the band
       crosses a wide button and a small one at the SAME rate, which means the
       duration has to differ, which is why a script is involved at all (CSS can
       only fix the duration). 106 is measured off the header's gold review icon
       button - 55px wide, 1s, the one Kevin picked as the reference.
       --sheen-max is a ceiling in ms, and it is a genuine compromise: at 106px/s
       the widest button on the site would take 6.4 seconds, which stops reading
       as a flourish. Raise it for a slower, longer sweep on the big elements.
       2200 replaced 3500 on 2026-10-15 - he found the big ones draggy, the
       service bubbles most of all. The arithmetic: anything wider than
       max * speed / 1935 (about 120px here) runs at the ceiling, so the ceiling
       IS the speed for the service bubbles, the gallery tiles and the wide
       buttons, and only the small ones still scale. His reference, the header
       icon button, is untouched at 1s either way. */
    --sheen-speed: 106;
    --sheen-max: 2200;
}

* {
    box-sizing: border-box;
}

html {
    scroll-behavior: smooth;
}

body {
    margin: 0;
    font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.5;
}

img {
    max-width: 100%;
    height: auto;
    display: block;
}

a {
    color: inherit;
    text-decoration: none;
}

.container {
    max-width: var(--max);
    margin: 0 auto;
    padding: 0 var(--gutter);
}

/* Header */
.site-header {
    position: sticky;
    top: 0;

    /* HEADER WATERMARK, 2026-10-15 - his third attempt at one, and the first with
       a WIDE source. The two that were scrapped in September failed for reasons
       worth not repeating: the first was a square photo, and this bar is roughly
       11:1, so `cover` showed about a tenth of it and threw the composition away;
       the second was a room full of COIN-OP machines, which quietly says he
       services commercial equipment. The image in use now is the third candidate
       he made: the appliances in it are a domestic line-up - two front loaders,
       a range, a microwave and a fridge - under a neon crossed-wrench sign.
       An EARLIER candidate arrived LETTERBOXED, 1482x1061 with black bars, which
       made it look like a 1.4:1 square when the picture inside was 2.6:1. Always
       measure the picture, not the file.
       The source in use is "Header Final.png", 2073x758, cropped to 2036x758 on
       2026-10-16 to centre the sun under the logo - see the note on the media
       query below.
       TO REVERT: delete the four background-* lines below, and the media block
       further down. The background-color stays behind them and is exactly what
       the header was before, which is also the fallback if the file ever fails
       to load. The previous sunset pair is kept in _tools as
       header-bg_sunset.webp / header-bg-wide_sunset.webp - swapping back is a
       copy, not a rebuild.
       THE TWO DIALS:
         .72 in the gradient - the purple wash laid OVER the photo. Lower shows
              more image. He approved this value. The first thing to suffer if it
              goes lower is the nav links; they are --muted, the dimmest text in
              the bar, and they sit over the middle-right.
         76%  in background-position - WHICH horizontal band of the photo shows.
              THIS IS NOW THE PHONE AND TABLET NUMBER ONLY. 1200px and up was
              taken to 65% with a 184px bar later the same day, in the header
              editor, and that override lives under the .header-inner min-height
              block further down. Everything below reads as the history of this
              value; the measured result of the newer pair, looked at rather
              than calculated, is: 1366 shows the whole sun with the machines
              nearly whole, 1920 and 2560 clip the TOP of the sun - which lands
              behind the logo, so it reads as sky rather than as a mistake.
              Same fixed-window arithmetic as always: a wider screen is a
              thinner slice, 269 source rows at 1366 against 144 at 2560.
              50% -> 76% on 2026-10-16: at 50% the strip cut straight across the
              washer and dryer doors, which was his complaint. The doors sit at
              source rows 490-620 and the range front at 440-630; at 76% a 1440
              bar shows rows 402-631, so they are whole. 70% was tried first and
              still clipped the bottom of the doors by about 20 rows.
              ONE WIDTH IT CANNOT FIX: at 2560 the window is only 127 source rows
              and the doors are 130 tall, so on a very wide monitor they cannot
              be whole at any position. That is the fixed-window problem below.
              READ THIS BEFORE CHANGING IT AGAIN. He asked to "scale the image
              out", and that is NOT what this number does - it slides the window,
              it does not widen it. The window is a FIXED number of source rows:
              barHeight / barWidth x imageWidth, which is 229 rows of 758 at a
              1440 bar. Nothing in CSS changes that. Making it bigger needs
              either a WIDER CANVAS (which is the purple-margin fade he rejected
              the same day) or a TALLER BAR - and fitting the sun AND the
              appliance faces together, rows 230 to 640, would take a 276px
              header. So this is a choice between them, not a setting.
              What 70% costs: the sun is above the frame now. The crop that
              centred it still holds if he ever slides back up.
       No JPEG twin: a CSS background has no <picture> to fall back through, and
       the graceful failure here is simply the flat purple bar. */
    background-color: rgba(44, 7, 53, .92);
    background-image:
        linear-gradient(rgba(44, 7, 53, .72), rgba(44, 7, 53, .72)),
        url('../img/header-bg.webp');
    background-repeat: no-repeat, no-repeat;
    background-position: center, center 76%;
    background-size: auto, cover;

    border-bottom: 1px solid var(--border);
    z-index: 50;
    backdrop-filter: blur(8px);
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

/* THE FADE IS GONE, 2026-10-16, at his ask: "the edges of the header image seem
   to fade to background color purple instead of the image truly stretching from
   side to side."
   HISTORY, because the fade was not decoration and somebody will want to put it
   back. On 10-15 he wanted more of the picture visible, and `cover` is driven by
   WIDTH - the wider the window, the bigger the photo is scaled and the thinner
   the slice that fits the bar. CSS cannot zoom out: anything smaller than cover
   stops spanning the bar. The only lever was a WIDER CANVAS, so the picture was
   put on one padded 25% per side with the ends dissolved into the page purple,
   which bought 35% of the picture in frame instead of 22%.
   What changed since: the logo got bigger, which made the bar taller, which
   shows more of a backdrop by itself. Edge to edge now gives 30% at a 1440 bar -
   nearly all of what the fade was buying, without the fade. THAT TRADE IS THE
   WHOLE DECISION HERE: no purple margins, about five points less picture.
   Both files are now the same photo at two resolutions, 2000px for desktop and
   1400 for phones, so this media query is purely a bandwidth switch and both
   ends of it show exactly the same framing.
   THE SUN IS CENTRED ON THE LOGO, also his ask. Measured rather than nudged: the
   disc's midpoint sat 18.5px left of the image's own centre, so 37px came off
   the RIGHT edge of the source and the disc now lands within 2px of centre on
   every row sampled. The logo is centred in a .header-inner that is centred in
   the viewport, and the background is on .site-header which is full width, so
   image centre and logo centre are the same point.
   The previous faded pair is kept in _tools as header-bg_faded.webp and
   header-bg-wide_faded.webp - going back is a copy, not a rebuild. */
@media (min-width: 901px) {
    .site-header {
        background-image:
            linear-gradient(rgba(44, 7, 53, .72), rgba(44, 7, 53, .72)),
            url('../img/header-bg-wide.webp');
    }
}

/* PHONES GET PLAIN `cover` AND THAT IS THE CLOSEST THEY CAN GET TO THE DESKTOP
   FRAMING, 2026-10-15. He asked for mobile to match desktop - the bust at the
   left, the appliances at the right - and this is the setting that does it,
   because cover on a bar wider in ratio than the picture shows the FULL WIDTH.
   An earlier 120% zoom was removed for exactly that reason: it was cropping 17%
   off the sides, and on the phone file those sides are real picture, not fade.
   WHAT CANNOT BE MATCHED, and the arithmetic is worth keeping so nobody tries
   again: with the full width showing, the number of picture rows that land in
   the bar is barHeight / barWidth x pictureWidth. A phone bar is about 390x82,
   so that is 0.21 x 1482 = roughly 311 of the 570 rows, 55%. The desktop bar is
   the whole viewport wide - the background is on .site-header, not on the
   1100px .header-inner inside it - so at 1440x124 it is a far flatter box and
   takes 34%, and a tablet at 760 lands in between at 28%. Showing MORE width
   and a THINNER band are the same dial pulling opposite ways; the only other
   lever is a taller phone header. Full width wins because that is what he
   asked for by name. */

/* FULL WIDTH, 2026-10-16, set by Kevin in _tools/header-editor.html at a
   simulated 1920 and copied out of it.
   WHAT IT FIXES: .header-inner is a .container, so it inherited
   `max-width: var(--max)` = 1100px. On a 1920 monitor that penned the whole bar
   into the middle 1100px with 410px of dead space either side, which squeezed
   the nav and the two buttons in against the centred logo. His words: "the new
   seperated review and gallery buttons are crowding the logo in the center."
   Widening the box IS the fix - the nav goes left, the buttons go right, and the
   logo keeps the true centre.
   `none`, not a bigger number: he dragged the edges to the screen edge at both
   1920 and 2560, so the intent is "use the window", and one keyword does that at
   every width instead of a cap that would come back as dead space on the next
   monitor. Below 1100px nothing changes - the cap was never reached there.
   SAFE BECAUSE IT IS SCOPED TO THE HEADER: --max still governs the footer and
   every .container below it. Do NOT "fix" this by raising --max.
   The 7px side padding is what keeps the nav off the glass. */
.header-inner {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 8px 7px 4px;
    flex-wrap: nowrap;
    min-height: 72px;
    max-width: none;
    justify-content: space-between;
}

/* phone above the logo, hours below — stacked and centred on the logo */
.brand {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 0;
}

/* LOGO CENTRED OVER THE SUN, 2026-10-15 (his ask). The sun's weighted centre is
   at 51% of the picture's width - measured by counting sun-pink pixels per
   column, not judged by eye - and the picture is centred in the bar, so the bar's
   own centre IS the sun.
   Taking .brand out of the flex row is what allows a TRUE centre: left in the
   row it would only ever centre in the space left over by the nav and the
   buttons, which are not the same width. The two knock-ons are both handled:
     - the row's height came from the logo, so min-height has to carry it now.
       124px = the 112px logo + the 12px of vertical padding, and 104px at the
       narrower desktop where the logo drops to 92px.
     - space-between would have parked the review button in the middle, under the
       logo. margin-right:auto on the nav pins the nav left and keeps the two
       buttons together on the right instead.
   The fit check in site.js still measures .brand's width, which is still real,
   so the hamburger still appears at the width where these would collide.
   PHONES ARE LEFT ALONE on purpose: the bar is short there and the logo would
   sit on top of the review button and the hamburger. */
@media (min-width: 901px) {

    /* 160px: set by Kevin, 2026-10-16, after working the bottom edge in the
       header editor - 162 originally, then 235, then 184, settled at 160.
       901-1199 overrides this back to 116px below, so this number is really
       "1200 and up".
       IT IS ALSO A PICTURE CONTROL, which is the part to remember: the band of
       photo that lands in the bar is barHeight / barWidth x imageWidth, so each
       of those heights was a different crop - 245 source rows at 235, 192 at
       184, 167 here. That is why the background-position moved with it, and why
       changing this number alone will re-frame the picture.
       THE LOGO IS 150px AND THIS IS 160px, so there are only 10px of slack left.
       Going lower starts squeezing the logo rather than the picture - that is
       the floor, not a preference. */
    .header-inner {
        min-height: 160px;
    }

    /* flex-start is only right while the nav is SHOWING. Collapsed, the row is
       just the review button and the hamburger, and they need to go to opposite
       ends - which is what the base space-between already does. */
    body:not(.nav-collapsed) .header-inner {
        justify-content: flex-start;
    }

    /* .site-header .main-nav, not .main-nav: the base rule further down carries
       margin-left:auto, which is what USED to shove the nav over to the right to
       join the buttons while the logo held the left. That has to be undone here
       or the nav floats into the middle, under the logo. Scoping to the header
       wins on specificity rather than on source order. */
    .site-header .main-nav {
        margin-left: 0;
        margin-right: auto;
    }

    /* SIDES SWAPPED, 2026-10-16, his ask: nav links to the right, the two
       buttons to the left. Done with flex `order` rather than by moving the
       markup, because the DOM order is what a screen reader and the keyboard
       follow, and the logo-brand-first, nav-second reading order is the right
       one however it is painted.
       `margin-left: auto` on the nav is the whole mechanism: it eats the free
       space to the nav's left and shoves it against the right edge, leaving the
       two buttons packed at the left. It has to override the margin-right:auto
       set three lines above, which is why this rule is more specific rather
       than just later.
       SCOPED TO body:not(.nav-collapsed) ON PURPOSE. Collapsed - which can
       happen above 901px too, if the row stops fitting - the row is only the
       review button and the hamburger, and the base space-between puts one at
       each end. An unscoped `order` on the review button would have reordered
       that pair and flipped the phone header, where he asked for the review
       button on the LEFT back on 10-15.
       Button order within the pair, settled 2026-10-16: the gold review icon
       furthest left, then the intake form button. Swapping them is swapping
       these two numbers. It also matches the phone header, where the review
       button has been the left-hand item since 10-15. */
    body:not(.nav-collapsed) .site-header .btn-review {
        order: 1;
    }

    body:not(.nav-collapsed) .site-header .btn-primary.desktop-only {
        order: 2;
    }

    body:not(.nav-collapsed) .site-header .main-nav {
        order: 3;
        margin-left: auto;
        margin-right: 0;
    }

    .brand {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        z-index: 2;
    }
}

@media (min-width: 901px) and (max-width: 1199px) {
    .header-inner {
        min-height: 116px;
    }
}

/* THE PICTURE SITS HIGHER ON BIG SCREENS ONLY, 2026-10-16 - 65% here against
   the base 76%, from Kevin's drag in the header editor at a simulated 1920.
   WHY IT IS SCOPED AND NOT JUST CHANGED: the base 76% is the number he called
   "perfect" on the phone the same day, and a phone bar is a far squarer box - at
   390px wide and 88px tall it shows 451 of the 745 rows, so 11 points of
   position moves the picture 32 rows there. Changing the base would have quietly
   re-cropped the version he cares most about. MOBILE IS THE PRIMARY TARGET: when
   a framing number only suits one end, scope it to that end.
   1200px matches the bar-height band above, so the taller bar and the higher
   crop arrive together - they are one composition, not two settings. */
@media (min-width: 1200px) {
    .site-header {
        background-position: center, center 65%;
    }
}

/* THE SAME CENTRED LOGO ON PHONES, 2026-10-15, with the review button moved to
   the left to balance it - his ask. The row ends up review | logo | hamburger,
   and it needs no justify-content of its own: the base space-between puts the
   two real flex items at opposite ends, and the logo is out of the flow in the
   middle. The push-right on the review button had to come off for that; see the
   rule above.
   MEASURED BEFORE COMMITTING, because this bar is narrow and the three things
   nearly meet. At 390px wide the inner row is 376px: the review icon takes 48,
   the hamburger about 46, and the 70px-tall logo is 143 wide, so it sits with
   roughly 68px of clear air either side. At 360px it is still about 73px.
   Only 521-900px needs the min-height: there the logo is 70px and the bar took
   its height from it. At 520px and below the logo drops to 56 and then 46, both
   of which fit inside the base 72px min-height, so those widths are untouched. */
@media (max-width: 900px) {
    .brand {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        z-index: 2;
    }
}

/* THE LOGO, GROWN AS FAR AS IT SENSIBLY GOES, 2026-10-15 ("i want it as big as
   we can fit"). It is NOT width that stops it: measured against the real header
   row - the three nav links, the stacked Reviews & Gallery, the review icon and
   the portal button, all at their real font sizes - a centred logo could
   be 434px wide before it came within 24px of anything.
   THAT 434 IS NOW ABOUT 341, re-measured 2026-10-20: the portal button was
   relabelled from "New Customers" to "New Customer Intake Form" and grew 92.5px.
   The logo renders 306.1px wide at its 150px cap, so it STILL FITS - but the
   headroom fell from 128px to 35px. DO NOT GROW THE LOGO AGAIN without
   re-measuring this row, and if the hamburger starts appearing on real desktops,
   shorten that button rather than shrinking the logo. At the 600x294 artwork's
   shape that is 212px TALL, which would mean a 224px sticky bar eating a quarter
   of a laptop screen on every scroll. So the real limit is how much of the screen
   the bar may keep, and these numbers are that judgement, not a measurement:
     >=1200   logo 150 -> bar 162   (was 112 -> 124)
     901-1199 logo 104 -> bar 116   (was  92 -> 104)
     521-900  logo  94 -> bar 106   (was  70 ->  82)
     <=520    logo  76 -> bar  88   (was  56 ->  72)
     <=400    logo  62 -> bar  74   (was  46 ->  72)
   Every bar is logo + 12px, which is the 8/4 vertical padding.
   THE FIT CHECK STILL CLEARS: at the narrowest desktop width, 901px, the row now
   needs about 806px of the 901 available, so the nav does not collapse any
   earlier than it used to. And the picture was re-cropped to match - a taller bar
   shows more of a backdrop by itself, so the desktop canvas needed LESS side
   padding (6.8% instead of 25%) to keep the same 35% of the picture in frame. */
@media (min-width: 521px) and (max-width: 900px) {
    .header-inner {
        min-height: 106px;
    }
}

@media (max-width: 520px) {
    .header-inner {
        min-height: 88px;
    }
}

@media (max-width: 400px) {
    .header-inner {
        min-height: 74px;
    }
}

.brand img {
        height: 150px;
    width: auto;
}

/* the whole logo is the call link now — the number is printed in the
   artwork, so a separate gold line above it was saying it twice */
.brand-logo {
    display: block;
}

/* opening hours, sitting just above the Call / Text pair. A JADE PILL since
   2026-10-15: green reads as "open" without needing a word for it.
   THE TYPE IS WHITE, which is right for #0a8268 at 4.56:1. A brighter #04c2bb
   was tried the same day and reverted; on that one white fell to 2.13:1 and
   this had to become var(--bg). Brighten the accent again and this flips too.
   width:max-content plus auto margins is what centres it: it is still a block,
   so it keeps its own line, but it shrinks to the words. Centred rather than
   left like the paragraph above it, because it belongs to the Call/Text pair
   directly below, which is centred. */
.panel-hours {
    width: max-content;
    max-width: 100%;
    margin: 0 auto 10px;
    padding: 5px 12px;
    border-radius: 999px;
    background-color: var(--jade);
    background-image: linear-gradient(180deg, var(--jade) 0%, var(--jade-dark) 100%);
    color: #fff;
    font-size: .8rem;
    font-weight: 700;
    letter-spacing: .1em;
}

.main-nav {
    display: flex;
    gap: 10px;
    margin-left: auto;
    white-space: nowrap;
}

.main-nav a {
    color: var(--muted);
    padding: 10px 8px;
    border-radius: 10px;
}

.main-nav a:hover {
    color: var(--text);
    background: rgba(255, 255, 255, .06);
}

/* GONE 2026-10-16: .nav-stacked, the three-line "Reviews & Gallery" item that
   used to sit outside .main-nav. Kevin asked for two separate links, so the
   markup now carries them as ordinary nav links and these rules styled nothing.
   Removed rather than left as dead weight - the four rules were: display:block
   with text-align:center and line-height 1.15 on the anchor, display:block on
   its spans to stack them, opacity .6 on the "&", and the shared hover. */

/* Buttons */
.btn {
    padding: 10px 14px;
    border-radius: 12px;
    font-weight: 700;
    font-size: 1.05rem;
    border: 1px solid var(--border);
    display: inline-block;
    transition: transform .08s ease, background .15s ease, opacity .15s ease;
    white-space: nowrap;
    max-width: 100%;
}

.btn:active {
    transform: translateY(1px);
}

/* red face, thin dark line, gold outer ring — the logo A's edge treatment.
   Letters stay plain white: gold on red never read cleanly at button size. */
.btn-primary {
    background: var(--accent);
    color: #fff;
    border: 2px solid #1c1008;
    box-shadow: 0 0 0 2px var(--gold);
}

/* SOLID JADE FACE — the gallery switch, Kevin 2026-10-16. Gold was tried first
   and he didn't like it. Same shape as .btn-primary above - face colour, thin
   dark line, outer ring - which keeps it in the family while making it
   unmistakably NOT one of the red action buttons: nobody should confuse "look
   at the cats" with "book a repair". The ring is GOLD rather than red here
   because red beside jade reads as Christmas. Revert is this whole block.

   TEXT IS THE PAGE PURPLE at his request, and the number he should know:
   #2c0735 on #0a8268 is 3.71:1. That is under the 4.5:1 bar for text this size
   (bold only counts as "large" from 18.66px, and this is 16.8px falling to
   ~12.9px on a phone). Two one-word fixes if he wants it to pass: white text
   here is 4.77:1, or keep the purple and brighten the face to #0DA382 - the
   same hue - for 5.53:1. Shipped as asked; the choice is his.

   THE SHEEN IS THE SITE-WIDE GOLD ONE, deliberately NOT overridden. While this
   button was gold I redefined --sheen-grad here as a white band, because gold
   over gold is invisible. Kevin spotted immediately that it no longer matched
   the rest of the site. On jade the gold band reads perfectly well, so the
   override is gone and this button inherits --sheen-grad like every other
   button. If the face colour ever changes again, check the band against it
   before adding a local override - matching the rest of the site wins. */
.btn-teal {
    background-color: var(--jade);
    background-image: linear-gradient(180deg, var(--jade) 0%, var(--jade-dark) 100%);
    color: var(--bg);
    border: 2px solid #1c1008;
    box-shadow: 0 0 0 2px var(--gold);
}

/* THE CAT LABEL IS 33% WIDER THAN THE OLD ONE - measured, not estimated: 332.9px
   against 252.6px at 16.8px Segoe UI Bold, and 69px of that is the three emoji
   alone (Segoe UI Emoji advances 1.375em per glyph, far wider than a letter).
   On a 390px phone the button has 280px of text room, so at full size it would
   overshoot by 53px.
   Kevin's standing rule is SHRINK, DON'T WRAP, so: min(3.3vw, 1.05rem), nowrap.
   3.3vw is ~6% under the 12.6px that fits a 360px screen and still fits a 320px
   one; the cap means desktop is untouched at 1.05rem, reached at 509px.
   SIZED PER LABEL via data-roll, which site.js sets: the return label ("Repair
   Photos" plus three tool emoji, 180px measured) needs no shrink at all - it
   fits a 360px phone with 70px to spare - so it keeps full size rather than
   being dragged down by the long one. */
#gallery-toggle[data-roll="work"] {
    font-size: min(3.3vw, 1.05rem);
}

/* gold outline button — the Google review link */
.btn-review {
    background: transparent;
    border: 1px solid var(--gold);
    color: var(--gold);
}

.btn-review:hover {
    background: rgba(183, 136, 76, .14);
}

/* icon-only variant, used in the header so the button takes a fraction of the
   width the words did. The icon file is already gold with a transparent
   background, so it needs no filtering here. line-height:0 stops the inline
   box from adding a few stray pixels under the image. */
.btn-icon {
    padding: 5px 9px;
    line-height: 0;
}

.btn-icon img {
    display: block;
    height: 28px;
    width: auto;
}

/* Stays visible when the nav collapses. MUST stay scoped to .site-header:
   unscoped, this also caught the big Leave A Review button at the foot of the
   Contact box and squeezed it to 96px of .82rem text on every phone, because
   body.nav-collapsed .btn-review (0,2,1) outranks .btn-large (0,1,0).
   The width allowance that used to live here is gone with the words — the
   header button is an icon now, so all it still needs is its own padding.
   THE PUSH RIGHT IS GONE, 2026-10-15: it used to carry this button and the
   hamburger over to the right-hand end, with the logo holding the left. Now the
   logo is centred and Kevin asked for this button on the LEFT, so the row is
   left this button and the hamburger with the base space-between between them -
   which puts one at each end on its own. */
body.nav-collapsed .site-header .btn-review {
    padding: 5px 8px;
}

body.nav-collapsed .btn-icon img {
    height: 24px;
}

/* long label, so it must be allowed to wrap — nowrap made this button wider
   than a phone screen, which was dragging the whole page sideways */
.btn-large {
    font-size: 1.25rem;
    padding: 14px 20px;
    white-space: normal;
    max-width: 100%;
}

/* Hamburger (only shown via .nav-collapsed rules) */
.hamburger {
    background: transparent;

    /* same greek key frame instead of the plain square box */
    border: 9px solid transparent;
    border-image: url('../img/greek-key-frame.png') 57 round;
    padding: 6px;
    display: inline-flex;
    flex-direction: column;
    gap: 6px;
    cursor: pointer;
}

.hamburger-bar {
    width: 26px;
    height: 3px;
    background: var(--accent);
    border-radius: 2px;
    display: block;
}

/* Mobile menu */
.mobile-menu {
    overflow: hidden;
    max-height: 0;
    opacity: 0;
    transform: translateY(-6px);
    transition: max-height 280ms ease, opacity 220ms ease, transform 220ms ease;
    border-top: 1px solid var(--border);
    background: rgba(44, 7, 53, .98);

    /* NEW shadow */
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

/* the bottom padding used to be an empty <br> sitting in the grid as a spare
   row. Same gap, but as spacing rather than as markup. */
.mobile-menu-inner {
    padding: 12px 0 48px;
    display: grid;
    gap: 10px;
}

.mobile-link {
    padding: 12px 10px;
    font-size: 1.1rem;
    color: var(--muted);
    border-radius: 10px;
    border: 1px solid var(--border);
    background: rgba(255, 255, 255, .03);
}

.mobile-link:hover {
    color: var(--text);
    background: rgba(255, 255, 255, .08);
}

/* the red call / text actions at the foot of the menu */
.mobile-cta,
.mobile-cta:hover {
    background: var(--accent);
    color: #fff;
    border: 2px solid #1c1008;
    box-shadow: 0 0 0 2px var(--gold);
    font-weight: 700;
}

/* Only allow mobile menu to exist when nav is collapsed */
body:not(.nav-collapsed) .mobile-menu {
    display: none;
}

/* When open */
body.menu-open .mobile-menu {
    opacity: 1;
    transform: translateY(0);
}

body.menu-open main {
    filter: blur(2px);
    pointer-events: none;
    user-select: none;
}

body.menu-open .mobile-menu,
body.menu-open .site-header {
    pointer-events: auto;
    filter: none;
}

/* Desktop vs mobile controls */
.desktop-only {
    display: inline-block;
}

.mobile-only {
    display: none;
}

/* When collapsed -> hide desktop nav + portal, show hamburger */
body.nav-collapsed .main-nav,
body.nav-collapsed .desktop-only {
    display: none !important;
}

body.nav-collapsed .mobile-only {
    display: inline-flex !important;
}

/* Hero */
/* top padding trimmed: the technician photo sits directly above, and 48px
   left too much air between it and the wordmark. The 900px rule below sets
   its own value, so this only affects tablet and desktop. */
.hero {
    padding: 30px 0 48px;
}

/* Single column at every width, so desktop keeps the mobile top-to-bottom
   order: write-up, button, note, then the industry paragraph underneath. */
.hero-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 18px;
    max-width: var(--content-max);
    margin-left: auto;
    margin-right: auto;
}

.hero h1 {
    margin: 0 0 10px;
    font-size: clamp(1.5rem, 2.6vw, 2.05rem);
    line-height: 1.15;
}

.hero-text {
    text-align: center;
}

/* THIS is the container .panel-note-last measures, not .hero-text above.
   The nearest inline-size ancestor wins, and this is the box the line actually
   sits in - .hero-text is 36px wider, because this panel's own 18px of side
   padding is subtracted from it. Measuring .hero-text is what left the nowrap
   line with only accidental headroom.

   SCOPED TO .hero-text's PANELS ON PURPOSE - DO NOT widen this to .hero-panel.
   `container-type: inline-size` also applies `contain: inline-size`, which
   computes an element's intrinsic inline size AS IF IT HAD NO CONTENTS. The two
   panels in here are plain blocks in a stretched column, so their width comes
   from the parent and containment costs nothing. But the Mission Statement
   panel lives in .hero-images, which sets `justify-items: center` - that makes
   it a SHRINK-TO-FIT grid item whose width comes from its own max-content. With
   containment that max-content is 0, so the panel collapsed to its 36px of
   padding. Applying this to .hero-panel broke the Mission Statement outright.
   Nothing in that panel uses cqw, so it needs no container at all. */
.hero-text .hero-panel {
    container-type: inline-size;
}

/* The brand mark above the h1; fills the column width but won't blow past its
   own artwork size on a wide screen.
   560px WAS CHOSEN FOR THE EXPANDED WORDMARK AND STILL FITS THE WINGED A that
   replaced it on 2026-10-17: the wordmark was 1120x645 and drew a 560x323 block,
   the winged A is 600x294 and draws 560x274, so the swap moves the rest of the
   hero up by 49px and nothing else. If that mark ever reads too big for a
   monogram, this is the one number to change. */
/* the <picture> wrapper is inline by default, which would stop the image
   centring the way it did as a bare <img> */
.hero-logo-pic {
    display: block;
}

.hero-logo {
    width: 100%;
    max-width: 560px;
    height: auto;
    margin: 0 auto 10px;
}

.hero-sub {
    margin: 0 0 36px;
    font-size: clamp(1.05rem, 2vw, 1.25rem);
    font-weight: 600;
    color: var(--text);
}

/* small credential line under the service-area headline */
.hero-licensed {
    margin: -26px 0 36px;
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--gold);
}

.hero-about {
    font-size: 1rem;
    color: var(--muted);
    margin: 0 0 18px;
}

/* font-size:1.7rem and color:var(--text) removed 2026-10-17 - the section-title
   treatment below lists .mission-title at the same (0,1,0), same context, later
   in the file, so neither had applied since 2026-10-14. */
.mission-title {
    margin: 0 0 12px;
    text-align: center;
    font-weight: 700;
    line-height: 1.2;
}

.hero-thanks {
    margin: 14px 0 0;
    font-size: 1.4rem;
    font-weight: 600;
    color: var(--text);
}

/* Raised panels in the hero — same depth treatment as the service cards */
.hero-panel {
    background: rgba(255, 255, 255, .04);
    padding: 18px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    box-shadow: var(--shadow-card), var(--shadow-inset);
    margin: 0 0 18px;
}

.hero-panel> :last-child {
    margin-bottom: 0;
}

.lead {
    font-size: 1.1rem;
    color: var(--muted);
    margin: 0 0 12px;
}

/* Full-width button: matches the width of the text block above it.
   Its label is long and its width is fixed, so the type scales with the
   viewport — sized to stay on one line right down to a narrow phone. */
.btn-block {
    display: block;
    width: 100%;
    text-align: center;
    white-space: normal;
    font-size: clamp(1rem, 3.9vw, 1.75rem);
}

/* Call / Text, side by side and equal width above the form button.
   Short labels, so these can carry the largest type on the page. */
.btn-pair {
    min-width: 140px;
    text-align: center;
    font-size: 1.35rem;
}

.hero-actions {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    justify-content: center;
    margin-top: 16px;
}

/* Technician photo, full width band above the hero */
.tech-banner {
    padding: 22px 0 0;
}

/* a <picture> is inline by default, which breaks the img's width/centring */
.tech-banner picture {
    display: block;
}

.tech-banner img {
    margin: 0 auto;
    max-width: 260px;
    width: 100%;
    border-radius: var(--radius);
    border: 2px solid var(--gold);
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

/* small print inside a hero panel */
.panel-note {
    font-size: .72rem;
    color: var(--muted);
    margin: 14px 0 0;
}

.panel-note u {
    text-underline-offset: 2px;
}

/* held to a single line: the size is tied to the panel's own width rather
   than a fixed value, because this sentence can't fit a phone at .85rem */
/* black highlight behind the warning — inline-block so the fill hugs the
   text instead of running the full width of the panel */
.panel-note-last {
    display: inline-block;
    margin-top: 10px;
    white-space: nowrap;

    /* 3.1cqw, and the cqw now measures .hero-panel rather than .hero-text.
       It used to measure .hero-text, which is 36px WIDER than the space this
       line actually has, because the line lives inside .hero-panel and that
       panel has 18px of padding each side. The number was therefore tuned
       against the wrong box and only fitted by luck - about 12% headroom at
       390px, which is inside the range that font metrics vary by between
       Segoe UI, Roboto and SF. Measuring the real box makes the headroom real:
       ~11% at every phone width from 320 up. 3.1 against the panel renders at
       almost exactly the size 2.8 against the column did, so this looks
       unchanged - it is the same size for an honest reason. */
    font-size: min(3.1cqw, .95rem);

    /* JADE instead of black, 2026-10-15. Nothing about the GEOMETRY changed -
       it was already a highlighted pill, so the padding and that hard-won
       min(3.1cqw) stay exactly as measured and the fit is untouched.
       The type stayed WHITE: it was fine on black (19:1) and is fine on
       jade at 4.56:1. A brighter accent was tried and reverted the same day;
       on that one white fell to 2.13:1 and this had to be var(--bg). */
    color: #fff;
    background-color: var(--jade);
    background-image: linear-gradient(180deg, var(--jade) 0%, var(--jade-dark) 100%);
    padding: 5px 9px;
    border-radius: 6px;
}

.small-note {
    font-size: .92rem;
    color: var(--muted);
    margin-top: 10px;
}

.hero-images {
    display: grid;
    gap: 12px;
    justify-items: center;
    align-content: start;
}

/* the Mission Statement paragraph. Both of these are load-bearing: .hero-about
   sets no text-align of its own and .hero-images doesn't either, so without the
   centring this paragraph alone would read left-aligned; and the margin replaces
   .hero-about's 18px bottom, which would otherwise gap below the last line. */
.hero-images .hero-about {
    text-align: center;
    margin: 6px 0 0;
}

/* Sections */
.section {
    padding: 36px 0;
}

.section.alt {
    background: var(--panel);
}

/* h1 is here only on reviews.html, where the page's own subject is the heading.
   It carries the same base size as h2 ON PURPOSE: the visual difference between
   the two comes from the title treatment further down, so if that trial block is
   ever deleted this still lands somewhere sane instead of a browser-default 2em. */
.section h1,
.section h2 {
    margin: 0 0 16px;
    font-size: 1.45rem;
}

/* Services */
.service-group {
    margin-bottom: 26px;
}

.service-group h3 {
    margin: 0 0 10px;
    /* bumped from .95rem at his request. These two words are short and sit on a
       full-width row, so there is no width constraint on them at all - unlike
       the tier headings below, which look the same but are boxed in a card.
       1.1 -> 1.2rem on 2026-10-15, and that bump is NOT cosmetic: jade is
       3.71:1 on the purple, which clears the 3.0 that LARGE text needs but not
       the 4.5 that small text needs, and "large" starts at 18.66px when bold.
       1.1rem is 17.6px - a pixel short. 1.2rem is 19.2px and cleared it.
       A brighter #04c2bb would have lifted that to 7.94:1, but it was reverted
       the same day - so the constraint is REAL and 1.2rem is load-bearing. Do
       not tidy this back to 1.1rem while the colour is #0a8268. */
    font-size: 1.2rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--jade);
    text-align: center;
}

.service-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 12px;

    /* without this, grid items stretch to match the tallest in their row —
       so opening one card made its closed neighbours grow into tall empty
       boxes rather than staying the height of their own titles */
    align-items: start;

    /* NOTE: auto-fit sizes the track from the number of cards in the grid, so
       the 2-card Laundry row came out wider than the 3-card Kitchen row. The
       .service-grid-pair override in the 901px media query holds them equal -
       change the minmax above and that one needs re-measuring too. */
}

/* ---------------------------------------------------------------------------
   ONE CARD IN A GRID BUILT FOR THREE, 2026-10-18 (the Freezers group).
   Same root cause as .service-grid-pair, one step further: auto-fit sizes a
   track from the NUMBER of items in the grid, so a single card means a single
   track of 1fr - the module's entire inner width. Left alone the freezer card
   would be 636px sitting under Laundry's 312px pair, and because the shared
   watermark on .service-accordion>summary is `cover`, it would crop to a
   different part of the shop. That is exactly the complaint he raised about the
   laundry pair, so it is fixed here before he can see it rather than after.

   WHAT KITCHEN ACTUALLY DOES - derived by reading the rules, not guessed. The
   bubble's inner width is min(viewport, --content-max - 2*--gutter) - 48:
   `.section>.container.module-box` sets padding:24px on all four sides and
   outranks `.container`'s gutter (0,3,1 beats 0,1,0), and neither .section nor
   main nor body adds horizontal padding. That formula reproduces the 736px
   already documented for the 901-1299 band, which is the check that it is right.

       viewport   inner   Kitchen   track
          360       312     1 col   312.0   <- the lone card matches for free
          390       342     1 col   342.0   <- the lone card matches for free
          500       452     2 col   220.0
          684       636     2 col   312.0
          901       736     3 col   237.3
         1300       796     3 col   257.3

   Two columns need 452px of inner width, which is viewport 500 - so there is
   deliberately NO base rule here. Below 500px the natural 1fr already equals
   what Kitchen does, and on a phone, the view that matters most, this class
   does nothing at all. From 500px it pins to Kitchen's half; from 901px to
   Kitchen's third, and that override lives in the 901 block beside
   .service-grid-pair (later in the file, so it wins on order).

   IF A SECOND FREEZER SERVICE IS EVER ADDED, swap this class on the div for
   .service-grid-pair instead of editing these numbers - that is what it is for.
   --------------------------------------------------------------------------- */
@media (min-width: 500px) {
    .service-grid-single {
        grid-template-columns: calc((100% - 12px) / 2);
        justify-content: center;
    }
}

.service-card {
    background: rgba(255, 255, 255, .04);
    padding: 16px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    text-align: center;
    font-weight: 700;

    /* NEW shadow */
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

/* Areas + map.
   Top row is the map plus its key; the tier lists run full width beneath.
   The 2027 map moved from two tiers to three marketed ones and from 39 towns
   to 46 - stacked beside the map the way the old list was, that column ran
   well past the height of the map next to it. Wide and short beats tall. */
.areas-wrap {
    display: grid;
    grid-template-columns: 1.6fr 1fr;
    gap: 18px;

    /* stretch, NOT start. The map card sits at its own 320px while the key panel
       beside it is taller (its three key rows, the call/text line and the
       counties list come to roughly 360px), so the map finished short of it.
       Stretching makes both columns take the row height, which is the taller of
       the two - so the map matches the panel automatically and keeps matching if
       the panel's content ever changes. Hard-coding a map height would need
       re-measuring every time a line is added to that panel. */
    align-items: stretch;
    margin-bottom: 18px;
}

/* Same idiom as .service-grid up in Services - auto-fit rather than a fixed
   column count, so the three tiers go 3-across, then 2, then 1 as the viewport
   narrows, with no media query of its own.

   230px, NOT 240px. At 240 the three tiers needed 3x240 + 2x12 = 744px, and the
   content column is only 736px wide between 901 and 1299px viewport - so Tier 3
   wrapped onto a second row underneath Tiers 1 and 2 on most laptops. 230 needs
   714px, which fits. The alternative was widening the page in that band, but
   that would have pulled the hero wider too and undone the single content width
   set the day before; shrinking the lists was the cheaper change.
   Still resolves to 3 columns at 1300px+ and to 2 then 1 below 900px. */
.area-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: 12px;
}

/* each tier is a card, exactly like the service cards, .contact-box, .map-card
   and .area-note: same translucent white, same border, same radius, same
   shadow pair. Nothing here is a one-off value. */
.area-group {
    background: rgba(255, 255, 255, .04);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    /* 13px, not the 16px the other cards use. The ZIP codes added on 2026-10-13
       cost every row about 40px of width, and this is the cheapest place to buy
       some back - 3px off each side is invisible but it is 6px per column. */
    padding: 13px;
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

/* Gold, uppercase, centred - the same family as .service-group h3, which was
   bumped to 1.1rem on 2026-10-12. This one stayed at .95rem because its text is
   far longer ("TIER 1 SERVICE AREA" vs "KITCHEN") and it is boxed in a card with
   a swatch beside it, so it has a real width budget where the other has none.

   THE BUDGET, measured, and it moved on 2026-10-12: the tier cards used to be
   362px because only two fitted per row. Pulling all three onto one row made
   them 237px, which leaves just 179px for this text after 32px of padding and
   the swatch's 26px - and "TIER 1 SERVICE AREA" measures 180.4px at .95rem with
   .1em tracking. One pixel over, so it wrapped.
   Tracking cut .1em -> .05em, which brings it to 166px and leaves ~7% headroom.
   The size stayed at .95rem so it still reads as the same family as
   .service-group h3 (KITCHEN / LAUNDRY), which sits on a full-width row and has
   no width budget at all - that one is 1.1rem.
   NOTE: this same trim was made and then reverted earlier the same day, when the
   cards were still 362px and it was unnecessary. It is correct now because the
   row changed, not because the earlier reasoning was right.
   If a longer tier name is ever added, re-measure against 179px. The swatch
   rides inside this heading, which is why it is a centred flex row. */
.area-group h3 {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    margin: 0 0 10px;
    /* JADE RULE, GOLD TEXT, 2026-10-15. The split was FORCED when it was made:
       these headings are .95rem and the first jade was only 3.71:1 on the
       purple, too low for type this small, and the size could not be raised to
       fix it because this heading has a measured width budget (see above -
       "TIER 1 SERVICE AREA" already wrapped once at 179px and only fits now
       because the tracking was cut). A rule is not text, so it cleared the bar
       and the section still picked up the accent.
       A brighter #04c2bb would have lifted the text to 7.94:1 and freed this,
       but it was tried and reverted the same day - so the constraint STANDS and
       the gold text here is still forced, not a preference. */
    padding-bottom: 9px;
    border-bottom: 1px solid var(--jade);
    font-size: .95rem;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--gold);
}

/* One column, not two. Split in two, a tier card leaves ~150px per column and
   "Lookout Valley / Tiftonia, TN" breaks onto three lines.

   .85rem because the cards got narrower when the three tiers were pulled onto
   one row: at 3-across the card is 237px, which leaves 187px of text space
   after 32px of card padding and the list's own 18px. That longest town name
   measures 200.7px at 1rem - it would have wrapped - and 170.6px at .85rem,
   leaving about 9% headroom.
   THE NUMBER TO CHECK AGAINST IS 187px, and the longest name is
   "Lookout Valley / Tiftonia, TN" (tied with "Downtown Chattanooga, TN"). If a
   longer town is ever added to the map, re-measure rather than assuming. */
/* ZIP CODES, 2026-10-13. The sizes here are measured, not chosen by eye.
   The binding case is the 901-1299px band: --content-max is 820px there, which
   after the bubble's gutter and 24px padding leaves 736px of content, and the
   3-column grid makes each card 237px. Minus this card's padding, its border and
   the list indent, a row gets 194px of text.
   The longest row is "Lookout Valley / Tiftonia, TN 37419". Measured in Segoe UI
   it needs 183px at .76rem with the ZIP at .7rem - about 6% to spare. At .85rem,
   the size this list used before the ZIPs, that same row needed 211px and would
   have wrapped, along with "Downtown Chattanooga, TN 37402".
   Above 1300px there is 214px per row, so everything has ~15% of room there.
   IF A LONGER TOWN IS EVER PINNED, lower these, don't raise them. */
.area-list {
    margin: 0;
    padding-left: 15px;
    font-size: .76rem;
}

.area-list li {
    margin: 4px 0;
    break-inside: avoid;
}

/* The ZIP is supporting detail, so it sits back from the town name rather than
   competing with it. nowrap because a ZIP split across two lines is unreadable -
   if a row ever runs out of width it breaks before the number, never inside it. */
.area-list .zip {
    font-size: .7rem;
    color: var(--muted);
    white-space: nowrap;
}

/* Wrapper around the map card. It holds only the map now - the key panel used to
   sit under it in here and is a sibling in .areas-wrap these days.
   align-content: stretch so its single auto row grows to fill the height
   .areas-wrap hands it; with `start` the row stayed at the card's own height and
   the stretch above would have had nothing to push against. */
.map-col {
    display: grid;
    gap: 14px;
    align-content: stretch;
}

.area-note {
    background: rgba(255, 255, 255, .04);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 16px;
    color: var(--muted);
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

.area-note p {
    margin: 0 0 10px;
}

/* colour key + service call rates */
.rate-key {
    list-style: none;
    margin: 0 0 14px;
    padding: 0;
}

.rate-key li {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 10px;
    color: var(--text);
    font-weight: 600;
    /* three rows, so this sits between the original 1.33rem (two rows) and the
       1.05rem the five-row version needed */
    font-size: 1.15rem;
}

/* ONE swatch rule for the whole module. The key and the three tier headings use
   the same element, so a colour is defined once and can never disagree between
   the two places it appears.
   The colours are the map's own, read out of the KML style ids rather than
   eyeballed - 9C27B0 / 0097A7 / FF5252 - and they stay exact. Tier 1's purple
   is Google's purple sitting on our purple and there is nothing to be done
   about that; his call is that the dot matches the map regardless of how it
   sits against the background, so the fill is never adjusted. The hairline
   ring is the one concession - it separates the dot from the panel without
   touching the colour inside it. */
.swatch {
    width: 17px;
    height: 17px;
    border-radius: 50%;
    flex: 0 0 auto;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .3);
}

.sw-t1 {
    background: #9c27b0;
}

.sw-t2 {
    background: #0097a7;
}

.sw-t3 {
    background: #ff5252;
}

/* a word inside a sentence made tappable — underline only, nothing else
   changes, so the line keeps its size, weight and colour */
.inline-link {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* scoped to .contact-box: that block sets `.contact-box p { margin: 0 0 12px }`,
   which is more specific than a bare class and was silently winning */
.contact-box .contact-review {
    margin: 52px 0 10px;
}

.area-note strong {
    color: var(--text);
}

/* jade rule, not --border: this divider and the one under each .area-group h3 are
   the same device in the same section, so they match. Same reason that one is jade
   (2026-10-15) applies here - a rule is not text, so it carries the accent at a
   contrast no small type could. */
.area-counties {
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--jade);
}

.area-counties h4 {
    margin: 0 0 6px;
    font-size: .82rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--gold);
}

/* One column, not two. Two was right when this list ran to six-plus counties
   and the note panel was ~400px wide. The note is ~290-315px now that the
   bubbles match the hero, which leaves about 110-118px per column - and
   "Hamilton County, TN" needs roughly 133px at this size, so every entry would
   have wrapped. Four short rows beat four wrapped ones. */
.area-counties ul {
    margin: 0;
    padding-left: 18px;
    font-size: .92rem;
}

.area-counties li {
    margin: 3px 0;
    break-inside: avoid;
}


.map-card {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: rgba(255, 255, 255, .03);
    min-height: 320px;

    /* NEW shadow */
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

.map-card iframe {
    width: 100%;
    height: 100%;
    min-height: 320px;
    border: 0;
    display: block;
}

/* ---- Module bubbles -------------------------------------------------------
   Our Services and Service Area each sit in their own bubble now, the way
   Contact Us already did, with their inner cards nesting inside them - bubbles
   in bubbles. The class goes ON the .container rather than in a wrapper inside
   it, which is exactly how .contact-box is applied, so all three module bubbles
   share one geometry and line up at every width.

   Nesting reads correctly for free: the inner cards use the same
   rgba(255,255,255,.04), and translucent white over translucent white composites
   a step lighter than the bubble under it, so each level separates itself
   without a second colour. */
.module-box {
    background: rgba(255, 255, 255, .04);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

/* ---------------------------------------------------------------------------
   THE EXCLUSIONS FLAG AND ITS POPUP, 2026-10-16.
   A red flag in the Services module's top-left corner opening his "we do not
   service" list. Only the Services module gets the positioning context - a bare
   .module-box { position: relative } would hand one to every bubble on the site
   for the sake of one button.
   --------------------------------------------------------------------------- */
#services>.module-box {
    position: relative;
}

/* 40px is the smallest comfortable touch target, and it sits inside the
   module's own 24px padding so it never lands on the heading. */
.exclusions-flag {
    position: absolute;
    top: 8px;
    left: 8px;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 10px;
    cursor: pointer;
    transition: background .15s ease, border-color .15s ease;
}

.exclusions-flag:hover,
.exclusions-flag:focus-visible {
    background: rgba(229, 24, 55, .14);
    border-color: var(--accent);
    outline: none;
}

.exclusions-flag svg {
    width: 28px;
    height: 28px;
    display: block;
}

/* the pole is chrome rather than red so the flag reads as a flag and not as a
   solid red blob at 26px */
.flag-pole {
    fill: none;
    stroke: var(--chrome);
    stroke-width: 2.2;
    stroke-linecap: round;
}

.flag-cloth {
    fill: var(--accent);
    stroke: #1c1008;
    stroke-width: 1;
    stroke-linejoin: round;
}

/* a label for screen readers that takes no space on screen. The button also has
   a title for the mouse, but a title is not an accessible name on its own. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* LOAD-BEARING, and it is the same trap as .gallery-grid[hidden] and .lb[hidden]
   before it: a browser that does not understand <dialog> gives it no UA style at
   all, so it renders as an ordinary block and the ENTIRE exclusions list sits
   open in the middle of the Services module. The UA rule that hides a closed
   dialog is a browser-stylesheet rule, so this one has to exist in ours. */
dialog:not([open]) {
    display: none;
}

.exclusions {
    width: min(460px, calc(100vw - 32px));
    max-height: calc(100vh - 80px);
    overflow-y: auto;
    padding: 22px 24px 20px;
    color: var(--text);
    background: var(--bg);
    border: 2px solid var(--accent);
    border-radius: var(--radius);
    box-shadow: 0 24px 60px rgba(0, 0, 0, .7);
}

.exclusions::backdrop {
    background: rgba(12, 2, 15, .82);
}

.exclusions h3 {
    margin: 0 28px 14px 0;
    font-size: 1.15rem;
    line-height: 1.3;
    color: var(--accent);
}

.exclusions-list {
    margin: 0;
    padding: 0 0 0 20px;
    list-style: none;
}

.exclusions-list li {
    position: relative;
    margin: 0 0 9px;
    line-height: 1.35;
}

/* a red square rather than a disc: this is a list of refusals, and it matches
   the flag that opened it */
.exclusions-list li::before {
    content: "";
    position: absolute;
    left: -16px;
    top: .52em;
    width: 7px;
    height: 7px;
    background: var(--accent);
    border-radius: 1px;
}

/* the "(SubZero, Thermador...)" asides - smaller and dimmer so the thing being
   excluded reads first. Shared by the popup and the per-card lists below. */
.exclusions .eg,
.card-exclusions .eg {
    display: block;
    font-size: .85rem;
    color: var(--muted);
}

/* ---------------------------------------------------------------------------
   "MORE ABOUT ..." - THE ONLY DOOR TO A SERVICE PAGE, 2026-10-18.
   His design, stated plainly: no nav item, no hamburger item, no footer row.
   One button, inside the card for that appliance, between the brand logos and
   the exclusions - the moment a reader has decided the appliance is theirs and
   has not yet been told no.
   (He asked whether the footer row was worth it for Google. It is not: one real
   <a href> inside the page is all a crawler needs, and the content of a closed
   <details> is still in the source. The thing that actually helps is putting the
   page in sitemap.xml, which happens when the page stops being a shell.)

   IT IS NOT .btn-teal, DELIBERATELY. That class is the same jade gradient but
   its text is the page purple, which measures 2.22:1 over the dark end of the
   gradient - recommended against twice and still shipped at his word on the cats
   button. A new button had no reason to inherit a known problem, so this one is
   white: 4.77:1 at the light end, 7.96:1 at the dark end. Everything else - the
   gradient, the dark edge, the gold ring, the shared gold sheen - is identical,
   so it still reads as "the teal button" beside the others.

   TWO LINES ON A NARROW CARD, AND THE BREAK IS CONTROLLED. Measured at 16.8px
   Segoe UI Bold: "More About Refrigerator Repair" is 252.9px on one line, and a
   3-across card leaves the button about 169px of text - so one line was never
   possible, and shrinking to fit would need ~11px type. Two lines at full size
   fit with room: "Refrigerator Repair" is 152.3px against 169px, the same 10%
   headroom every other measured line on this site keeps.
   .btn sets white-space: nowrap, so this rule has to turn it off; the markup
   then uses a NON-BREAKING SPACE inside "Refrigerator Repair" so the only legal
   break is after "About". Without it a 390px phone - where the whole label fits
   on one line at 253px of a possible 274px - could still break as
   "More About Refrigerator / Repair", which looks like an accident.
   WHEN THE OTHER FOUR PAGES ARE BUILT, re-measure each label against 169px
   rather than assuming this one generalises: "Electric Dryer Repair" is longer
   than "Refrigerator Repair". */
/* ---------------------------------------------------------------------------
   A ONE-LINE SCOPE NOTE INSIDE A SERVICE CARD, 2026-10-18. Only the Freezers
   card has one, and it exists because the words would not fit in either of the
   two places they belong - both of which were measured rather than eyeballed:

     THE GROUP HEADING is pinned at 1.2rem by the jade contrast floor (large
     text starts at 18.66px when bold, and 1.1rem = 17.6px failed it - see
     .service-group h3, where that is called load-bearing). So it cannot shrink.
     "UPRIGHT & CHEST FREEZERS" measures 309.2px and a 360px phone gives the
     heading 312px: 0.9% of room, where his standing rule wants 10%.
     "FREEZERS" measures 103.5px and sits beside KITCHEN (95.3) and LAUNDRY
     (104.2), which is why the heading is just the one word.

     THE SUMMARY LABEL has less room still - about 148px at a 3-column card -
     and "Upright & Chest Freezer Repair" measures 217.4px at .8rem.
     "Freezer Repair" measures 86.1px and fits with room to spare.

   PADDING, 2026-10-18: .service-accordion sets padding:0 and this is a direct
   child of it, so a line of running text here would touch the gold border. 16px
   on the sides matches .brands immediately below it; .card-more and
   .card-exclusions get away with none because their text is centred and short.
   font-weight 400 is NOT redundant - .service-card sets 700 for its title and
   that inherits straight into here.
   --------------------------------------------------------------------------- */
.card-note {
    margin: 0;
    padding: 12px 16px 0;
    font-size: .78rem;
    font-weight: 400;
    line-height: 1.35;
    color: var(--muted);
}

/* ---------------------------------------------------------------------------
   THE DISHWASHER CARD'S INSTALLATION HEADLINE, 2026-10-25, at his ask while
   writing the Terms of Service. Three hard lines, centred, inside the existing
   Dishwasher Repair dropdown - his words: "just like the freezer dropdown has",
   so it reuses .card-note and only changes what it has to.

   NO text-align HERE ON PURPOSE: .service-card already sets text-align:center
   and .card-note never overrides it, so the freezer note is centred too and
   this one inherits it. Adding it again would just be noise.

   WHY .7rem AND NOT .card-note's .78rem - measured, not guessed. "of
   Dishwashers & Garbage Disposals" is the longest of the three lines and
   measures 191px in Segoe UI at .78rem (Pillow, no kerning, so that number is
   slightly WIDE - the safe direction for a fits-or-not test).
   The NARROWEST a Kitchen card ever gets is the 2-across band at viewport
   500px: a 220px track, per the measurement table beside .service-grid-single.
   .service-accordion sets padding:0, so the track IS the content box, and the
   10px sides below leave 200px for text:

       viewport   track   text avail   line at .7rem   slack
          360     312.0      292.0        181.0        38.0%
          390     342.0      322.0        181.0        43.8%
          500     220.0      200.0        181.0         9.5%   <- binding
          684     312.0      292.0        181.0        38.0%
          901     237.3      217.3        181.0        16.7%
         1300     257.3      237.3        181.0        23.7%

   At .78rem that band has only 4.5% of slack and the line wraps to four,
   which loses the three-line shape he asked for. .7rem lands at 9.5%, which
   is the shrink-don't-wrap rule's "about 10% under".
   DELIBERATELY NO nowrap: the <br>s already force the shape, and if someone
   runs a larger default font size a wrap inside the card is better than text
   pushing out through the gold border.
   --------------------------------------------------------------------------- */
.card-note-inquire {
    padding: 12px 10px 0;
    font-size: .7rem;
}

.card-more {
    margin: 20px 0 4px;
}

.btn-more {
    background-color: var(--jade);
    background-image: linear-gradient(180deg, var(--jade) 0%, var(--jade-dark) 100%);
    color: #fff;
    border: 2px solid #1c1008;
    box-shadow: 0 0 0 2px var(--gold);
    white-space: normal;
    line-height: 1.25;

    /* .95rem, NOT .btn's 1.05rem, and the number comes from the LONGEST of the
       five labels rather than from this one. Second lines at 16.8px, against the
       169px a 3-across card gives the button:
           Washer Repair        114.8   Dishwasher Repair     146.0
           Refrigerator Repair  152.3   Electric Oven Repair  158.3
           Electric Dryer Repair 161.1  <- 95% of the budget, 5% headroom
       Three of the five were over the 10% headroom this site holds everywhere
       else, and "Electric Dryer Repair" was close enough that a slightly wider
       face - Roboto or SF rather than Segoe UI - could push it to three lines.
       At 15.2px the worst case is 145.8px, 86% of the budget. One size for all
       five, set by the longest, so the buttons stay identical to each other. */
    font-size: .95rem;
}

/* ---------------------------------------------------------------------------
   THE SERVICE PAGE ITSELF, 2026-10-18. One module, same bubble as the home
   page's sections, so it reads as part of the site rather than bolted on.
   Everything here is shared by all five service pages when they exist - nothing
   in it names refrigerators. */
/* ---------------------------------------------------------------------------
   THE TITLE BREAK, 2026-10-18 (his ask): on a phone the h1 reads "More About"
   on one line and the appliance on the second.

   IT IS NOT COSMETIC - IT IS FIXING RAGGED WRAPS THAT WERE ALREADY HAPPENING,
   differently on each of the five. Measured at 1.8rem bold with the 3px ring,
   against the 306px a 390px phone gives the module:
       "More About Refrigerator Repair"   439.5px   wraps
       "More About Electric Dryer Repair" 454.6px   wraps
   Both already broke on their own - but WHERE depends on the words. "More About
   Washer" is 286px and fits on the first line, so that page broke as
   "More About Washer / Repair" while the fridge broke as "More About /
   Refrigerator Repair". Forcing the break makes all five agree.

   A NEW BREAKPOINT, 560px, and it is measured rather than borrowed. One line
   needs 454.6px of text, which needs a 476px module, which needs a 561px
   viewport. br.br-sm - the site's existing small-screen break - turns on at
   520px, which would have left 521-560px wrapping on its own again. One narrow
   band of wrongness is still wrongness, so this gets its own class rather than
   reusing that one, and br-sm keeps doing its own job in the hero.
   --------------------------------------------------------------------------- */
br.br-title {
    display: none;
}

@media (max-width: 560px) {
    br.br-title {
        display: inline;
    }
}

.service-page h2 {
    margin: 26px 0 10px;
    font-size: 1.15rem;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--jade);
    text-align: center;
}

/* CENTRED AT HIS ASK, 2026-10-18 - and the measure is capped in the same rule,
   because the two go together. Centred text has no straight edge for the eye to
   return to, so every extra character of line length costs more here than it
   would left-aligned; at the module's full 736px these paragraphs would run
   about 110 characters a line. 62ch is the long end of the readable range and
   holds them to roughly 60-70, which is where centred prose still works.
   auto margins keep the narrowed block in the middle of the bubble.
   IF HE EVER FINDS THESE HARD TO READ, the fix is text-align: left on this rule
   alone - the headings, captions, buttons and back link are centred by their
   own rules and would not move. */
.service-page p {
    margin: 0 auto 14px;
    max-width: 62ch;
    line-height: 1.6;
    text-align: center;
}

/* auto-fit rather than repeat(3), the same idiom as .service-grid and
   .area-grid: three across on desktop, two then one as it narrows, with no
   media query of its own. 150px is small enough that three still fit the
   module's 736px inner width at the 901-1299 band with room to spare. */
.svc-figs {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 14px;
    margin: 18px 0 26px;
}

.svc-fig {
    margin: 0;
}

/* THE EMPTY FRAME. aspect-ratio rather than a fixed height so the three boxes
   stay the same shape at every column width, and 4:3 because that is the shape
   most of the gallery photographs already are - a real photo dropped in here
   will not change the page's height.
   Dashed and labelled so it can never be mistaken for a finished element. */
.svc-fig-box {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 4 / 3;
    border: 2px dashed var(--border);
    border-radius: var(--radius);
    background: rgba(255, 255, 255, .03);
    color: var(--muted);
    font-size: .85rem;
    letter-spacing: .08em;
    text-transform: uppercase;
}

.svc-fig figcaption {
    margin-top: 7px;
    font-size: .82rem;
    line-height: 1.35;
    text-align: center;
    color: var(--muted);
}

.svc-actions {
    display: flex;
    justify-content: center;
    gap: 12px;
    margin: 22px 0 6px;
}

.svc-back {
    margin: 16px 0 0;
    text-align: center;
    font-size: .9rem;
}

/* 2026-11-04, his ask: the "Back to all services" link in the same teal as the
   section headings above it (.service-page h2 uses var(--jade)). Scoped to
   .svc-back so every other .inline-link keeps inheriting its text color. */
.svc-back .inline-link {
    color: var(--jade);
    font-weight: 700; /* his ask, same day: jade is dim as small text on the purple */
}

/* ---------------------------------------------------------------------------
   PER-CARD EXCLUSIONS, 2026-10-18 (his ask). Seven of the red flag's eleven
   items moved out to the bottom of the service they actually belong to, so a
   reader meets "no LG kitchen products" while already looking at refrigerators
   rather than having to open a popup and work out which line applies to them.
   The flag keeps only what fits no card.

   IT REUSES THE POPUP'S OWN HEADING LINE, which is what he asked for - "I like
   the red 'we're sorry - we do not service'" - so the two places say the same
   sentence and read as one policy in two locations.

   CENTRED AND UNBULLETED, unlike the popup list. The popup is a wide dialog
   where a hanging red square has room; a 3-across service card is about 200px
   of text at its narrowest, and a bullet with a hanging indent in that width
   eats the line and leaves ragged wraps. Centred matches the brand chips
   directly above it, so the card keeps one axis.

   font-weight 400 is NOT redundant: .service-card sets font-weight 700 for its
   title and that inherits straight into this list, which would have shipped the
   whole thing bold.
   --------------------------------------------------------------------------- */
.card-exclusions {
    margin-top: 16px;
    padding-top: 12px;
    border-top: 1px solid var(--accent);
}

.card-exclusions h4 {
    margin: 0 0 9px;
    font-size: .74rem;
    letter-spacing: .04em;
    text-transform: uppercase;
    line-height: 1.3;
    color: var(--accent);
}

.card-exclusions ul {
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: .8rem;
    font-weight: 400;
    line-height: 1.35;
    color: var(--muted);
}

.card-exclusions li {
    margin: 0 0 7px;
}

/* THE HYPHENS, 2026-10-18 at his ask: "- LG Kitchen Products".
   Drawn with ::before rather than typed into the markup, for three reasons that
   all point the same way: one rule covers all five cards and every item in them,
   nobody can add an item later and forget the hyphen, and the text stays clean -
   a visitor who copies the line gets the exclusion, not a stray dash. It also
   means these can be swapped for a bullet, a dot or nothing at all by editing
   one line.
   The popup keeps its red squares - this is the card list only. */
.card-exclusions li::before {
    content: "- ";
}

.card-exclusions li:last-child {
    margin-bottom: 0;
}

/* NOTE on the asides: .eg is .85rem of its PARENT, and the parent here is .8rem
   rather than the popup's full size, so they land at about .68rem. That is small
   on purpose - inside a narrow card the brand examples are a footnote to a
   footnote - and it needs no rule of its own to achieve it. A second
   `.card-exclusions .eg` block was written and then deleted: it bought one pixel
   of margin and cost a duplicate selector. */

.exclusions-terms {
    margin: 18px 0 0;
    padding: 12px 0 0;
    border-top: 1px solid var(--border);
    font-size: .9rem;
    line-height: 1.4;
    text-align: center;
    color: var(--muted);
}

.exclusions-terms-strong {
    display: block;
    font-weight: 700;
    color: var(--gold);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.exclusions-close {
    position: absolute;
    top: 10px;
    right: 12px;
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    font: inherit;
    font-size: 1.4rem;
    line-height: 1;
    color: var(--text);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 50%;
    cursor: pointer;
    padding: 0 0 3px;
}

.exclusions-close:hover,
.exclusions-close:focus-visible {
    border-color: var(--accent);
    background: rgba(229, 24, 55, .14);
    outline: none;
}

/* PADDING IS SET BY SPECIFICITY, NOT ORDER - and that is deliberate.
   These bubbles are themselves .container elements, so .container's own
   `padding: 0 var(--gutter)` lands on them. A bare `.module-box { padding: 24px }`
   would tie .container at (0,1,0) and lose to whichever rule sits later in the
   file, flattening the bubble's top and bottom padding to zero. This selector
   scores (0,3,1) and holds regardless of order.

   .contact-box is included because it HAD that bug in production: the 520px
   media query used to restate `.container { padding: 0 14px }`, which wiped
   contact-box's vertical padding on phones. That is the real reason a
   `padding-bottom: 40px` had to be pasted back to stop the review button
   touching the edge - and the top was tight for the same reason and was never
   reported. The media query now sets --gutter instead of restating padding, so
   the hazard is gone at the source as well as being outranked here. */
.section>.container.module-box,
.section>.container.contact-box {
    padding: 24px;

    /* THE GEOMETRY THAT MAKES ALL FOUR BUBBLES MATCH.
       The hero's panels are NOT the container - they sit inside it, so
       .container's side padding insets them automatically and their width comes
       out as (content-max - 2x gutter). These bubbles ARE the container, so
       their padding is on the inside and their outer edge gets no inset at all.
       Capping them at content-max therefore left them 2x gutter WIDER than the
       hero panels, and on a phone - where no max-width is reached - their edge
       sat flush against the screen.
       So subtract the gutter from both the cap and the live width. Now:
         hero panel   = content-max - 2x gutter
         module bubble= content-max - 2x gutter   <- identical, by construction
       and on a phone the bubble is viewport - 2x gutter, which is exactly the
       inset the hero panels have. Both numbers come from --gutter, so changing
       the page's breathing room moves all of them together. */
    width: calc(100% - var(--gutter) * 2);
    max-width: calc(var(--content-max) - var(--gutter) * 2);
    margin-left: auto;
    margin-right: auto;
}

/* ===========================================================================
   SECTION TITLE TREATMENT  —  TRIAL, 2026-10-12. DELETE THIS WHOLE BLOCK TO REVERT.
   Removing these two rules puts the titles back as they were (h2 at 1.45rem plain,
   .mission-title at 1.7rem plain).
   NO LONGER QUITE SELF-CONTAINED, 2026-10-17: .hero-brandname was added to the
   list below and takes its colour and rings from here, at his ask that the hero
   name "use the same colorizing as the module titles". Deleting this block would
   leave that line plain body text at the size set further down - not broken, but
   no longer matching anything. One source for the look is the point.

   The look is the logo A's layering, from the inside out: red face, a hairline
   dark line, then a thicker gold border.

   Built from text-shadow rings rather than -webkit-text-stroke, for two reasons.
   One stroke can only make ONE outline, and this needs two concentric ones.
   And text-shadow layers paint in order with the FIRST in front, so listing the
   dark ring before the gold ring reproduces the logo's order exactly, with both
   sitting behind the red fill so the face stays full strength. It also uses no
   non-standard properties.

   HISTORY WORTH KNOWING BEFORE TUNING: this same red/black/gold outline was
   tried on BUTTON text on 2026-09-24 and he rejected it twice as too hard to
   read, ending with plain white. Headings are nearly twice the size, which is
   why it is worth another try here — but legibility is the thing to judge it on.
   =========================================================================== */
#services h2,
#areas h2,
#contact h2,
#reviews h1,
#gallery h2,
#service-page h1,
.mission-title,
.hero-brandname {
    font-size: 1.8rem;
    color: var(--accent);

    /* ring 1, hairline dark, 1px — listed first so it paints IN FRONT of the gold
       ring 2, gold, out to 3px — 16 offsets, because 8 leaves visible gaps at
       this radius and the border has to read as a solid border */
    text-shadow:
        1px 0 0 #1a1114, -1px 0 0 #1a1114, 0 1px 0 #1a1114, 0 -1px 0 #1a1114,
        1px 1px 0 #1a1114, -1px -1px 0 #1a1114, 1px -1px 0 #1a1114, -1px 1px 0 #1a1114,
        3px 0 0 var(--gold), -3px 0 0 var(--gold), 0 3px 0 var(--gold), 0 -3px 0 var(--gold),
        2px 2px 0 var(--gold), -2px 2px 0 var(--gold), 2px -2px 0 var(--gold), -2px -2px 0 var(--gold),
        3px 1px 0 var(--gold), 3px -1px 0 var(--gold), -3px 1px 0 var(--gold), -3px -1px 0 var(--gold),
        1px 3px 0 var(--gold), -1px 3px 0 var(--gold), 1px -3px 0 var(--gold), -1px -3px 0 var(--gold);
}

/* THE TWO LONG ONES, and the arithmetic that sets the breakpoint below.
   Measured in the real face at 1.8rem bold, including the 3px gold ring:
       "Mission Statement"  258.5px      "Customer Reviews"  253.6px
       "My Services"        166.1px      "Service Area"      173.1px
   ("Our Services" was 173.7px until he renamed the module on 2026-10-18; the new
   name is 7.6px narrower, so nothing here needed re-tuning.)
   A module bubble gives 244px of text room on a 320px phone and 284px on a 360px
   one, so only the smallest phones are short - and only for those two headings.
   His standing rule is to lower the font size rather than let text wrap, so that
   is what this does. 1.6rem brings the longest to 229px, inside 244px with ~6%
   spare. This also fixes a wrap that was already shipping on "Mission Statement"
   before reviews.html existed and had simply been written off as acceptable. */
@media (max-width: 360px) {

    #services h2,
    #areas h2,
    #contact h2,
    #reviews h1,
    #gallery h2,
    #service-page h1,
    .mission-title {
        font-size: 1.6rem;
    }

    /* The service-page h1 goes one step further than the rest of the group,
       because the break above gives it a SECOND line to fit rather than a
       first. On a 320px phone the module holds 244px of text, and "Electric
       Dryer Repair" - the longest of the five second lines - is 251.4px at
       1.6rem, so it would have wrapped AGAIN into a third line. At 1.4rem it
       is 220.7px, 90% of the box, the same headroom used everywhere else.
       Ties the group rule above on specificity (1,0,1) and wins on order, so
       it must stay below it. */
    #service-page h1 {
        font-size: 1.4rem;
    }

    /* .hero-brandname is deliberately NOT in this list. It carries a clamp that
       is already scaling with the viewport at this width, and a flat 1.6rem here
       would be a second, contradictory answer to the same question. */
}

/* ---------------------------------------------------------------------------
   CINZEL DECORATIVE, 2026-10-18. The ONLY webfont on this site, used by exactly
   one line of text - the business name in the hero.

   HE ASKED FOR ALGERIAN FIRST and it could not be done, for two reasons both
   worth leaving here so nobody tries again: Algerian ships with Microsoft Office
   rather than with Windows (it is not even on his own machine), and its licence
   covers documents, not serving the file to a website's visitors. No phone has
   it either. He picked this from _tools/hero-font-picker.html instead.

   SELF-HOSTED, NOT LINKED TO GOOGLE, which costs nothing and buys three things:
   no third-party request on every page view, no dependency on a service that can
   change or be blocked, and no visitor data going anywhere else.

   2,948 BYTES, because the file is SUBSET to the characters this one line uses
   (A C E H I L N P R S V and a space, upper and lower case). The full family is
   60 KB. WARNING THAT GOES WITH THAT: if the hero text is ever changed to words
   containing other letters, those letters will fall back to the serif below and
   look wrong. Re-cut the subset with the new text if that happens.
   Licence: SIL Open Font Licence - self-hosting and subsetting are both allowed.

   font-display: swap means the name paints in the fallback serif first and
   switches when the font lands. For 2.9 KB that is almost never visible, and the
   alternative (invisible text until it arrives) is worse on a slow phone. */
@font-face {
    font-family: "Cinzel Decorative";
    src: url("../fonts/cinzel-decorative-700-subset.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/* THE BUSINESS NAME IN THE HERO, 2026-10-17 (his ask). Colour and rings come
   from the shared rule above; this sets only the font, size and spacing.

   SPECIFICITY, NOT ORDER - (0,2,0) beats both the 1.8rem in the block above and
   the 1.6rem in the 360px query, so this holds wherever anyone moves it. Written
   that way on purpose: a bare .hero-brandname would tie them and win or lose on
   position alone.

   SET IN CAPS at his ask, and the caps are done with text-transform rather than
   by typing the name in capitals in the HTML. The markup keeps "Alpha Appliance
   Service" as the business actually writes it, which is what gets copied, read
   aloud and indexed; only the rendering shouts.

   THE NUMBERS ARE MEASURED, not chosen by eye, and they have been re-measured
   TWICE as this line changed - once when it went to caps, and again when it
   changed face on 2026-10-18. Each face was measured from its own font file with
   GDI+ at typographic spacing, which agrees with the three heading widths
   already recorded further up (258.5 / 253.6 / 173.7px) to within half a percent:
       Segoe UI bold,   "Alpha Appliance Service"   330.9px at 1.8rem = 11.49/px
       Segoe UI bold,   "ALPHA APPLIANCE SERVICE"   382.7px at 1.8rem = 13.29/px
       Cinzel Dec 700,  "ALPHA APPLIANCE SERVICE"   428.3px at 1.8rem = 14.87/px
   So CINZEL IS 11.9% WIDER than the system face at the same size, which is why
   every number below came down when the font changed. The glyphs alone are
   14.663 x the font size; the +6px is the 3px gold ring on each side, and it is
   a CONSTANT, not a proportion - that matters at small sizes.

   The box this has to fit is (content-max or viewport, whichever is smaller)
   minus two gutters, and the largest size that leaves 10% headroom in each:
       320px phone   292px ->  17.5px      360px phone  332px ->  20.0px
       390px phone   362px ->  21.8px      720-900px    684px ->  41.6px
       901-1299     784px ->  47.7px      1300+        844px ->  51.4px

   5.4vw is the phone term: 17.3px on a 320 against a possible 17.5, 21.1 on a
   390 against 21.8. That is his shrink-don't-wrap rule with real headroom rather
   than a guess. (It was 7.2vw in mixed-case Segoe, 6.2vw in caps, and 5.4 here -
   each drop is exactly the width penalty of the change and nothing else.)

   THE CAP IS AT ITS CEILING FOR THIS BAND. The binding width is NOT the widest
   screen, it is the 720-900px band, where --content-max is still 720 and the
   column is only 684px: that allows 41.6px = 2.60rem, so 2.55rem is as large as
   this can be without the line overflowing a small laptop or a landscape tablet
   - and because it is nowrap, overflow is what would happen, not a second line.
   DO NOT RAISE THIS NUMBER ON ITS OWN. Desktop goes bigger through a separate
   step in the 901px media query, where the column is measurably wider.

   IT LOOKS BIGGER THAN THE NUMBER SUGGESTS, and that is not wishful thinking:
   Cinzel is an all-caps design whose ascent is 0.976 of its em, against 1.079
   for Segoe UI - so Segoe spends more of each em on space for descenders this
   line never uses. Cinzel at 2.95rem draws caps close to the height Segoe drew
   at 3.3rem.

   NO LETTER-SPACING, unlike the site's other uppercase headings (.service-group
   h3 at .1em, .area-group h3 at .05em). Those are short labels with room; this
   line is the longest piece of type on the page and every 0.01em costs about 1%
   of its width budget. Add tracking only together with a lower cap.
   nowrap is the other half of the shrink rule: the line shrinks instead of
   breaking "ALPHA APPLIANCE / SERVICE" across two lines.
   THE FALLBACK IS A SERIF, deliberately - if the webfont never arrives, Georgia
   is NARROWER than Cinzel, so the line comes out small rather than overflowing.
   A sans fallback would have been the wrong way round. */
.hero-text .hero-brandname {
    margin: 2px 0 8px;
    font-family: "Cinzel Decorative", Georgia, "Times New Roman", serif;
    font-size: clamp(1rem, 5.4vw, 2.55rem);
    line-height: 1.15;
    font-weight: 700;
    text-transform: uppercase;
    white-space: nowrap;
}

/* Each of these starts its own line inside the Services intro, with the SAME
   gap above it - so the brand list, the exclusions and the case-by-case note
   read as three separate statements at even spacing. One class used for every
   one of them, which is what keeps the gaps equal; two classes would drift.
   display:block rather than a <br> so the gap is a real margin that can be
   tuned, and so a line can never end up sharing a row with the sentence before
   it at some width. The underline still draws only under the words, not across
   the whole block. */
.intro-line {
    display: block;
    margin-top: 14px;
}

/* plain lead line that sits under a section heading */
.section-intro {
    margin: -8px 0 16px;
    color: var(--muted);
}

/* Services and Service Area both lead with a centred heading block, and so do
   the five service pages - #service-page added 2026-10-18 at his ask to
   "center all 5 pages". Before that its h1 and intro were the only headings on
   the site sitting left while everything under them was centred, which read as
   an unfinished page rather than a choice. */
#services h2,
#services .section-intro,
#areas h2,
#areas .section-intro,
#service-page h1,
#service-page .section-intro {
    text-align: center;
}

/* the intro runs long here, so hold it to a readable measure */
#services .section-intro {
    max-width: 620px;
    margin-left: auto;
    margin-right: auto;
}

/* Contact box */
.contact-box {
    /* so the nowrap warning below sizes off this box, the way it does off
       the hero panel — without this, cqw would fall back to the viewport */
    container-type: inline-size;
    background: rgba(255, 255, 255, .04);
    padding: 24px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    text-align: center;

    /* NEW shadow */
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

/* ONE PIXEL OF RED AROUND EVERY BUBBLE, 2026-10-15 (his ask).
   All three of these classes draw the SAME object - the translucent white panel
   with the rounded corners - they just sit in different places: the two hero
   panels, the four .module-box bubbles across both pages, and the Contact box.
   Bordering one and not the others would have left those looking like a mistake,
   so the rule takes all three. If he meant only the big section bubbles, delete
   the other two selectors.
   It sits AFTER all three of their own rules on purpose: it ties them on
   specificity (0,1,0) and wins on source order, so none of them had to be
   edited and reverting is deleting this block.
   The gold stays where it was - on the content tiles (gallery photos, review
   cards, service bubbles) - so the site now reads red for the containers and
   gold for the things inside them. */
.hero-panel,
.module-box,
.contact-box {
    border: 1px solid var(--accent);
}

.contact-lead {
    font-size: 1.3rem;
    font-weight: 600;
    color: var(--text);
    margin: 0 0 18px;
}

/* business hours — white and a touch heavier than body copy, since this is
   the line people scan for */
.contact-box .contact-hours {
    color: var(--text);
    font-weight: 600;
    margin: 0 0 18px;
}

/* GONE 2026-10-18: .hours-note, which styled the after-hours line under the
   contact hours. He moved that sentence into the My Services intro on 10-18, so
   the only element this rule ever matched no longer exists. Removed rather than
   left behind - a rule that matches nothing is a trap for the next reader. */

/* the hours themselves, as the same jade pill the hero panel carries. The
   sentence in front of them stays OUTSIDE the pill - that part of the original
   note still holds.
   IT IS TWO LINES NOW, 2026-10-18, at his ask for the emergency note inside this
   bubble. The old comment here said a two-line pill "reads as a box rather than
   a badge", and that was true of the version it described: a full sentence set
   at the same size as the times. What makes it still read as a badge is that the
   second line is deliberately smaller and lighter, so the eye takes the hours as
   the statement and the note as its footnote. A fully-rounded 999px radius on a
   two-line box would have given stadium ends wide enough to look like a mistake,
   so the radius steps down to 16px - still a pill, no longer a lozenge. */
.hours-pill {
    display: inline-block;
    margin-top: 7px;
    padding: 7px 16px 8px;
    border-radius: 16px;
    background-color: var(--jade);
    background-image: linear-gradient(180deg, var(--jade) 0%, var(--jade-dark) 100%);
    /* white type - see the note on .panel-hours */
    color: #fff;
    font-weight: 700;
    line-height: 1.25;
}

/* "Emergency Service on Case-By-Case Basis". Small, and FULL WHITE - it sits
   back from the hours by SIZE and WEIGHT only, never by colour, and that is a
   measured decision rather than a stylistic one.
   var(--muted) was the obvious choice and is wrong here: it was picked against
   the page's purple and is close to unreadable on jade. Translucent white was
   the second attempt and is also wrong: at 80% it reads 3.43:1 against the top
   of the gradient, and at 92% it is 4.29:1 - both under the 4.5 that text this
   small needs. It would have passed in practice only because the second line
   happens to sit over the darker half, which is contrast by accident and would
   break the moment the padding or the line count changed.
   Full white is 4.77:1 at the light end of the gradient and 7.95:1 at the dark
   end, so it is safe wherever in the pill it lands.
   font-weight 400 against the pill's 700 is what makes it recede. */
.hours-pill-note {
    display: block;
    margin-top: 2px;
    font-size: .64rem;
    font-weight: 400;
    letter-spacing: .02em;
    color: #fff;
}

/* a break that only exists on small screens — off by default */
br.br-sm {
    display: none;
}

.contact-box p {
    color: var(--muted);
    margin: 0 0 12px;
}

.contact-box .hero-actions {
    margin: 0 0 14px;
}

/* Both big buttons in this box are held to ONE line at every width: the type
   shrinks to fit rather than wrapping. .contact-box is an inline-size
   container, so cqw measures this box, not the window — which is what makes
   nowrap safe here. 4.8cqw is deliberately ~10% under the width the longest
   label actually needs, because with nowrap an overshoot doesn't wrap, it
   pushes the whole page sideways.
   THE LABEL GOT SHORTER ON 2026-10-20, so this number is now MORE conservative
   than it was, not less: "New Customer Service Request Form" measured 346.7px at
   the 1.25rem cap and "New Customer Intake Form" measures 257.5px, a 26% drop.
   LEFT AS IT WAS ON PURPOSE - the rule still fits and raising it to reclaim the
   slack would only move this button out of step with its twin beside it.
   If a longer label is ever added here, drop this number, don't raise it. */
.contact-box .btn-large {
    white-space: nowrap;
    font-size: min(4.8cqw, 1.25rem);
}

/* .contact-box p sets the muted colour and is more specific than
   .panel-note-last, so the white has to be restated here */
.contact-box .panel-note {
    font-size: .72rem;
    margin: 0 0 8px;
}

.contact-box .panel-note-last {
    /* restated here for the same specificity reason the size is, explained
       below. White, because this line sits on the jade pill and #0a8268 takes
       white at 4.56:1 - if the accent is ever brightened, this flips too. */
    color: #fff;
    margin: 0 0 22px;

    /* THE OVERFLOW FIX. Same sentence as in the hero, but in here it was pinned
       to a FIXED .72rem with white-space: nowrap - so it could not shrink, and
       measured out 15px past the box at 390px wide and 45px past it at 360px.
       Cause is the same specificity trap this block already works around for
       colour: `.contact-box .panel-note` scores (0,2,0) and beats
       `.panel-note-last` at (0,1,0), so the responsive cqw size never reached
       this copy of the line at all. Restated here, at (0,2,0) and later in the
       file, which is the convention the colour above already follows.
       cqw is correct unqualified here: .contact-box IS the inline-size
       container and this line is its direct child, so cqw already measures the
       right box - no extra container needed, unlike in the hero. */
    font-size: min(3.1cqw, .95rem);
}

.footer-licensed {
    display: block;
    margin-top: 4px;
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--gold);
}

/* The Terms link, 2026-10-16, under Licensed & Insured and deliberately small -
   his words were "very small". .7rem is 11.2px, which is the smallest I will set
   running text on this site; --muted on the page purple is about 11:1 contrast,
   so it stays easily readable at that size. It is underlined because at 11px a
   link that is only a colour change does not read as a link at all. */
.footer-terms {
    display: block;
    margin-top: 6px;
    font-size: .7rem;
    letter-spacing: .04em;
    color: var(--muted);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.footer-terms:hover,
.footer-terms:focus-visible {
    color: var(--text);
}

/* ---------------------------------------------------------------------------
   THE TERMS PAGE, 2026-10-16. One module box of running text, so it only needs
   the few things the rest of the site never had: a styled bare h3 (the only
   other h3 rules on the site are scoped to #services and #areas), readable
   paragraphs and a plain bulleted list.
   --------------------------------------------------------------------------- */
/* TWO SIZES BIGGER, 2026-10-25, at his ask - and the floor is measured, not
   chosen. "Terms & Conditions" in Segoe UI Bold is 274px at the old 1.8rem and
   299px at 2.0rem. The phone is the binding case: .container.module-box gives
   min(viewport, --content-max - 2*--gutter) - 48, so a 360px phone has 312px,
   and 2.1rem (319px) wraps the title. His shrink-don't-wrap rule asks for about
   10% under, which lands on 1.9rem - 280px against 312, 10.3% of slack.

   The 1rem + 4vw middle term is set so the floor WINS at exactly 360px
   (16 + 14.4 = 30.4px = 1.9rem) and the cap takes over from about 560px up:

       viewport   font    title   box    slack
          360     30.4px  280px   312     10.3%
          390     31.6px  291px   342     14.9%
          500     36.0px  332px   452     26.5%
          901+    38.4px  356px   736     51.6%

   KNOWN AND ACCEPTED: below about 330px the title wraps to two lines. It
   already did at 1.8rem (274px against a 272px box at viewport 320), so this
   is not a regression - and 320px is an iPhone 5. */
.terms-block h1 {
    margin: 0 0 14px;
    font-size: clamp(1.9rem, 1rem + 4vw, 2.4rem);
    line-height: 1.2;
}

/* THE ONLY h2s ON THIS PAGE are "The Basics" and "The Technicals", 2026-10-25.
   They are the top-level split he asked for, so they outrank the h3 section
   headings: gold rather than jade, bigger, and ruled off underneath. Both fit
   a 360px phone with room to spare - "The Technicals" is the longer of the two
   at 156px against 312. */
.terms-block h2 {
    margin: 44px 0 4px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--border);
    /* 1.8rem is a FLOOR WITH A JOB: his own #1-#19 headings are 18pt against
       10.5pt body, which is 1.714rem below, and an h2 has to stay above its
       h3s. On a 360px phone "The Technicals" is 215px against 312, so there is
       room for the cap. */
    font-size: clamp(1.8rem, 1.3rem + 2vw, 2rem);
    color: var(--gold);
}

/* ...except the first one, which follows the intro line rather than a section,
   so it does not need the full gap above it. */
.terms-block h2:first-of-type {
    margin-top: 26px;
}

.terms-block h3 {
    margin: 26px 0 8px;
    font-size: 1.2rem;
    color: var(--jade);
}

/* ===========================================================================
   THE TECHNICALS - his Terms Of Service document, carried over as he set it.
   2026-10-25. REWRITTEN the same evening after he looked at the first attempt:
   "none of this is centered, i can barely tell bold text and no colored text
   as it was in the original document. i also dont see where text sizes i set
   are coming though. also, i was very deliberate with my linebreaks."

   He was right on every count. The first converter read only the ODT's
   text-properties, so alignment, colour and size never left the file. Every
   number below comes out of the document itself:

     SIZE LADDER - his three sizes, as ratios against his 10.5pt body, so the
     relationships he set survive instead of being re-chosen by me:
         18pt  / 10.5 = 1.714   the #1-#19 headings      -> h3 below
         12pt  / 10.5 = 1.143   the document default     -> .tos-lg
         10.5pt        = 1.000  section body             -> the block's base
          6pt  / 10.5 = 0.571   ONE span, the Bitcoin
                                line, fine print on
                                purpose                  -> .tos-xs

     ALIGNMENT - every paragraph in the document is text-align:center. All of
     them, not some, which is why "none of this is centered" was exactly right.

     BLANK LINES - 48 single, 22 double, one run of four. His paragraphs carry
     NO bottom margin, so in his document the blank lines do all the spacing.
     That is why collapsing them flattened the page. Margins are zeroed here
     and .tos-gap-N carries the space instead, one run at a time.

     COLOUR - most paragraphs say #000000, which is only the default black and
     is ignored; black on #2c0735 would be invisible. The reds are real: the
     whole #16 heading plus four spans in #4 at #ff0000, and one span in #2 at
     #c9211e. Both map to --accent (#e51837) - still red, and brighter against
     this background than #ff0000, which only manages about 3.5:1.

     BOLD - the body of this block is --muted and bold is --text at 800, so the
     two differ in weight AND brightness. At one weight step on a dark
     background they did not read apart, which is the "barely tell" complaint.
   =========================================================================== */
.terms-technicals {
    text-align: center;
    color: var(--muted);
}

.terms-technicals p,
.terms-technicals h3 {
    margin: 0;
}

.terms-technicals p {
    line-height: 1.55;
}

/* his 18pt section headings. NOT jade, unlike the headings in The Basics: in
   his document they are plain text at the default colour, and leaving them
   plain is what lets the one red heading - #16 - actually mean something. */
.terms-technicals h3 {
    font-size: 1.714rem;
    line-height: 1.25;
    color: var(--text);
}

.terms-technicals .tos-lg {
    font-size: 1.143em;
}

.terms-technicals .tos-xs {
    font-size: .571em;
}

.terms-technicals .tos-red {
    color: var(--accent);
}

.terms-technicals strong {
    font-weight: 800;
    color: var(--text);
}

.terms-technicals u {
    text-underline-offset: 3px;
}

/* One blank paragraph in his document = one line of space. These are not
   decoration; they are his line breaks, counted. */
.tos-gap {
    height: 1em;
}

.tos-gap-2 {
    height: 2em;
}

.tos-gap-4 {
    height: 4em;
}

/* His "~ Damage Waivers ~" line, tildes and all - they are his, and with the
   whole block centred they read the way he typed them. The rule above it is
   the only addition, to group #14-#19 off from #13. */
.terms-technicals h3.terms-divider {
    font-size: 1.3rem;
    letter-spacing: .1em;
    color: var(--gold);
    border-top: 1px solid var(--border);
    padding-top: 26px;
}

.terms-block p {
    margin: 0 0 12px;
    line-height: 1.6;
}

.terms-block ul {
    margin: 0 0 12px;
    padding-left: 22px;
}

.terms-block li {
    margin: 0 0 6px;
    line-height: 1.45;
}

.terms-block a {
    color: var(--gold);
}

.terms-updated {
    margin-top: 24px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
    font-size: .8rem;
    color: var(--muted);
}

/* GONE 2026-10-17: .page-panel, written for page 2 before page 2 existed. When
   it was built it used .module-box, and so did terms.html - the rules had never
   styled a single element. */

#reviews h1,
#reviews .section-intro,
#gallery h2,
#gallery .section-intro {
    text-align: center;
}

/* REVIEWS, 2026-10-14. 44 quotes transcribed from his own screenshots.
   CSS multi-column rather than grid, deliberately: the quotes run from six words
   to ninety, and columns packs uneven heights with no gaps, which a grid cannot
   do without JavaScript. Two columns at most - at the 880px content width three
   would leave about 270px a card, and a 90-word quote in a 270px column is a
   newspaper, not a review. */
/* Sits UNDER the reviews, inside the bubble - read the quotes first, then go and
   check them. Margin is top-only now that nothing follows it. */
/* flex rather than plain centring because this now holds TWO buttons (Google and
   Facebook). Side by side they measure ~480px, so on a phone they have to be
   allowed to stack - flex-wrap does that, and the gap replaces the collapsing
   inline whitespace that would otherwise set their spacing. text-align stays as
   the fallback for the single-button copy at the top. */
.review-cta {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
    text-align: center;
    margin: 22px 0 0;
}

/* the Leave A Review button under the intro, 2026-10-15. It needs air BELOW it
   (the version at the bottom of the section only ever needed air above). */
.review-cta-top {
    margin: 16px 0 24px;
}

/* Half-size, measured rather than eyeballed. The .btn-large it used to carry made
   it 350 x 56px; this makes it 228 x 37px, which is 65% of the width and 67% of
   the height - about 44% of the area. Scoped to .review-cta rather than done by
   dropping a class, so it cannot resize any other button on the site.
   Note the font-size is the lever that matters: the LABEL is most of the width,
   so padding alone would barely move it. */
.review-cta .btn {
    font-size: .8rem;
    padding: 9px 14px;
}

.review-grid {
    columns: 2;
    column-gap: 14px;
}

/* break-inside is what stops a card being sliced across the column break; the
   inline-block + width pair is the old belt-and-braces for the same thing, which
   some engines still need */
.review-card {
    display: inline-block;
    width: 100%;
    break-inside: avoid;
    margin: 0 0 14px;
    padding: 16px;
    background: rgba(255, 255, 255, .04);
    /* GOLD 2px, matching the Services dropdown buttons on page 1 - his call,
       2026-10-14, to tie the two pages together. The gallery tiles below carry
       the same. If it ever reads too heavy, the dial is this width, not the
       colour: 1px gold still reads gold and halves the weight on the page. */
    border: 2px solid var(--gold);
    border-radius: var(--radius);
    box-shadow: var(--shadow-card), var(--shadow-inset);
}

.review-card blockquote {
    margin: 10px 0 12px;
    font-size: .92rem;
    line-height: 1.55;
    color: var(--text);
}

/* The customer's name. Only the NAME takes this colour - the Google/Facebook
   label beside it is a .rv-src span with its own muted grey, so it is unaffected.
   JADE, 2026-10-15, at his ask, AND THE NUMBER IS WORSE HERE THAN ANYWHERE ELSE
   jade is used, which is worth knowing before this gets copied around: a review
   card is two 4% white layers over the page, so its background resolves to about
   rgb(61,26,69) - LIGHTER than the page - and #0a8268 measures 3.08:1 on it,
   against the 4.63:1 gold was giving. These names are .85rem, which needs 4.5.
   A brighter #04c2bb would have taken them to 6.59:1, but he tried that colour
   the same day and did not like it, so these sit UNDER the bar by his choice,
   knowingly, on the hardest surface on the site. If it ever needs to pass
   without changing the hue, #0DA382 is the same colour a quarter-step lighter
   and measures 4.59:1 here. */
.review-card figcaption {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    font-size: .85rem;
    font-weight: 700;
    color: var(--jade);
}

/* Google: the real star emoji, five of them, at his request. The aria-label on
   the span carries "5 out of 5 stars" so a screen reader says it once instead of
   reading the word star five times. */
.rv-stars {
    display: block;
    font-size: .95rem;
    letter-spacing: .06em;
    line-height: 1;
}

/* Facebook: NO stars, and that is not an oversight. Meta replaced star ratings
   with "Recommends" in 2018, so a Facebook review has no star value to show -
   printing five would be inventing a rating the customer never gave. */
.rv-rec {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: .8rem;
    font-weight: 700;
    letter-spacing: .02em;
    color: #93b7f3;
}

.rv-fb {
    width: 1.05rem;
    height: 1.05rem;
    flex: none;
    fill: #1877f2;
}

/* the source tag, right-aligned opposite the name */
/* GOOGLE / FACEBOOK, the source tag beside the name. Jade since 2026-10-15 at
   his ask - it was --muted grey, which is 9.87:1 on a review card against jade's
   6.59:1, so this spends contrast but keeps plenty: .72rem needs 4.5 and this
   clears it. The name beside it is the same colour, which is what he wanted. */
.rv-src {
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--jade);
}

@media (max-width: 700px) {
    .review-grid {
        columns: 1;
    }
}

/* GALLERY, 2026-10-14. Three per row, his call. */
.gallery-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
}

/* LOAD-BEARING, and the same trap the lightbox hit on 2026-10-14. [hidden] is a
   BROWSER-stylesheet rule, so ANY author rule that sets display beats it - and
   the rule directly above sets display:grid. Without this line the hidden roll
   is not hidden at all: both galleries render, 101 tiles, and the switch appears
   to do nothing but change its own label. It is one line and it is the entire
   difference between the two-roll gallery working and not. */
.gallery-grid[hidden] {
    display: none;
}

.gallery-item {
    display: block;
    /* the tile is the frame; the image inside keeps its own shape */
    background: rgba(255, 255, 255, .04);
    /* same gold 2px as the review cards and the page-1 dropdowns */
    border: 2px solid var(--gold);
    border-radius: var(--radius);
    box-shadow: var(--shadow-card), var(--shadow-inset);
    padding: 8px;
    position: relative;
    overflow: hidden;
    transition: transform .12s ease, box-shadow .12s ease;
}

/* hover can no longer be "turn the border gold" - it already is - so it lifts
   and adds a gold halo instead */
.gallery-item:hover,
.gallery-item:focus-visible {
    transform: translateY(-2px);
    box-shadow: var(--shadow-card), 0 0 0 3px rgba(183, 136, 76, .4);
}

.gallery-item picture {
    display: block;
}

/* CONTAIN, NOT COVER, and this is the one rule here that actually matters.
   These are before/after pairs: cover would crop the sides to fill the box and
   quietly cut off part of the "before" or the "after", which destroys the whole
   point of the photo. contain letterboxes instead, so every tile shows the
   complete pair. The fixed aspect-ratio is what keeps the three columns tidy
   while the images underneath run anywhere from 1.5:1 to 2.7:1. */
.gallery-item img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: contain;
    background: #1a1114;
    border-radius: 8px;
}

.gallery-ba {
    display: block;
    margin-top: 7px;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-align: center;
    color: var(--gold);
}

/* THE SWITCH between the two rolls, 2026-10-16. A .btn so it reads as a button
   and picks up the sheen for free, centred, with air under it so it never looks
   like it belongs to the grid below it. */
.gallery-switch {
    display: flex;
    justify-content: center;
    margin: 0 0 18px;
}

/* PLAY BADGE on a clip's tile. Drawn in CSS rather than shipped as an icon file:
   a circle plus a triangle made from a border trick, so it costs no request and
   has nothing to go blurry.
   pointer-events: none is load-bearing - the whole tile is one link, and a badge
   that swallowed the click would make the clips the only tiles that felt broken.
   It needs no positioning context of its own; .gallery-item is already
   position: relative for the sheen. */
.gallery-play {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 54px;
    height: 54px;
    margin: -27px 0 0 -27px;
    border-radius: 50%;
    background: rgba(44, 7, 53, .72);
    border: 2px solid var(--gold);
    pointer-events: none;
    z-index: 2;
}

.gallery-play::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    /* nudged 3px right of true centre: a triangle's visual mass sits left of its
       bounding box, so a mathematically centred play symbol looks off */
    margin: -10px 0 0 -5px;
    border-style: solid;
    border-width: 10px 0 10px 16px;
    border-color: transparent transparent transparent var(--gold);
}

@media (max-width: 520px) {
    .gallery-play {
        width: 40px;
        height: 40px;
        margin: -20px 0 0 -20px;
    }

    .gallery-play::after {
        margin: -7px 0 0 -4px;
        border-width: 7px 0 7px 11px;
    }
}

/* GOLD SHEEN, 2026-10-15. One diagonal highlight that sweeps across the photo:
   on hover on desktop, on press on a phone, and once more when the enlargement
   bubble opens. Shared by the grid tile and the bubble so the two are the same
   gesture at two sizes.

   It is a ::after rather than a real element so it costs no markup, and it moves
   with transform + opacity ONLY - both of which the compositor can animate
   without repainting the photo underneath. The tile already has overflow:hidden,
   which is what clips the highlight to the frame; .lb-frame below gets the same.
   The band is taller and wider than it needs to be and rotated, so the leading
   edge clears the corners instead of cutting in as a visible triangle. */
.gallery-item::after,
.lb-frame::after {
    content: "";
    position: absolute;
    top: -60%;
    left: -75%;
    width: 45%;
    height: 220%;
    pointer-events: none;
    opacity: 0;
    transform: translateX(0) rotate(12deg);
    background: var(--sheen-grad);
}

@keyframes sheen-sweep {
    0%   { opacity: 0; transform: translateX(0) rotate(12deg); }
    10%  { opacity: 1; }
    90%  { opacity: 1; }
    100% { opacity: 0; transform: translateX(430%) rotate(12deg); }
}

/* hover is desktop-only on purpose: on a touch screen :hover sticks after a tap
   and the highlight would sit there frozen mid-sweep */
@media (hover: hover) {
    .gallery-item:hover::after {
        animation: sheen-sweep var(--sheen-dur, 1s) ease-out;
    }
}

/* :active is the phone's half of it - the sweep runs under the finger. It also
   covers keyboard users via :focus-visible. */
.gallery-item:active::after,
.gallery-item:focus-visible::after {
    animation: sheen-sweep var(--sheen-dur, 1s) ease-out;
}

/* THE ONE THAT ACTUALLY RUNS, 2026-10-15. site.js adds this class on the way in
   and takes it off only when the browser reports the animation finished, so the
   sweep completes whether or not the pointer is still there - Kevin's ask. The
   :hover and :active rules above are kept as the no-JS fallback; they set the
   same animation to the same value, so they cannot cause a second sweep (an
   animation restarts only when that value changes, and it never does). */
.gallery-item.is-sheen::after {
    animation: sheen-sweep var(--sheen-dur, 1s) ease-out;
}

/* ---------------------------------------------------------------------------
   THE SAME SHEEN ON EVERY BUTTON - Kevin, 2026-10-15, DESKTOP ONLY.
   His list was every .btn on both pages and the five service bubbles; I checked
   it against the markup and .btn is an exact match for the nine on page 1 and
   the five on page 2, with nothing else wearing the class. So this is selected
   by .btn rather than by a list that would need maintaining.

   THE WHOLE BLOCK IS INSIDE @media (hover: hover) and that is deliberate on two
   counts. First, he asked for desktop. Second, overflow:hidden is what clips the
   highlight to the button - and on a narrow phone a nowrap label that overshoots
   its button currently spills out and stays READABLE; clipped, it would be cut
   off mid-word. Scoping it here means phones are not touched at all.

   THE PSEUDO-ELEMENT ITSELF IS DECLARED IN HERE TOO, not added to the shared
   block above, and that is the subtle one. An absolutely positioned child still
   counts toward its container's scrollable overflow even at opacity 0 - so a
   band declared for a phone, where there is no overflow:hidden to clip it,
   could have pushed the page sideways without ever being visible. Declaring it
   inside the guard means it does not exist on a touch device at all.

   The bubbles use ::before because their ::after is already the open/close
   chevron. The clipping that keeps the sweep inside the card's rounded corners
   now lives on .service-accordion itself, OUTSIDE this block - see the comment
   there. It moved on 2026-10-15 because the watermark needs it too, and a
   watermark is not a hover effect.
   --------------------------------------------------------------------------- */
@media (hover: hover) {

    .btn,
    .service-accordion>summary {
        position: relative;
        overflow: hidden;
    }

    /* geometry differs from the gallery's on one axis only: a button is much
       wider than it is tall, and the band is rotated, so at 220% of a 56px
       button the overhang barely covers the corners. 300% costs nothing - it is
       clipped away - and keeps the leading edge off the corner. */
    .btn::after,
    .service-accordion>summary::before {
        content: "";
        position: absolute;
        top: -100%;
        left: -75%;
        width: 45%;
        height: 300%;
        pointer-events: none;
        opacity: 0;
        transform: translateX(0) rotate(12deg);
        background: var(--sheen-grad);
    }

    /* the duration comes from site.js, per element, so every sweep on the site
       moves at the same pixels-per-second. The fallback is only what shows if
       the script never runs.
       .is-sheen is the one that does the work - see the gallery rule above and
       the long comment in site.js. :hover is the no-JS fallback. */
    .btn.is-sheen::after,
    .btn:hover::after,
    .btn:focus-visible::after,
    .service-accordion>summary.is-sheen::before,
    .service-accordion>summary:hover::before {
        animation: sheen-sweep var(--sheen-dur, 1s) ease-out;
    }
}

/* 3 across is too tight for a before/after pair on a tablet, and hopeless on a
   phone - each half would be under 60px wide. */
@media (max-width: 760px) {
    .gallery-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 520px) {
    .gallery-grid {
        grid-template-columns: 1fr;
    }
}

/* LIGHTBOX, 2026-10-14. The markup for this is built by site.js and never exists
   without JavaScript, so none of these rules can affect a no-JS visitor. */
.lb {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(12, 2, 15, .82);
    opacity: 0;
    transition: opacity .15s ease;
}

/* THIS RULE IS LOAD-BEARING. `hidden` only hides an element because the BROWSER
   stylesheet says [hidden] { display: none }, and any author rule beats the
   browser's. `.lb { display: flex }` above is an author rule, so without this the
   lightbox would sit open over the page from the moment it is built. */
.lb[hidden] {
    display: none;
}

.lb.is-open {
    opacity: 1;
}

/* the bubble. Same material as every other panel on the site - translucent fill,
   gold border, the shared radius - so it reads as part of the page rather than a
   generic viewer dropped on top of it. */
/* ONE SIZE FOR EVERY ENLARGEMENT, 2026-10-16, his ask: "so when scrolling
   enlarged images, the scroll arrow isnt moving around."
   WHY IT MOVED: the panel used to shrink-wrap whatever was in it, and the two
   galleries hold 32 landscape photos and 61 portrait ones, ratios from 2.69 to
   0.40 - measured, not guessed. Every step changed the panel's width AND its
   height, and the arrows sit at top:50% of the panel with left/right:-16px, so
   they chased the picture around the screen. The caption moved it too: 1 line
   for most, 2 for the longest at desktop width.
   THE FIX IS THREE FIXED NUMBERS - panel WIDTH, frame HEIGHT, caption HEIGHT.
   With all three constant the panel is constant, and since it is centred in a
   fixed overlay it lands in exactly the same place every time. Nothing else
   had to change: the arrows keep their existing position rules.
   WHAT IT COSTS, and it is a real cost he should see: a panorama and a tall
   portrait cannot both fill one box, so each now sits letterboxed inside it
   rather than the box hugging the picture. No image is displayed SMALLER than
   before except on a very tall monitor, where the 720px cap bites - without
   that cap a 2.69 panorama would float in the middle of a 940px-tall void. */
.lb-panel {
    position: relative;
    width: min(1100px, 100%);
    max-height: 100%;
    display: flex;
    flex-direction: column;
    padding: 12px;
    background: var(--bg);
    border: 2px solid var(--gold);
    border-radius: var(--radius);
    box-shadow: 0 24px 60px rgba(0, 0, 0, .7);
}

/* The photo's frame inside the bubble. It exists for two reasons now: an <img>
   cannot have a ::after, so the sheen needs something wrapped around it to live
   in - and since 2026-10-16 it is also THE fixed box that stops the panel
   resizing between photos.
   It no longer shrink-wraps the picture. It is the full width of the panel and a
   FIXED height, and it centres whatever is handed to it. The old align-self is
   gone with the shrink-wrapping; the sheen is clipped to this box, which is now
   bigger than the photo on most steps - acceptable, since the sweep reads as a
   highlight over the frame either way.
   THE HEIGHT: `100vh - 150px` is the room left once everything around it is
   accounted for - 40px of .lb padding, 24px of panel padding, 4px of border,
   47px of two reserved caption lines and 16px of counter, which is 131 with
   19px of slack so the panel can never be clamped by its own max-height.
   The 720px cap only bites on a tall monitor, and it is there so a wide photo
   is not marooned in the middle of a 940px-tall empty box. */
.lb-frame {
    position: relative;
    width: 100%;
    height: min(calc(100vh - 150px), 720px);
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
    overflow: hidden;
    border-radius: 8px;
    line-height: 0;
}

/* replayed on every open by site.js, which removes the class and forces a reflow
   so the animation restarts rather than being ignored as already-applied */
.lb-frame.is-sheen::after {
    animation: sheen-sweep var(--sheen-dur, 1.2s) ease-out .05s;
}

/* The picture now sizes itself to the FIXED frame above rather than to the
   viewport: whichever of its two dimensions runs out first stops it, and the
   other is centred by the frame's flexbox. That is what lets one box hold a
   2.69 panorama and a 0.40 portrait without either being cropped or the box
   changing size.
   The old `max-height: calc(100vh - 140px)` moved here as the frame's height,
   where it belongs - it was always really a statement about the box.
   `max-height: 100%` ONLY WORKS BECAUSE THE FRAME NOW HAS A DEFINITE HEIGHT. A
   percentage max against an auto-height parent resolves to nothing at all, which
   is exactly the bug that let three brand logos hang out of their chips on
   2026-10-15. If anyone ever makes .lb-frame auto-height again, this line
   silently stops doing anything and tall photos will overflow.
   min-width/min-height: 0 are the flexbox floor. A flex item's automatic minimum
   size is its CONTENT size, so a 2048px photo in a row flex container can refuse
   to shrink; zeroing both lets max-width/max-height actually govern. */
.lb-img,
.lb-video {
    display: block;
    min-width: 0;
    min-height: 0;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border-radius: 8px;
}

/* The clip and the photo share one frame and only one is ever in it, so they
   share the sizing rule above and differ only here.
   THE OLD WIDTH FLOOR AND CAP ARE GONE, 2026-10-16. Both existed to stop a
   <video> with no metadata yet from collapsing the shrink-wrapping panel, and
   to stop a 336px-wide phone clip being blown up on a desktop. The frame is a
   fixed box now, so there is nothing left to collapse - and all eight clips are
   portrait at roughly 0.5, so inside a 720px-tall frame they come out about
   360px wide on their own, well under the 480 the cap used to impose. Keeping a
   dead cap would only have surprised whoever adds a landscape clip. */
.lb-video {
    background: #000;
}

/* hidden rather than removed, so switching between a photo and a clip never
   rebuilds the panel */
.lb-img[hidden],
.lb-video[hidden] {
    display: none;
}

/* THE CAPTION TRACKS THE PHOTO'S WIDTH, not the panel's. 2026-10-15, his report.
   The panel is a shrink-to-fit flex column, so its width came out as the widest
   thing inside it - and that was the CAPTION, whose max-content width is the
   whole sentence on one line. A long caption therefore stretched the panel to its
   full 1100px and left a portrait photo stranded in the middle of it with huge
   empty margins either side.
   width:0 drops the caption's max-content contribution to nothing, so it can no
   longer push the panel open; min-width:100% then pulls it back out to whatever
   width the panel actually settled at, which is now the photo's width. The text
   wraps to the edges of the image above it.
   And yes, this one wraps on purpose. The shrink-don't-wrap rule is about buttons
   and headings - short labels that must stay on one line - not about running
   prose, which has to wrap or it cannot be read at all.
   THE HEIGHT IS FIXED AT TWO LINES, 2026-10-16, and it is the third of the three
   numbers that hold the panel still. Measured across all 101 captions at the
   1072px the panel gives them: 99 need one line and 2 need two, so two lines
   covers every caption on the site with nothing reserved for nothing. The
   width:0 trick above is now doing less work than it was - the panel no longer
   shrink-wraps - but it stays, because without it a long caption would still be
   a max-content contribution fighting the fixed width. */
.lb-cap {
    width: 0;
    min-width: 100%;
    height: 2.8em;
    overflow-y: auto;
    margin: 10px 0 2px;
    font-size: .78rem;
    line-height: 1.4;
    color: var(--muted);
    text-align: center;
}

/* PHONES NEED A TALLER CAPTION BOX. Same 101 captions measured at the 322px a
   390px phone gives them: 51 fit one line, 31 need two, 18 need four and one
   needs six. Four lines is the honest cut - it covers 100 of the 101, and the
   one that does not (the long range-wiring caption) scrolls inside its box
   rather than being allowed to move the panel, which is the whole point. The
   frame gives back the two extra lines it costs. */
@media (max-width: 700px) {
    .lb-frame {
        height: calc(100vh - 185px);
    }

    .lb-cap {
        height: 5.6em;
    }
}

/* sits ON the panel's corner, not outside it, so it is never off-screen on a
   phone and never overlaps the photo's own content area */
.lb-close {
    position: absolute;
    top: -14px;
    right: -14px;
    width: 38px;
    height: 38px;
    display: flex;
    align-items: center;
    justify-content: center;
    font: inherit;
    font-size: 1.5rem;
    line-height: 1;
    color: var(--text);
    background: var(--accent);
    border: 2px solid var(--gold);
    border-radius: 50%;
    cursor: pointer;
    /* a 38px circle is already at the lower end of a comfortable touch target,
       so it does not shrink any further on small screens */
    padding: 0 0 3px;
}

.lb-close:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(183, 136, 76, .55);
}

/* STEP ARROWS, 2026-10-14. Half outside the panel, same trick as the close
   button, and the placement is the point: the photos use object-fit: contain, so
   there is no guaranteed dark margin inside the frame for an arrow to sit on. An
   arrow floated over the image would land on the "after" half of a before/after
   pair - the exact thing the photo exists to show. Outside, it never covers
   anything. .lb's own 20px of padding means a 16px overhang still clears the
   viewport edge on a 320px phone. */
.lb-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    color: var(--text);
    background: var(--accent);
    border: 2px solid var(--gold);
    border-radius: 50%;
    cursor: pointer;
}

.lb-prev {
    left: -16px;
}

.lb-next {
    right: -16px;
}

.lb-nav:hover {
    filter: brightness(1.15);
}

.lb-nav:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(183, 136, 76, .55);
}

/* a drawn path, so it is centred by geometry rather than by whatever the font
   decides the chevron's side bearings should be */
.lb-chev {
    display: block;
    width: 20px;
    height: 20px;
}

/* same width:0 trick as the caption - "12 / 45" is short enough that it would
   rarely matter, but a counter that can widen the panel is a bug waiting for a
   three-digit gallery */
.lb-count {
    width: 0;
    min-width: 100%;
    margin: 4px 0 0;
    font-size: .7rem;
    letter-spacing: .08em;
    color: var(--gold);
    text-align: center;
}

/* stop the page behind scrolling while the bubble is up */
body.lb-open {
    overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {

    .lb,
    .gallery-item {
        transition: none;
    }

    /* the sheen is decoration, so it goes entirely rather than going faster */
    .gallery-item::after,
    .lb-frame::after,
    .btn::after,
    .service-accordion>summary::before {
        display: none;
    }

    .gallery-item:hover::after,
    .gallery-item:active::after,
    .gallery-item:focus-visible::after,
    .gallery-item.is-sheen::after,
    .lb-frame.is-sheen::after,
    .btn:hover::after,
    .btn:focus-visible::after,
    .btn.is-sheen::after,
    .service-accordion>summary:hover::before,
    .service-accordion>summary.is-sheen::before {
        animation: none;
    }
}

.site-footer {
    border-top: 1px solid var(--border);
    padding: 18px 0;
    color: var(--muted);
    box-shadow: var(--shadow-inset);
}

/* position:relative is gone with the visit counter it used to anchor */
.footer-inner {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 40px;
}

.footer-copy {
    margin: 0;
    text-align: center;
    font-size: .95rem;
}

/* Responsive */
@media (max-width: 900px) {
    .brand img {
        height: 94px;
    }

    .hero {
        padding: 34px 0;
    }

    .areas-wrap {
        grid-template-columns: 1fr;
    }

    .map-card iframe {
        min-height: 300px;
    }
}

/* Tablet-width desktops (Surface Pro is 912px). Everything stays — logo,
   nav, review and the form button — by scaling the header down enough for
   all four to fit on one row, instead of collapsing to the hamburger. */
@media (min-width: 901px) and (max-width: 1199px) {
    .brand img {
        height: 104px;
    }

    .header-inner {
        gap: 10px;
    }

    .main-nav a {
        font-size: 15px;
        padding: 9px 6px;
    }

    .site-header .btn {
        font-size: 12px;
        padding: 8px 11px;
    }
}

@media (max-width: 520px) {
    /* Tighter breathing room on small phones. Setting the VARIABLE rather than
       .container's padding is what keeps the module bubbles in step - they
       inset themselves by this same number, so one line moves both. It also
       removes the ordering hazard that used to live here: this rule used to
       restate .container's padding, which (equal specificity, later in the
       file) silently wiped the padding off any bubble that was itself a
       .container. */
    :root {
        --gutter: 14px;
    }

    /* The review button is the last thing in the box, and 24px left it sitting
       too close to the bottom edge at phone width. Raised to match the
       specificity of the module-box padding rule above - as a bare .contact-box
       at (0,1,0) this would now LOSE to that rule's (0,3,1) and his extra
       bottom space would vanish. */
    .section>.container.contact-box {
        padding-bottom: 40px;
    }

    /* break the greeting after the comma instead of mid-phrase */
    br.br-sm {
        display: inline;
    }

    /* make room for the review button beside the hamburger */
    .brand img {
        height: 76px;
    }
}

/* Narrow phones: the header bar carries logo + review button + hamburger,
   which together were wider than the screen and forced a sideways scroll. */
@media (max-width: 400px) {
    .header-inner {
        gap: 8px;
        /* side padding was 0 here, which is why the hamburger and the wing
           tip were sitting hard against the screen edges */
        padding: 7px 7px 4px;
    }

    .brand img {
        height: 62px;
    }

    body.nav-collapsed .btn-icon img {
        height: 22px;
    }

    .hamburger {
        border-width: 7px;
        padding: 5px;
    }

    .hamburger-bar {
        width: 22px;
    }

    /* below ~400px even the first half of the greeting won't fit at 1.3rem */
    .contact-lead {
        font-size: 1.1rem;
    }
}

/* Desktop. The root font-size scale-up that used to live here is gone —
   text is back to the browser's own size at every width. The layout widths
   below stay, since those are composition rather than type scale. */
@media (min-width: 901px) {
    /* steps the hero column AND all three module bubbles together */
    :root {
        --content-max: 820px;
    }

    .tech-banner img {
        max-width: 290px;
    }

    /* The header measures its own contents to decide when to collapse into
       the hamburger. Scaling the root font size inflated everything in it,
       pushing the measurement past the window and forcing the collapse on
       desktop. Pin the header's type so page scaling can't reach it. */
    .main-nav a {
        font-size: 16px;
    }

    /* buttons set smaller than the nav: their labels are long, and at 16px
       they were costing more width than the logo */
    .site-header .btn {
        font-size: 13px;
    }

    /* LAUNDRY'S TWO CARDS SIZED LIKE KITCHEN'S THREE, 2026-10-17 (his ask:
       "the washer and dryer dropdown are bigger than the other 3 which scales
       the background image differently").
       Both grids are `repeat(auto-fit, minmax(220px, 1fr))`, so the track width
       is set by how many cards are IN the grid, not by the page - Kitchen's
       three came out 237px and Laundry's two came out 362px. The watermark is
       background-size: cover, so a card half again as wide crops the picture
       differently; equal tracks fix the picture for free.
       The track here is the same arithmetic Kitchen's auto-fit does:
       (row - one gap x 2) / 3. Measured: the module's inner width is 736px in
       the 901-1299 band and 796px at 1300+, giving 237.3px and 257.3px - both
       identical to the card beside them. justify-content centres the short row
       under the KITCHEN trio instead of leaving a hole on the right.
       WHY min-width: 901px IS EXACTLY RIGHT, not a guess - 901px is where
       --content-max steps 720 -> 820, which takes the module's inner width from
       636px to 736px, and three 220px tracks plus two 12px gaps need 684px.
       So Kitchen is 2-across below this line (where Laundry already matches it)
       and 3-across above it. If --content-max or minmax() ever change, this
       breakpoint has to be re-measured with them. */
    .service-grid-pair {
        grid-template-columns: repeat(2, calc((100% - 24px) / 3));
        justify-content: center;
    }

    /* The lone Freezers card, pinned to the same third as Kitchen - identical
       arithmetic to the pair above with one track instead of two. The full
       measurement table is beside .service-grid-single further up the file. */
    .service-grid-single {
        grid-template-columns: calc((100% - 24px) / 3);
        justify-content: center;
    }

    /* THE BRAND NAME GETS ITS OWN DESKTOP SIZE, 2026-10-17, re-measured for
       Cinzel Decorative on 2026-10-18 (3.3rem -> 2.95rem - the face is 11.9%
       wider, so the same column holds less of it).
       The base rule's cap is pinned at 2.55rem by the 720-900px band, where the
       hero column is only 684px wide - but from 901px up --content-max steps to
       820 and the column is 784px, so there is room the cap cannot use.
       784px of box, minus the 6px ring, over 14.663px-per-font-px = 53.1px
       absolute; 10% back off that is 47.7px, and 2.95rem is 47.2px. Flat rather
       than a clamp because the column is a CONSTANT here: the viewport is past
       --content-max at every width in this query, so the box is 784px from 901
       to 1299 and 844px above that. The same 2.95rem is therefore ~89% of the
       budget at 901px and ~83% at 1300+ - it only gets safer as the screen grows.
       Lower this, not the base cap, if the name ever outshouts the h1. */
    .hero-text .hero-brandname {
        font-size: 2.95rem;
    }
}

@media (min-width: 1300px) {
    :root {
        --content-max: 880px;
    }

    .tech-banner img {
        max-width: 310px;
    }
}

/* Prevent visual flicker while recalculating nav mode */
body.nav-measuring .main-nav,
body.nav-measuring .desktop-only,
body.nav-measuring .mobile-only {
    visibility: hidden;
}

/* Expandable service cards */
.service-accordion {
    padding: 0;
    /* details controls its own padding */
    cursor: pointer;

    /* GOLD BORDER - TRIAL, 2026-10-13. Revert by deleting these two lines.
       .service-card sets `border: 1px solid var(--border)` and is the SAME
       specificity as this rule (0,1,0), so this wins only because it is
       further down the file. Don't move this block above .service-card.
       2px is safe on width because of the global box-sizing: border-box -
       the extra pixel eats inward, so the card does not grow and the grid
       track is unchanged. It costs 2px of the nowrap label's room, which
       had ~10% headroom at the tightest 3-column card; still clear. */
    border-color: var(--gold);
    border-width: 2px;

    /* Clips anything inside to the card's rounded corners - the hover sweep and,
       since 2026-10-15, the summary's watermark. Without it a rectangular child
       background pokes out past the gold border at all four corners, which is
       exactly what Kevin saw. It used to sit inside the @media (hover: hover)
       block with the sweep; a watermark is not a hover effect, so it lives here
       now and applies on every device. */
    overflow: hidden;
}

.service-accordion>summary {
    list-style: none;
    padding: 16px;
    font-weight: 800;

    /* ...and the summary rounds its OWN corners as well, which is belt and
       braces rather than duplication: the parent's overflow clips it, and these
       make the background box itself the right shape even if something later
       re-establishes a stacking or paint context that the clip does not reach.
       14px, not 16: the card's radius is var(--radius) measured on its OUTER
       edge, and the 2px gold border eats inward, so the inside curve is 14.
       Closed, the summary IS the card, so all four corners are round. Open, the
       brand wall sits underneath it and the bottom two have to be square -
       see the [open] rule below. */
    border-radius: 14px;
    text-align: center;
    user-select: none;

    /* WATERMARK ON THE DROPDOWN BUTTONS, 2026-10-15 (his ask). Same idiom as the
       header: the photo underneath, a flat purple wash laid over it, and the
       label on top.
       It goes on the SUMMARY, not on the card, so it stops at the button and the
       brand wall below keeps the plain panel - which is what makes the button
       read as a button once the card is open.
       .80 is heavier than the header's .72 and that is deliberate: this strip is
       64px tall with bold text and a coloured icon sitting right on it, where the
       header has 160px and its text sits over smooth water. Checked by rendering
       the real button at 237px and 355px wide, at four crops and three wash
       strengths, before choosing.

       THE PICTURE CHANGED 2026-10-17: "Vapor Shop" (737x265) replaced "Vapor
       Appliance" (631x526), and the swap is why the watermark finally works. The
       button is about 3.7:1; the old picture was 1.20, a nearly square photo in a
       letterbox, so cover threw most of it away. The new one is 2.78, close to the
       button's own shape - it shows 186 of its 265 rows on a 3-column card, 70% of
       the picture, where the old one showed a sliver. The file also got smaller,
       69.5 KB to 51.6 KB. The old watermark is kept as
       _tools/service-bg_vapor-appliance.webp; going back is a copy, not a rebuild.

       43% is Kevin's own setting, dragged in _tools/service-bg-editor.html at a
       simulated 1366 and copied out of it. It puts the NEON WINGED A whole in
       frame on the right with the sunset bay door on the left and the machines
       across the middle. Note the direction: a HIGHER percentage shows a LOWER
       part of the picture. With this picture X IS DEAD at cover - 737x265 scaled
       to fill the button's width leaves no horizontal slack at all - so only the
       second number does anything until something zooms in. Re-run the editor
       rather than guessing at these.

       Neither source had a transparent pixel, but the file is still flattened onto
       the page purple when it is built, so none can ever punch through the card.
       TO REVERT: delete the four background-* lines. */
    background-image:
        linear-gradient(rgba(44, 7, 53, .80), rgba(44, 7, 53, .80)),
        url('../img/service-bg.webp');
    background-repeat: no-repeat, no-repeat;
    background-position: center, center 43%;
    background-size: auto, cover;

    /* NEVER WRAP, and the reason is arithmetic, not taste.
       This one line carries three things: the 32px icon plus its margin, the
       label, and the chevron plus its 10px margin - about 57px of furniture
       before a single letter. At 3-column desktop a card is 237px wide, which
       leaves roughly 148px of text space, and FOUR of the five labels needed
       more than that at 1rem. "Refrigerator Repair" was over by 5px and had
       been wrapping long before the labels were renamed.
       His standing rule is to lower the font size rather than let text wrap, so
       .8rem is set here: it fits the longest label ("Electric Dryer Repair",
       measured in the real face, not estimated) with about 10% to spare at the
       tightest column. ONE size for all five, so the kitchen row and the
       laundry row stay identical - a container-query size would have made the
       2-card laundry row larger than the 3-card kitchen row.
       If a longer label is ever added, LOWER this number, don't raise it:
       nowrap text that overshoots doesn't wrap, it pushes the page sideways. */
    white-space: nowrap;
    font-size: .8rem;
}

/* The icon sprite itself must never show. Not display:none - a few browsers
   refuse to resolve <use> into a hidden subtree - so it is simply given no
   size and taken out of the flow. */
.svg-sprite {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

/* Sized in REM, not em, on purpose. In em it tracked the summary's font-size -
   so when that dropped to .8rem to stop the labels wrapping, the icons would
   have shrunk 20% with it, and he likes them at this size ("i like these old
   ones"). rem pins them to the root instead, which nothing on this page
   scales, so they need no breakpoints either way.
   The negative vertical-align drops the icon onto the text baseline. */
.appliance-icon {
    /* 2.25rem, up from 2rem on 2026-10-15. MEASURED, because doubling these was
       tried once and failed: the summary label is nowrap, so every pixel the icon
       takes comes straight off the label's room.
       The binding case is the 3-across kitchen row, where a card is about 237px:
       take off 4px of gold border and 32px of padding and the row has 201px.
       The widest label there is "Electric Oven Repair" at roughly 129px, and the
       furniture around it - icon, its .4rem margin, the chevron and the chevron's
       10px margin - is about 60px. That leaves only ~12px of slack in the whole
       row, so 32 -> 36px spends a third of it and keeps the rest as margin for
       the fact that the label renders at weight 800 while that measurement was
       taken at 700. There is no room here for a big jump, which is exactly what
       the failed attempt found.
       vertical-align scales with it: -.62rem was tuned for 32px, so 2.25/2 of
       that is -.7rem, which keeps the icon sitting on the same optical line. */
    width: 2.25rem;
    height: 2.25rem;
    vertical-align: -.7rem;
    margin-right: .4rem;
}

.av-body {
    fill: var(--avocado);
}

.av-seam,
.av-door {
    fill: var(--avocado-dark);
}

.av-chrome {
    fill: var(--chrome);
}

/* oven window and the dishwasher's console — near-black, not pure, so they don't
   punch a hole in the card the way #000 would */
.av-glass {
    fill: #241d16;
}

.service-accordion>summary::-webkit-details-marker {
    display: none;
}

.service-accordion>summary::after {
    content: "▾";
    margin-left: 10px;
    opacity: .8;
}

.service-accordion[open]>summary::after {
    content: "▴";
}

/* open, the brand wall sits under the button, so the button's bottom corners
   have to go square or there is a notch where the two meet */
.service-accordion[open]>summary {
    border-radius: 14px 14px 0 0;
}

/* ONE LOGO PER ROW, 2026-10-15 (his ask: "keep every logo stacked... and none
   side by side on the stack"). Both axes change: .brands becomes a COLUMN of
   groups, and each .brand-group becomes a column of chips. flex-wrap goes with
   them - with one chip per row there is nothing left to wrap, and leaving it on
   would let a group silently break into two columns if a chip ever grew.
   The family spacing he asked for is the DIFFERENCE between the two gaps: 6px
   inside a parent company, 16px between them. */
.service-accordion .brands {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    padding: 16px;
    border-top: 1px solid var(--border);
}

/* brands sit in parent-company clusters: tight inside a group, wide between
   groups, so the family relationships read without needing labels */
.service-accordion .brand-group {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    /* the side margins are gone with the row: spacing between families is now
       the 16px gap on .brands above, and 0 here keeps a group from adding its
       own indent to a centred column */
    margin: 0;
}

/* Logos sit on white chips. The artwork is dark-on-white — knocking the
   white out would make JennAir, Whirlpool, Maytag and Frigidaire vanish
   against the purple, and a light chip is how brand walls are done anyway. */
/* Not links any more — outbound links to manufacturers pass value away from
   this site rather than to it, so the chips are plain. No hover effect
   either: hover feedback on something unclickable just misleads. */
.service-accordion .brand-chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 112px;
    height: 40px;
    /* 2px, not the 5px it was: with the overflow bug below fixed, every logo is
       held inside the content box for the first time, and 5px would have shrunk
       the ones that already looked right. At 2px they land on the same size they
       were drawing at before - checked logo by logo, from each file's aspect
       ratio - so this fixes the three that hung out without touching the nine
       that did not. */
    padding: 2px 9px;
    border-radius: 8px;
    background: #fff;
    border: 1px solid rgba(0, 0, 0, .12);
}

/* THE FIT BUG, found 2026-10-15 after Kevin said GE, LG and JennAir were still
   "hanging out of their bubble".
   The chip is a flex container, and its flex item is the <picture>, NOT the
   <img>. So `max-height: 100%` on the img was resolving against the picture -
   whose own height is auto - and a percentage against an auto height resolves to
   nothing at all. It constrained NOTHING. Width still worked, because flex
   shrank the item, which is why only the three TALLEST logos showed it: GE
   (square) wanted 92px of height in a 28px box, LG and JennAir about 45 and 50.
   The nine wordmarks are wide enough that width caught them first.
   Giving the picture a definite box - 100% of a chip that has a fixed height -
   makes that percentage mean what it looks like it means.
   WHY MY RENDERED MOCK-UP MISSED IT: the mock fitted each logo into the content
   box, which is what the CSS was SUPPOSED to do. It tested the arithmetic, not
   the browser. Only his eyes on the real page could catch this one. */
.service-accordion .brand-chip picture {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
}

/* TWO LOGOS THAT ARE NOT WORDMARKS, 2026-10-15. GE's mark is a circle and
   JennAir's is a stacked lockup, so in a chip shaped for a wordmark each sat as a
   small island in a field of white while its neighbours filled theirs edge to
   edge - Kevin: "those logos are not sized properly".
   The fix is the CHIP, not the file: both images are already cropped tight to
   their ink (measured - the opaque bounding box is the whole image), so there was
   nothing left to trim and nothing to gain from re-exporting them. Giving each a
   chip roughly its own shape brings the mark up to the same visual size as the
   lettering beside it. The padding comes down with the width for the same reason:
   9px of side padding on a 56px chip is a third of the chip. */
.service-accordion .brand-chip.chip-sq {
    width: 56px;
    padding: 2px 4px;
}

.service-accordion .brand-chip.chip-tall {
    width: 84px;
    padding: 2px 4px;
}

.service-accordion .brand-chip img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
}

/* ===========================================================================
   MOTION PREFERENCE — keep this LAST in the file so it can override anything
   above it.

   Some people get migraines, nausea or vertigo from movement on screen, and
   set "reduce motion" in Windows, macOS, iOS or Android. This honours that.

   It already earns its keep: the smooth scrolling at the top of this file,
   the button press transitions and the mobile menu slide are all motion.

   >> IF ANIMATION IS EVER ADDED (the scrolling brand wall in Services is the
   >> idea on the table), it must be switched off in here too. The blanket
   >> rules below catch any CSS animation or transition automatically, but a
   >> JavaScript-driven animation will NOT be caught — that one has to check
   >> window.matchMedia("(prefers-reduced-motion: reduce)").matches itself.
   =========================================================================== */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}
