:root{
  --ink:#272823;
  --muted:#6e6e64;
  --cream:#ffffff;
  --paper:#ffffff;
  --linen:#f4f4f2;
  --sage:#6e8150;
  --sage-dark:#53683a;
  --sage-soft:#dfe4d3;--orange:#ffa038;/* SA-28: measured off her logo file. A FILL that carries dark ink at 7.31, never type at 1.80. */
  --rust:#b87145;
  /* SA-62: her buttons are one treatment now, white on charcoal. The
     charcoal sits in the --ink family rather than a cold grey, so it
     belongs to her logo's own neutral. White on it is 9.9:1. */
  --charcoal:#3f403c;
  /* SA-81: set inline on <body> for a page that has a header picture.
     Declared here so the token is never undefined, because an undefined
     custom property invalidates the WHOLE declaration and nothing
     errors -- the SA-26 fault that made a filter chip ink on ink. */
  --page-hero-img:none;
  /* SA-85: the top bar. Set inline on <body> from her stored colour, and
     declared here for the same reason --page-hero-img is: an undefined custom
     property invalidates the whole declaration, silently.
     #5a5a5a is MEASURED off the word "smart" in her logo (61% of that
     wordmark's pixels are exactly it), which is NOT --charcoal below: that is
     #3f403c, darker and slightly warm. Her lockup sets "smart" in this grey
     and "art" in the green, so the two are different colours in the same file
     and only one of them is what she asked for here. */
  /* SA-87: the charcoal of the word "smart" in her logo, measured off the
     file (SA-85): 61% of that wordmark's pixels are exactly it. A TOKEN
     rather than reusing --topnote-bg, which is a BACKGROUND she can change:
     ink that follows a surface colour goes pale the day she picks a pale
     bar. The top bar merely DEFAULTS to the same value. */
  --smart-grey:#5a5a5a;
  --topnote-bg:#5a5a5a;
  /* SA-89: what the PHONE's own bars are painted (see base.html). The body
     ground behind the overscroll must match it or the bounce shows a stray
     band -- which is what "the footer is still green" was. */
  --browser-bar:#5a5a5a;
  --topnote-ink:#ffffff;
  --topnote-stars:#ffcb92;
  --charcoal-dark:#2b2c29;
  --line:rgba(39,40,35,.16);
  --card:#ffffff;
  --shadow:0 14px 40px rgba(39,40,35,.07);
  --radius:28px;
  --radius-sm:18px;
  --serif:Georgia,'Times New Roman',serif;
  --sans:Arial,Helvetica,sans-serif;
}
*{box-sizing:border-box}
/* SA-54: any class that sets `display` (.btn, .preview, …) beats the user
   agent's own [hidden] rule, so an element the script has hidden stays on
   screen. V1 has carried this line since the beginning; V2 never got it and
   instead patched two symptoms (.artpop[hidden], .roomstrip button[hidden],
   both in components.css and now redundant). The one it had NOT patched was
   the quote dialog, where "Back" and "Close" are `hidden` on the first step
   and both sat there beside Continue, offering to abandon the form before
   anybody had filled it in. Fix the cause, not the next symptom. */
[hidden]{display:none!important}
html{scroll-behavior:smooth}
body{margin:0;color:var(--ink);font-family:var(--sans);line-height:1.55;background:var(--cream)}
img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
button,input,textarea,select{font:inherit}
button{cursor:pointer}
.container{width:min(1220px,calc(100% - 40px));margin:auto}
.section{padding:68px 0}
.section-tight{padding:38px 0}
.paper-surface{background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow)}
.linen-surface{background:var(--linen);border:1px solid var(--line);box-shadow:var(--shadow)}
.eyebrow{font-size:.74rem;text-transform:uppercase;letter-spacing:.18em;font-weight:700;color:var(--sage-dark)}
h1,h2,h3,.serif{font-family:var(--serif);font-weight:400;line-height:1.02;margin:0}
h1{font-size:clamp(2.55rem,5vw,5rem)}
h2{font-size:clamp(2rem,3.7vw,3.45rem)}
h3{font-size:clamp(1.35rem,2vw,2rem)}
p{margin:0 0 1rem}
.muted{color:var(--muted)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.7rem;padding:14px 22px;border-radius:999px;border:1px solid var(--line);font-weight:700;font-size:.92rem;transition:.22s ease;background:#fff;color:var(--ink)}
.btn:hover{transform:translateY(-2px);box-shadow:0 10px 24px rgba(54,58,43,.12)}
.btn-primary{background:var(--charcoal);color:#fff;border-color:var(--charcoal)}
.btn-primary:hover{background:var(--charcoal-dark);border-color:var(--charcoal-dark)}
.btn-outline{background:#fff;border-color:rgba(63,64,60,.38)}
.btn-sm{padding:10px 16px;font-size:.82rem}
.pill{display:inline-flex;align-items:center;gap:.55rem;padding:8px 13px;border-radius:999px;background:var(--sage);color:#fff;font-size:.75rem;font-weight:700}
.icon-circle{width:46px;height:46px;border-radius:50%;display:grid;place-items:center;background:var(--sage-soft);color:var(--sage-dark);font-weight:800}

/* Header */
.site-header{position:sticky;top:0;z-index:60;background:rgba(255,255,255,.94);backdrop-filter:blur(14px);border-bottom:1px solid var(--line)}
.header-inner{height:88px;display:flex;align-items:center;justify-content:space-between;gap:26px}
.brand img{width:190px;height:auto}
.main-nav{display:flex;gap:24px;align-items:center;font-size:.86rem;font-weight:700}
.main-nav a{padding:10px 0;position:relative}
.main-nav a::after{content:"";position:absolute;left:0;right:100%;bottom:4px;height:1px;background:var(--sage);transition:.2s}
.main-nav a:hover::after,.main-nav a.active::after{right:0}
.header-actions{display:flex;gap:8px;align-items:center}
.icon-btn{width:42px;height:42px;border-radius:50%;border:0;background:transparent;display:grid;place-items:center;font-size:1.25rem}
.icon-btn:hover{background:rgba(110,129,80,.10)}
.menu-toggle{display:none}

/* Homepage hero */
.hero-wrap{padding:0 0 34px}
.hero{display:grid;min-height:clamp(460px,68vh,640px);border-radius:0;overflow:hidden;position:relative}
.hero>.hero-copy,.hero>.hero-image{grid-area:1/1}
.hero-copy{position:relative;z-index:2;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;color:#fff;padding-block:clamp(52px,7vw,92px);padding-right:clamp(24px,10vw,180px);padding-left:max(5vw,calc((100% - 1220px)/2));background:linear-gradient(90deg,rgba(24,25,22,.82) 0%,rgba(24,25,22,.70) 36%,rgba(24,25,22,.32) 62%,rgba(24,25,22,0) 88%)}
.hero-copy>*{position:relative;z-index:1}
.hero-copy h1{max-width:15ch;margin:18px 0 22px;color:#fff}
.hero-copy p{font-size:1.06rem;max-width:46ch;color:rgba(255,255,255,.92)}
.hero-actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:18px}
.hero-image{background:url('../images/hero-room.webp') center/cover;position:relative;min-height:clamp(460px,68vh,640px)}
.hero-badges{position:absolute;right:26px;top:92px;display:grid;gap:12px}
.hero-badge{width:150px;padding:16px;border-radius:18px;background:rgba(255,255,255,.90);border:1px solid rgba(255,255,255,.75);box-shadow:0 10px 30px rgba(0,0,0,.08);font-size:.76rem;font-weight:700}
.hero-badge span{display:block;font-size:1.3rem;margin-bottom:5px;color:var(--sage-dark)}
/* SA-86: the rule above has been here since the export was converted and had
   nothing to style -- it was written for a glyph, and the glyph was dropped
   (SA-22: a character missing from the page font falls back per platform).
   The mark is inline SVG now and takes its green from that same rule through
   `currentColor`, so the colour still lives in ONE place. `display:block`
   kills the inline baseline gap, which otherwise adds ~4px under the icon and
   makes the three cards uneven. Measured 6.06 on the card. */
.hero-badge span svg{display:block}
/* SA-94: her ask -- centred, and the mark at twice the size. The card had no
   `text-align`, so the words sat left under a left-aligned icon; centring the
   card centres both, and `margin-inline:auto` is what actually moves the SVG,
   because `display:block` takes it out of the text flow that `text-align`
   governs. The mark is 44px from 22, set on the macro call rather than scaled
   in CSS: an SVG asked for its real size draws crisply, where `width` on a
   block-level one is a resample of whatever it decided to be. */
.hero-badge{text-align:center}
.hero-badge span svg{margin-inline:auto}
/* Doubling the mark took each card from 99px to 121px and the stack from 321
   to 386, which pushed the third card 21px INTO the reel caption at the foot
   of the hero -- caught by the overlap check SA-80 added, and invisible to
   every other measurement: nothing overflowed, no contrast moved, no target
   shrank. The stack moves up and tightens rather than the cards shrinking,
   because the size is the thing she asked for. */
.hero-badges{top:56px;gap:8px}
/* And the mark's own bottom margin, which was tuned for a 22px glyph. At 1px
   of clearance the next slightly longer badge label collides again, so this
   buys real headroom rather than just clearing today's measurement. */
.hero-badge span{margin-bottom:2px}

.product-float{margin-top:-62px;position:relative;z-index:5}
.product-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:14px}
.product-card{background:#fff;border-radius:24px;padding:9px 9px 14px;border:1px solid var(--line);box-shadow:0 12px 28px rgba(70,63,48,.12);transition:.22s}
.product-card:hover{transform:translateY(-5px)}
.product-card img{height:170px;width:100%;object-fit:cover;border-radius:18px}
.product-card .meta{padding:12px 7px 0;display:flex;justify-content:space-between;gap:10px;align-items:end}
.product-card strong{display:block;font-size:.9rem}.product-card small{color:var(--muted);font-size:.72rem}
.arrow-dot{width:34px;height:34px;border:1px solid var(--line);border-radius:50%;display:grid;place-items:center;flex:0 0 auto}

.bespoke{display:grid;grid-template-columns:1fr 1fr 1fr;gap:24px;padding:38px;border-radius:30px;align-items:center}
.bespoke-copy h2{font-size:2.55rem;margin:8px 0 16px}
.sample-art{height:230px;display:grid;place-items:center}
.sample-art img{max-height:220px;filter:drop-shadow(0 14px 20px rgba(0,0,0,.10))}
.feature-list{background:rgba(255,255,255,.70);border-radius:22px;padding:18px 22px;border:1px solid var(--line)}
.feature-row{display:grid;grid-template-columns:38px 1fr;gap:12px;padding:14px 0;border-bottom:1px solid var(--line)}
.feature-row:last-child{border-bottom:0}.feature-row b{font-size:.85rem}.feature-row small{display:block;color:var(--muted)}

.section-heading{display:flex;align-items:end;justify-content:space-between;gap:20px;margin-bottom:26px}
.section-heading.center{text-align:center;display:block}
.section-heading p{max-width:660px;color:var(--muted);margin-top:10px}
.style-shell{padding:34px;border-radius:30px}
.style-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:14px}
.style-card{position:relative;min-width:0}
.style-card img{aspect-ratio:1/1.12;width:100%;object-fit:cover;border-radius:22px;border:1px solid rgba(50,50,40,.10);box-shadow:0 7px 17px rgba(52,48,40,.08)}
.style-card strong{display:block;text-align:center;font-size:.78rem;margin-top:8px}
.style-card:hover img{transform:translateY(-3px);transition:.2s}
.style-cta{text-align:center;margin-top:22px}

.projects-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:14px}
.project-card{position:relative;border-radius:22px;overflow:hidden;min-height:210px;background:#ddd;box-shadow:0 8px 18px rgba(55,48,40,.10)}
.project-card img{width:100%;height:100%;object-fit:cover;position:absolute;inset:0}
.project-card::after{content:"";position:absolute;inset:30% 0 0;background:linear-gradient(transparent,rgba(20,21,18,.82))}
.project-card .caption{position:absolute;z-index:2;left:16px;right:16px;bottom:14px;color:#fff}
.project-card .caption b{display:block}.project-card .caption span{font-size:.75rem;opacity:.88}

.cta-band{border-radius:28px;padding:36px 42px;background:linear-gradient(90deg,rgba(96,112,69,.98),rgba(127,139,92,.92));color:#fff;display:grid;grid-template-columns:1.2fr 1fr auto;gap:30px;align-items:center;overflow:hidden}
.cta-band h2{font-size:2.35rem}.cta-band p{margin:0;color:rgba(255,255,255,.85)}
.cta-band .btn{background:rgba(255,255,255,.92);color:var(--charcoal)}

.newsletter-inline{margin-top:26px;border-radius:26px;padding:28px 34px;display:grid;grid-template-columns:1.1fr 1fr;gap:30px;align-items:center}
.newsletter-inline .copy{display:flex;gap:16px;align-items:center}.newsletter-inline h3{font-size:1.9rem}.newsletter-inline p{margin:5px 0 0;color:var(--muted);font-size:.9rem}
.inline-form{display:flex;gap:10px}/* SA-63: SCOPED to an input. `.field` is her export's newsletter pill,
   and the contact and checkout forms use the same class name for a
   field GROUP -- a <div> holding a label and an input. Unscoped, every
   one of those 13 groups drew a pill AND the input inside it drew its
   own box, so each field was two nested rounded outlines. The quote
   dialog already resets this by hand (SA-54); this fixes the cause. */
input.field{width:100%;border:1px solid var(--line);background:rgba(255,255,255,.76);border-radius:999px;padding:14px 18px;outline:none}input.field:focus{border-color:var(--sage)}

/* Newsletter modal */
.modal-backdrop{position:fixed;inset:0;background:rgba(34,34,30,.58);z-index:100;display:none;align-items:center;justify-content:center;padding:24px;backdrop-filter:blur(2px)}
.modal-backdrop.show{display:flex}
.newsletter-modal{width:min(650px,100%);border-radius:30px;position:relative;overflow:hidden;padding:46px;background:var(--paper);box-shadow:0 28px 100px rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.65)}
.newsletter-modal>*{position:relative;z-index:2}.newsletter-modal h2{font-size:2.75rem;max-width:420px}.newsletter-modal p{max-width:390px;margin:18px 0 22px}.modal-close{position:absolute;right:18px;top:18px;z-index:4;width:40px;height:40px;border-radius:50%;border:1px solid var(--line);background:rgba(255,255,255,.9);font-size:1.3rem}
.modal-form{display:flex;gap:9px;max-width:450px}.privacy-note{font-size:.72rem;color:var(--muted);margin-top:14px}

/* Footer */
.site-footer{padding:46px 0 22px;margin-top:54px;border-top:1px solid var(--line);background:var(--linen)}
.footer-grid{display:grid;grid-template-columns:1.25fr repeat(3,.8fr) 1.15fr;gap:34px}.footer-logo{width:170px;margin-bottom:16px}.footer-grid h4{font-size:.75rem;text-transform:uppercase;letter-spacing:.13em;margin:0 0 14px}.footer-grid a,.footer-grid p{font-size:.8rem;color:var(--muted);display:block;margin:0 0 8px}.socials{display:flex;gap:8px;margin-top:14px}.socials a,.socials span{width:32px;height:32px;border:1px solid var(--line);border-radius:50%;display:grid;place-items:center;color:var(--ink);font-size:.7rem}.footer-bottom{border-top:1px solid var(--line);margin-top:28px;padding-top:18px;display:flex;justify-content:space-between;gap:20px;font-size:.74rem;color:var(--muted)}

/* Interior pages */
.page-hero{padding:56px 0 30px}.page-hero .box{position:relative}.page-hero .box>*{position:relative;z-index:1}.page-hero h1{max-width:760px}.page-hero p{max-width:650px;margin-top:18px;font-size:1.05rem}
.category-chips{display:flex;flex-wrap:wrap;gap:9px;margin:26px 0 34px}.chip{border:1px solid var(--line);border-radius:999px;background:rgba(255,255,255,.55);padding:10px 14px;font-size:.76rem;font-weight:700}.chip:hover{background:var(--sage);color:#fff}
.collection-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.collection-card{border-radius:24px;overflow:hidden;background:rgba(255,255,255,.72);border:1px solid var(--line);box-shadow:0 10px 24px rgba(60,55,43,.08)}.collection-card img{width:100%;aspect-ratio:1.35/1;object-fit:cover}.collection-card .pad{padding:16px}.collection-card h3{font-size:1.4rem;margin-bottom:6px}.collection-card p{font-size:.83rem;color:var(--muted);margin:0}.content-panel{border-radius:30px;padding:38px}.two-col{display:grid;grid-template-columns:1fr 1fr;gap:30px}.three-col{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}.info-card{border-radius:24px;padding:26px;background:rgba(255,255,255,.68);border:1px solid var(--line)}.info-card h3{margin:10px 0}.info-card p{color:var(--muted);font-size:.9rem}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:15px}.form-grid .full{grid-column:1/-1}.form-input{width:100%;border:1px solid var(--line);border-radius:17px;background:rgba(255,255,255,.78);padding:14px 15px;outline:none}.form-input:focus{border-color:var(--sage)}textarea.form-input{min-height:140px;resize:vertical}.form-status{margin-top:10px;font-size:.85rem;color:var(--sage-dark)}

/* Visualizer */
.visualizer-main{padding:30px 0 68px}.steps{display:flex;justify-content:center;align-items:center;gap:38px;margin:10px 0 26px}.step{display:flex;align-items:center;gap:10px;min-width:170px}.step-no{width:44px;height:44px;border-radius:50%;background:var(--sage);color:#fff;display:grid;place-items:center;font-weight:800;font-size:1.05rem}.step .text b{display:block;font-size:.86rem}.step .text span{font-size:.8rem;color:var(--muted)}.step.active .text span{color:var(--sage-dark);font-weight:700}.step-line{width:80px;height:1px;background:rgba(110,129,80,.45)}
.visualizer-panel{border-radius:30px;padding:24px}.preview-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px}.preview-box h4{font-size:.72rem;text-transform:uppercase;letter-spacing:.22em;margin:0 0 12px}.preview-img{height:330px;border-radius:22px;overflow:hidden;position:relative;background:#ddd;border:1px solid var(--line)}.preview-img img,.preview-img canvas{width:100%;height:100%;object-fit:cover;display:block}.preview-meta{padding:12px 0 0}.preview-meta b{display:block;font-size:1.05rem}.preview-meta small{color:var(--muted)}.upload-control{position:absolute;left:12px;bottom:12px}.upload-control input{display:none}
.wallpaper-options{display:flex;gap:8px;margin-top:10px}.wallpaper-dot{width:34px;height:34px;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px var(--line);overflow:hidden;padding:0}.wallpaper-dot img{width:100%;height:100%;object-fit:cover}.wallpaper-dot.selected{box-shadow:0 0 0 2px var(--sage)}
.generate-panel{margin-top:18px;border-radius:28px;padding:30px;display:grid;grid-template-columns:1.2fr .8fr;gap:26px;align-items:center}.generation-row{display:grid;grid-template-columns:76px 1fr 110px;gap:18px;align-items:center}.progress-ring{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;background:conic-gradient(var(--sage) calc(var(--progress,0)*1%),#ded9cc 0);position:relative;font-weight:800}.progress-ring::after{content:"";position:absolute;inset:8px;background:#f7f2e8;border-radius:50%}.progress-ring span{position:relative;z-index:1}.gen-copy h1,.gen-copy h2{font-size:2.45rem;margin-bottom:10px}.progress-line{height:12px;border-radius:999px;background:#ded9cc;overflow:hidden;margin-top:20px}.progress-line span{display:block;height:100%;width:var(--progress,0%);background:var(--sage);border-radius:999px;transition:.35s}.time-note{font-size:.78rem;color:var(--muted)}.tip-box{border-radius:22px;background:rgba(255,255,255,.64);border:1px solid var(--line);padding:22px}.tip-box b{display:block;margin-bottom:8px}
.wait-heading{text-align:center;font-family:var(--serif);font-size:2rem;margin:34px 0 18px}.wait-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.wait-card{border-radius:22px;padding:22px;background:rgba(255,255,255,.72);border:1px solid var(--line);min-height:185px;display:flex;flex-direction:column}.wait-card h4{margin:12px 0 6px}.wait-card p{font-size:.82rem;color:var(--muted);flex:1}.wait-card a{font-size:.82rem;font-weight:700;color:var(--sage-dark)}
.visual-collections{margin-top:36px}.visual-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:12px}.visual-card{height:122px;border-radius:18px;overflow:hidden;position:relative}.visual-card img{width:100%;height:100%;object-fit:cover}.visual-card::after{content:"";position:absolute;inset:50% 0 0;background:linear-gradient(transparent,rgba(20,20,17,.75))}.visual-card span{position:absolute;z-index:2;left:10px;bottom:8px;color:white;font-weight:700;font-size:.75rem}
.result-panel{display:none;margin-top:20px;border-radius:30px;padding:30px}.result-panel.show{display:block}.result-grid{display:grid;grid-template-columns:1.3fr .7fr;gap:28px}.result-image{border-radius:24px;overflow:hidden;min-height:430px;background:#ddd}.result-image canvas{width:100%;height:auto;display:block}.result-actions{display:flex;flex-direction:column;justify-content:center}.result-actions h2{font-size:2.8rem;margin:10px 0 16px}.result-actions .btn{margin:5px 0;width:100%}

/* Responsive */
@media(max-width:1050px){.main-nav{gap:14px}.header-actions{display:none}.hero{grid-template-columns:1fr}.hero-copy{padding:58px 38px}.hero-image{min-height:430px}.product-grid{grid-template-columns:repeat(3,1fr)}.product-card:nth-child(n+4){display:none}.bespoke{grid-template-columns:1fr 1fr}.feature-list{grid-column:1/-1}.style-grid{grid-template-columns:repeat(4,1fr)}.projects-grid{grid-template-columns:repeat(3,1fr)}.project-card:nth-child(n+4){display:none}.cta-band{grid-template-columns:1fr auto}.cta-band p{display:none}.footer-grid{grid-template-columns:1.4fr repeat(2,1fr)}.footer-grid>div:nth-child(4){display:none}.preview-grid{grid-template-columns:1fr}.wait-grid{grid-template-columns:repeat(2,1fr)}.visual-grid{grid-template-columns:repeat(4,1fr)}.collection-grid{grid-template-columns:repeat(3,1fr)}}
@media(max-width:780px){.container{width:min(100% - 24px,1220px)}.header-inner{height:74px}.brand img{width:155px}.menu-toggle{display:grid}.main-nav{position:absolute;left:12px;right:12px;top:76px;background:var(--cream);border:1px solid var(--line);border-radius:22px;box-shadow:var(--shadow);padding:16px;display:none;flex-direction:column;align-items:stretch}.main-nav.open{display:flex}.main-nav a{padding:11px 12px}.hero-copy{padding:46px 24px}.hero{border-radius:0 0 24px 24px}.hero-image{min-height:350px}.hero-badges{right:14px;top:45px}.hero-badge{width:125px;padding:12px}.product-float{margin-top:-42px}.product-grid{grid-template-columns:repeat(2,1fr)}.product-card:nth-child(3){display:none}.product-card img{height:145px}.bespoke{grid-template-columns:1fr;padding:26px}.sample-art{height:180px}.style-shell{padding:24px 18px}.style-grid{grid-template-columns:repeat(2,1fr)}.style-card:nth-child(n+7){display:none}.projects-grid{grid-template-columns:1fr 1fr}.project-card:nth-child(3){display:none}.cta-band{grid-template-columns:1fr;padding:28px}.cta-band .btn{justify-self:start}.newsletter-inline{grid-template-columns:1fr;padding:24px}.inline-form,.modal-form{flex-direction:column}.newsletter-modal{padding:36px 24px;background:var(--paper)}.footer-grid{grid-template-columns:1fr 1fr}.footer-grid>div:first-child{grid-column:1/-1}.footer-bottom{flex-direction:column}.page-hero .box{padding:36px 24px}.collection-grid{grid-template-columns:1fr 1fr}.two-col,.three-col{grid-template-columns:1fr}.form-grid{grid-template-columns:1fr}.steps{gap:8px;justify-content:flex-start;overflow-x:auto}.step{min-width:145px}.step-line{width:24px}.visualizer-panel{padding:16px}.preview-img{height:270px}.generate-panel{grid-template-columns:1fr;padding:22px}.generation-row{grid-template-columns:64px 1fr}.time-note{grid-column:2}.gen-copy h1,.gen-copy h2{font-size:2rem}.wait-grid{grid-template-columns:1fr 1fr}.visual-grid{grid-template-columns:repeat(2,1fr)}.result-grid{grid-template-columns:1fr}.result-image{min-height:280px}.result-actions h2{font-size:2.3rem}}
@media(max-width:500px){h1{font-size:2.65rem}.hero-actions{flex-direction:column;align-items:stretch}.product-card img{height:125px}.style-grid,.projects-grid,.collection-grid,.wait-grid{grid-template-columns:1fr}.product-grid{grid-template-columns:1fr}.product-card:nth-child(2){display:none}.style-card:nth-child(n+5){display:none}.project-card:nth-child(2){display:none}.footer-grid{grid-template-columns:1fr}.newsletter-modal h2{font-size:2.25rem}.preview-img{height:230px}.generation-row{grid-template-columns:1fr}.progress-ring{margin:auto}.time-note{grid-column:1;text-align:center}.visual-grid{grid-template-columns:1fr 1fr}}

.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}


/* Standalone complete-site additions */
:focus-visible{outline:3px solid rgba(110,129,80,.42);outline-offset:3px}
.top-note{background:var(--topnote-bg);color:var(--topnote-ink);font-size:.74rem}.top-note .container{display:flex;justify-content:space-between;gap:20px;padding:7px 0}.top-note a{opacity:.95}
.nav-dropdown{position:relative}.nav-dropdown>button{font:inherit;font-weight:700;background:transparent;border:0;padding:10px 0;color:inherit}.nav-dropdown-panel{position:absolute;top:42px;left:-18px;width:260px;padding:14px;background:#faf7ef;border:1px solid var(--line);border-radius:18px;box-shadow:var(--shadow);display:none}.nav-dropdown:hover .nav-dropdown-panel,.nav-dropdown:focus-within .nav-dropdown-panel{display:grid}.nav-dropdown-panel a{padding:9px 10px;border-radius:11px}.nav-dropdown-panel a:hover{background:rgba(110,129,80,.09)}
.breadcrumbs{font-size:.78rem;color:var(--muted);margin-bottom:18px}.breadcrumbs a:hover{color:var(--sage-dark)}
.hero-note{display:flex;gap:14px;flex-wrap:wrap;margin-top:24px}.mini-stat{background:rgba(255,255,255,.66);border:1px solid var(--line);border-radius:18px;padding:13px 16px;min-width:160px}.mini-stat b{display:block;color:var(--sage-dark)}.mini-stat span{font-size:.76rem;color:var(--muted)}
.split-feature{display:grid;grid-template-columns:1fr 1fr;gap:24px;align-items:stretch}.feature-image{min-height:420px;border-radius:28px;overflow:hidden}.feature-image img{width:100%;height:100%;object-fit:cover}.feature-copy{padding:44px;border-radius:28px;display:flex;flex-direction:column;justify-content:center}.feature-copy h2{margin:10px 0 18px}.feature-copy p{color:var(--muted)}
.list-clean{list-style:none;padding:0;margin:18px 0}.list-clean li{padding:9px 0 9px 28px;position:relative;border-bottom:1px solid var(--line)}.list-clean li::before{content:'✓';position:absolute;left:0;color:var(--sage-dark);font-weight:800}
.card-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}
.gallery-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.gallery-card{border-radius:22px;overflow:hidden;min-height:270px;position:relative}.gallery-card img{width:100%;height:100%;object-fit:cover;position:absolute}.gallery-card:nth-child(3n+1){grid-row:span 2;min-height:554px}.gallery-card .overlay{position:absolute;inset:auto 0 0;padding:18px;color:white;background:linear-gradient(transparent,rgba(20,20,17,.82));padding-top:70px}.gallery-card .overlay span{display:block;font-size:.75rem;opacity:.86}
.shop-toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0 0 24px}.filter-select{border:1px solid var(--line);background:rgba(255,255,255,.72);border-radius:999px;padding:11px 16px}.product-shop-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.shop-card{border:1px solid var(--line);background:rgba(255,255,255,.70);border-radius:24px;overflow:hidden;box-shadow:0 10px 24px rgba(60,55,43,.07)}.shop-card img{aspect-ratio:1/1;width:100%;object-fit:cover}.shop-card .pad{padding:16px}.shop-card .tag{font-size:.68rem;text-transform:uppercase;letter-spacing:.12em;color:var(--sage-dark);font-weight:800}.shop-card h3{font-size:1.35rem;margin:8px 0}.shop-card p{font-size:.84rem;color:var(--muted)}
.artist-hero{display:grid;grid-template-columns:1fr .75fr;gap:24px}.artist-profile{border-radius:30px;padding:42px}.artist-profile h1{margin:8px 0 18px}.artist-profile p{max-width:680px}.artist-art{border-radius:30px;overflow:hidden;min-height:430px}.artist-art img{width:100%;height:100%;object-fit:cover}
.contact-layout{display:grid;grid-template-columns:.8fr 1.2fr;gap:24px}.contact-panel{border-radius:30px;padding:34px}.contact-method{display:flex;gap:14px;align-items:flex-start;padding:16px 0;border-bottom:1px solid var(--line)}.contact-method:last-child{border:0}.contact-method b{display:block}.contact-method span{font-size:.82rem;color:var(--muted)}
.notice{border-left:4px solid var(--sage);background:rgba(110,129,80,.08);padding:18px 20px;border-radius:0 16px 16px 0;margin:20px 0;font-size:.9rem}
.accordion{display:grid;gap:12px}.accordion details{border:1px solid var(--line);border-radius:20px;background:rgba(255,255,255,.62);padding:16px 20px}.accordion summary{cursor:pointer;font-weight:800}.accordion details p{margin:12px 0 0;color:var(--muted)}
.article{max-width:850px}.article h2{font-size:2.1rem;margin:34px 0 14px}.article h3{font-size:1.45rem;margin:26px 0 10px}.article p,.article li{color:#4e4f48}.article a{text-decoration:underline;text-decoration-color:rgba(110,129,80,.45)}
.category-cloud{display:grid;grid-template-columns:repeat(5,1fr);gap:12px}.category-tile{min-height:110px;border-radius:20px;padding:18px;background:var(--paper);border:1px solid var(--line);display:flex;align-items:center;justify-content:center;text-align:center;font-family:var(--serif);font-size:1.15rem}.category-tile:hover{background:var(--sage);color:white}
.visualizer-start{border-radius:28px;padding:28px;margin:24px 0}.visualizer-start .btn{margin-top:8px}.generate-start{display:flex;justify-content:flex-start;margin-top:18px}
.static-form-note{font-size:.76rem;color:var(--muted);margin-top:10px}
.form-success{padding:12px 14px;background:rgba(110,129,80,.10);border-radius:14px;color:var(--sage-dark);display:none}.form-success.show{display:block}
.site-map-list{columns:3;column-gap:30px}.site-map-list a{display:block;padding:6px 0}

@media(max-width:1050px){.nav-dropdown{display:none}.gallery-grid{grid-template-columns:repeat(3,1fr)}.product-shop-grid{grid-template-columns:repeat(3,1fr)}.category-cloud{grid-template-columns:repeat(4,1fr)}.artist-hero,.contact-layout{grid-template-columns:1fr}.artist-art{min-height:350px}}
@media(max-width:780px){.top-note{display:none}.split-feature{grid-template-columns:1fr}.gallery-grid{grid-template-columns:1fr 1fr}.gallery-card:nth-child(3n+1){grid-row:auto;min-height:270px}.product-shop-grid{grid-template-columns:1fr 1fr}.category-cloud{grid-template-columns:repeat(2,1fr)}.feature-copy{padding:28px}.site-map-list{columns:2}}
@media(max-width:500px){.gallery-grid,.product-shop-grid,.category-cloud{grid-template-columns:1fr}.shop-toolbar{align-items:stretch;flex-direction:column}.site-map-list{columns:1}}


/* v3 review corrections ------------------------------------------------ */
img{image-rendering:auto}
.footer-social{display:flex;align-items:center;gap:10px;margin-top:18px;flex-wrap:wrap}
.footer-social a{width:38px!important;height:38px!important;display:grid!important;place-items:center!important;border:1px solid rgba(52,55,44,.20)!important;border-radius:999px!important;color:var(--ink)!important;margin:0!important;background:rgba(255,255,255,.36);transition:.2s ease}
.footer-social a:hover{transform:translateY(-2px);background:rgba(110,129,80,.12);border-color:rgba(83,104,58,.35)!important}
.footer-social svg{width:18px;height:18px;display:block}
.footer-bottom{position:relative;align-items:center;display:grid;grid-template-columns:1fr auto 1fr}
.footer-bottom>span:last-child{text-align:right}
.hosted-by{font-size:.68rem;letter-spacing:.02em;opacity:.72;text-align:center;font-style:italic}

.featured-projects-section{padding-top:42px!important;padding-bottom:42px!important;overflow:hidden}
.featured-heading{align-items:flex-end!important;margin-bottom:22px!important}
.featured-heading h2{font-size:clamp(2rem,3.2vw,3.1rem);margin-top:5px}
.home-projects{grid-template-columns:repeat(4,minmax(0,1fr))!important;gap:18px!important}
.home-projects .project-card{min-height:0!important;aspect-ratio:1.18/1;border-radius:24px}
.home-projects .project-card img{position:absolute!important;inset:0!important;width:100%!important;height:100%!important;object-fit:cover!important}
.home-projects .project-card:nth-child(1) img{object-position:center 52%}
.home-projects .project-card:nth-child(2) img{object-position:center 50%}
.home-projects .project-card:nth-child(3) img{object-position:center 48%}
.home-projects .project-card:nth-child(4) img{object-position:center 44%}
.home-projects .project-card::after{inset:42% 0 0;background:linear-gradient(transparent,rgba(17,18,15,.76))}
.home-projects .caption{left:20px;bottom:18px}.home-projects .caption b{font-family:var(--serif);font-size:1.18rem;font-weight:400}.home-projects .caption span{font-size:.78rem;margin-top:3px;display:block}

.product-card img{height:auto!important;aspect-ratio:4/3;object-fit:cover;object-position:center}
.shop-card img{aspect-ratio:4/3!important;object-fit:cover;object-position:center}
.collection-card img{object-position:center}
.feature-image img,.gallery-card img{object-position:center}
.style-card img{object-position:center}

.category-hero-box{display:grid!important;grid-template-columns:1.15fr .85fr;gap:30px;align-items:center;overflow:hidden}
.category-hero-box>img{width:100%;height:300px;object-fit:cover;border-radius:24px;border:1px solid var(--line)}
.related-categories{display:flex;gap:10px;flex-wrap:wrap}.related-categories a{padding:11px 16px;border:1px solid var(--line);border-radius:999px;background:rgba(255,255,255,.55);font-size:.82rem}.related-categories a:hover{background:var(--sage);color:#fff}

.floating-whatsapp{position:fixed;right:22px;bottom:22px;z-index:95;display:inline-flex;align-items:center;gap:9px;background:#25D366;color:white;border-radius:999px;padding:12px 17px 12px 13px;box-shadow:0 14px 36px rgba(26,93,55,.28);font-weight:800;font-size:.84rem;border:2px solid rgba(255,255,255,.82);transition:transform .2s ease,box-shadow .2s ease}
.floating-whatsapp:hover{transform:translateY(-3px);box-shadow:0 18px 44px rgba(26,93,55,.36)}
.floating-whatsapp svg{width:25px;height:25px;display:block;flex:0 0 auto}

@media(max-width:1050px){.home-projects{grid-template-columns:repeat(2,minmax(0,1fr))!important}.project-card:nth-child(n+4){display:block!important}.category-hero-box{grid-template-columns:1fr}.category-hero-box>img{height:260px}}
@media(max-width:780px){.footer-bottom{grid-template-columns:1fr;text-align:center;gap:8px}.footer-bottom>span:last-child{text-align:center}.home-projects{grid-template-columns:1fr 1fr!important}.home-projects .project-card:nth-child(3),.home-projects .project-card:nth-child(4){display:block!important}.floating-whatsapp span{display:none}.floating-whatsapp{width:54px;height:54px;padding:0;justify-content:center;right:15px;bottom:15px}.floating-whatsapp svg{width:29px;height:29px}}
@media(max-width:520px){.home-projects{grid-template-columns:1fr!important}.home-projects .project-card{aspect-ratio:1.35/1}.footer-social{justify-content:flex-start}.category-hero-box>img{height:220px}}


/* v4 standalone shop -------------------------------------------------- */
.header-actions .icon-btn svg{width:22px;height:22px;display:block}.cart-icon-btn{position:relative}.cart-count{position:absolute;right:1px;top:1px;min-width:18px;height:18px;padding:0 4px;border-radius:999px;background:var(--sage-dark);color:#fff;font:700 10px/18px var(--sans);text-align:center}
.shop-hero{padding:38px 0 18px}.shop-hero-card{border-radius:32px;padding:34px;display:grid;grid-template-columns:1.05fr .95fr;gap:34px;align-items:center;overflow:hidden}.shop-hero-card h1{margin:9px 0 18px}.shop-hero-card>img{width:100%;height:340px;object-fit:cover;object-position:center;border-radius:26px}
.shop-controls{border-radius:24px;padding:18px 20px;display:flex;gap:20px;justify-content:space-between;align-items:end;margin-bottom:26px}.shop-search{flex:1;max-width:580px}.shop-search label{display:block;font-size:.76rem;text-transform:uppercase;letter-spacing:.12em;font-weight:800;margin-bottom:7px}.shop-filter-buttons{display:flex;gap:8px;flex-wrap:wrap}.shop-filter{border:1px solid var(--line);background:rgba(255,255,255,.6);padding:10px 15px;border-radius:999px;font-weight:700;color:var(--ink)}.shop-filter.active,.shop-filter:hover{background:var(--sage);border-color:var(--sage);color:#fff}.shop-result-head{display:flex;justify-content:space-between;align-items:end;gap:20px;margin-bottom:22px}.shop-result-head h2{font-size:2.6rem;margin-top:5px}
.catalog-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px}.catalog-card{border:1px solid var(--line);background:rgba(255,255,255,.70);border-radius:24px;overflow:hidden;box-shadow:0 10px 24px rgba(60,55,43,.07);display:flex;flex-direction:column}.catalog-image{display:block;overflow:hidden}.catalog-image img{width:100%;aspect-ratio:4/3;object-fit:cover;object-position:center;transition:transform .35s ease}.catalog-card:hover .catalog-image img{transform:scale(1.025)}.catalog-body{padding:17px;display:flex;flex-direction:column;flex:1}.catalog-topline{display:flex;justify-content:space-between;gap:10px;align-items:center}.catalog-card .tag{font-size:.68rem;text-transform:uppercase;letter-spacing:.12em;color:var(--sage-dark);font-weight:800}.quote-only{font-size:.68rem;padding:5px 8px;border-radius:999px;background:rgba(110,129,80,.11);color:var(--sage-dark);font-weight:800}.catalog-card h3{font-size:1.35rem;margin:10px 0 8px}.catalog-card p{font-size:.83rem;color:var(--muted);flex:1}.catalog-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:12px}.text-link{font-size:.78rem;color:var(--sage-dark);font-weight:800}.product-price{font-size:.82rem}.was-price{text-decoration:line-through;color:var(--muted);font-weight:400;margin-right:4px}.shop-note{border-radius:28px;margin-top:34px;padding:32px;display:flex;align-items:center;justify-content:space-between;gap:28px}.shop-note h2{font-size:2.15rem;margin:7px 0 10px}.shop-note p{max-width:680px;color:var(--muted);margin:0}
.product-detail-section{padding-top:42px}.product-detail{display:grid;grid-template-columns:1.08fr .92fr;gap:24px}.product-gallery,.product-info{border-radius:30px;padding:28px}.product-gallery img{width:100%;aspect-ratio:4/3;object-fit:cover;object-position:center;border-radius:23px}.product-thumb-note{font-size:.76rem;color:var(--muted);padding:12px 4px 0}.product-info{display:flex;flex-direction:column;justify-content:center}.product-info h1{font-size:clamp(2.5rem,4.2vw,4.5rem);margin:10px 0 15px}.enquiry-badge{align-self:flex-start;padding:8px 12px;border-radius:999px;background:var(--sage-soft);color:var(--sage-dark);font-size:.78rem;font-weight:800;margin-bottom:17px}.detail-price{font-size:1.1rem;margin:2px 0 19px}.detail-price strong{font-size:1.65rem;color:var(--sage-dark)}.small-note{font-size:.75rem;color:var(--muted);margin-top:14px}.qty-row{margin:12px 0}.qty-row label{font-size:.8rem;font-weight:800}.qty-input{width:80px;margin-left:10px;padding:10px;border:1px solid var(--line);border-radius:14px;background:#fff}
.cart-layout,.checkout-layout{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(300px,.65fr);gap:24px;align-items:start}.cart-panel,.cart-summary,.checkout-form,.checkout-summary{border-radius:28px;padding:28px}.cart-row{display:grid;grid-template-columns:100px 1fr auto;gap:18px;align-items:center;padding:16px 0;border-bottom:1px solid var(--line)}.cart-row img{width:100px;height:82px;object-fit:cover;border-radius:16px}.cart-row h3{font-size:1.25rem;margin-bottom:5px}.cart-row-meta{font-size:.8rem;color:var(--muted)}.cart-row-actions{display:flex;align-items:center;gap:10px}.cart-qty{width:64px;padding:8px;border:1px solid var(--line);border-radius:12px;background:#fff}.remove-cart{border:0;background:transparent;color:#8a5540;font-size:.78rem;text-decoration:underline}.cart-empty{display:none;text-align:center;padding:56px 20px}.cart-empty.show{display:block}.cart-summary,.checkout-summary{position:sticky;top:112px}.cart-summary h2,.checkout-summary h2{font-size:2.1rem;margin:8px 0 18px}.summary-line{display:flex;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid var(--line)}.summary-line.total{font-size:1.12rem;border-bottom:0}.cart-checkout-btn{width:100%;margin:16px 0 12px}.checkout-form h2{font-size:2.2rem;margin-bottom:20px}.checkout-item{display:grid;grid-template-columns:56px 1fr auto;gap:10px;align-items:center;padding:10px 0;border-bottom:1px solid var(--line)}.checkout-item img{width:56px;height:48px;object-fit:cover;border-radius:10px}.checkout-item b{font-size:.78rem}.checkout-item span{font-size:.72rem;color:var(--muted)}
@media(max-width:1050px){.catalog-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.shop-hero-card,.product-detail{grid-template-columns:1fr}.shop-hero-card>img{height:300px}.cart-layout,.checkout-layout{grid-template-columns:1fr}.cart-summary,.checkout-summary{position:static}.main-nav{gap:18px}}
@media(max-width:780px){.catalog-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.shop-controls,.shop-note{align-items:stretch;flex-direction:column}.shop-result-head{align-items:flex-start;flex-direction:column}.product-gallery,.product-info{padding:20px}.cart-row{grid-template-columns:74px 1fr}.cart-row img{width:74px;height:68px}.cart-row-actions{grid-column:2}.shop-hero-card{padding:24px}.shop-hero-card>img{height:240px}}
@media(max-width:500px){.shop-filter-buttons{display:grid;grid-template-columns:1fr 1fr}.catalog-card h3{font-size:1.5rem}.shop-result-head h2{font-size:2.15rem}}

/* Footer consultation button contrast fix */
.footer-grid a.btn.btn-primary{color:#fff;}
.footer-grid a.btn.btn-primary:hover{color:#fff;}

/* ── SA-22: the phone menu is a FULL-SCREEN sheet ────────────────────────────
 * The export shipped a dropdown panel pinned under the header (absolute, 22px
 * radius, page showing all round it). Asked for fullscreen, and it is the
 * treatment V1 (SA-19), Marble House (MH-1) and the PBC site (PBC-1) all use:
 * the sheet covers the masthead, so it has to carry its own logo and a visible
 * way out or the browser's back button is the only reliable exit.
 */
.nav-head, .nav-foot { display: none; }

@media (max-width: 1150px) {
  /* LOAD BEARING. `.site-header` sets `backdrop-filter: blur(14px)`, and a
     backdrop-filter makes an element the CONTAINING BLOCK for its
     position:fixed descendants. The sheet lives inside the header, so its
     `top: 0` resolved to the header's top rather than the viewport's and it
     opened 99px down, leaving the banners above it -- while still measuring
     100dvh tall, so "does it cover the viewport" answered yes and the bug
     survived the first check. The blur is decorative and a phone header is
     opaque anyway. Same family as the SA-7 note that a transform on .stage
     opens a stacking context. */
  .site-header { backdrop-filter: none; }

  .main-nav {
    position: fixed; inset: 0; left: 0; right: 0; top: 0;
    width: 100vw; height: 100dvh; max-height: none;
    border: 0; border-radius: 0; padding: 18px 22px 26px;
    background: var(--cream);
    display: none; flex-direction: column; align-items: stretch;
    overflow-y: auto; overscroll-behavior: contain; z-index: 90;
    /* SA-24: the desktop bar's `gap` is inherited by the column and was the
       hidden cost that made the sheet scroll -- 18px between eleven rows is
       180px, more than the contact line and the logo together. The rows carry
       their own 44px height and a separating border, so the gap buys nothing
       here. Measured at 360x740: 832px of content in a 740px sheet, and 661
       once this is zero. */
    gap: 0;
  }
  .main-nav.open { display: flex; }
  /* The page must not scroll under an open sheet. */
  body.nav-open { overflow: hidden; }
  /* The burger sits UNDER the sheet it opened, so hide it while open: the
     control that opened it must not float over the thing it opened (SA-19). */
  body.nav-open .menu-toggle { visibility: hidden; }

  .nav-head {
    display: flex; align-items: center; justify-content: center;
    position: relative; padding: 6px 0 18px; margin-bottom: 6px;
    border-bottom: 1px solid var(--line);
  }
  .nav-head .nav-logo img { width: 140px; height: auto; }
  .nav-close {
    position: absolute; right: -8px; top: 50%; transform: translateY(-50%);
    width: 44px; height: 44px; display: grid; place-items: center;
    background: none; border: 0; color: var(--ink);
  }

  /* Nine items plus a logo and a contact line: at 15px padding the sheet was
     109px over on a 390x844 phone and scrolled. 44px rows still clear the tap
     floor and the whole menu fits. */
  .main-nav > a {
    width: 100%; text-align: center; padding: 11px 0; font-size: 1rem;
    min-height: 44px; display: flex; align-items: center; justify-content: center;
    border-bottom: 1px solid var(--line);
  }
  /* The underline animation is a pointer affordance and reads as a strike
     through a centred row (the MH-1 bug), so the active item takes colour. */
  .main-nav > a::after { display: none; }
  .main-nav > a.active { color: var(--sage-dark); }

  /* SA-96: the vertical rhythm here is TIGHT on purpose. Every row in this
     foot is a 44px tap target holding about 15px of text, so the white is
     mostly inside the targets and cannot be taken back without shrinking
     them, which this file never does. What CAN go is the space BETWEEN the
     rows, and it had accumulated in four separate places: 20px of padding
     here, 10px under the promises, the base `.social` margin of 14px, and
     the 4px gap between all of them. Tightened to 12/2/4, which takes 26px
     out of the stack and closes the two gaps that actually read as holes --
     promises-to-phone and email-to-socials. */
  .nav-foot {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    margin-top: auto; padding-top: 12px; width: 100%;
  }
  /* `tel:` and `mailto:` here are standalone actions in a menu, not links
     inside a sentence, so WCAG 2.5.5's inline-text exemption does not cover
     them and they take the same 44px floor as everything else. */
  .nav-foot a {
    min-height: 44px; display: flex; align-items: center;
    font-size: .95rem; color: var(--muted); border: 0;
  }
  /* SA-96: her email in the green from her logo, at her request. `--sage-dark`
     rather than the raw #637f43 measured off the file: that green is 4.52 on
     white, which clears the 4.5 floor for 15px text by nothing at all, and
     this is 6.16. It is also the green already directly above it on the
     promise marks (SA-86) and on the active nav item, so the foot uses one
     green rather than introducing a second.
     The PHONE stays `--muted` deliberately: she asked for the email, and
     making both green would leave nothing for the green to distinguish. */
  .nav-foot a[href^="mailto:"] { color: var(--sage-dark); }

  /* ── SA-87: the three promises at the foot of the sheet ──────────────────
   * The claims a phone visitor otherwise never meets: `.hero-badges` is
   * display:none under 780px (SA-80) because it shared the hero's one grid
   * cell with the copy and landed on the headline.
   *
   * Three equal columns rather than a stack, because stacked they would push
   * the contact line off a 360x640 screen -- the sheet fits that size with
   * nothing to spare and SA-19 says so. The labels wrap to two lines and the
   * rows stay level because every column is 1fr.
   *
   * `minmax(0, 1fr)` and not `1fr`: a grid item's automatic minimum size is
   * its content's min-content width, so a long unbroken word would push the
   * track wider than the column and overflow the sheet. The SA-2 room-strip
   * trap, and SA-32 hit it again on the logo wall.
   */
  .nav-promises {
    list-style: none; margin: 0 0 2px; padding: 14px 0 0; width: 100%;
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px;
    /* NO border-top. Every `.main-nav > a` already carries a border-bottom, so
       the last menu item draws the rule above this row -- adding one here put a
       SECOND line 20px below the first with an empty band between them, which
       reads as a stray blank row. Same family as the double rule SA-19 found
       under the masthead: check what the neighbour already draws. */
  }
  .nav-promises li {
    display: flex; flex-direction: column; align-items: center; gap: 6px;
    text-align: center;
    /* The charcoal of "smart" in her logo (SA-85), which is what she asked
       for. 6.90 on the sheet's white ground. */
    color: var(--smart-grey);
  }
  .nav-promises li > span:last-child {
    font-size: .70rem; font-weight: 700; line-height: 1.3;
  }
  .nav-promises svg { display: block; }

  /* Her socials sit right under this and were `--muted` at 18px, which read as
     leftovers rather than as a row of links. Same charcoal, and big enough to
     see. SCOPED to the sheet: `.social` is also the page footer's row, and
     that one is not what she asked about. */
  .nav-foot .social a { color: var(--smart-grey); }
  /* The base `.social` rule carries `margin: 14px 0 0`, which is right in
     the page footer and is a hole here on top of the 4px flex gap. Scoped,
     because that other row is not what she asked about (the SA-87 rule). */
  .nav-foot .social { gap: 14px; margin-top: 4px; }
  /* The partial hardcodes width="18" height="18" as ATTRIBUTES, and CSS beats a
     presentation attribute -- so this is where the size is set, and setting the
     colour alone (which is all the first pass did) leaves them at 18px in a
     44px target, still reading as leftovers. The tap target does not change. */
  .nav-foot .social svg { width: 22px; height: 22px; }

}

/* ── SA-87b: the promises, and the viewport a REAL phone actually gives ───────
 * The first version hid these under 830px of viewport height, measured at
 * 390x844. THAT IS THE DEVICE'S SCREEN, NOT ITS VIEWPORT: Safari's address bar
 * takes ~100px, so an iPhone 14 gives a page about 745px and a Pixel 7 about
 * 732. The guard therefore hid the row on essentially every real phone, and
 * the "96px of dead space" it was meant to fill does not exist either -- at
 * 745 the sheet needs 739 and fits with SIX pixels to spare.
 *
 * MEASURE A PHONE AT ITS VIEWPORT, NOT AT ITS SCREEN SIZE. 844, 896 and 932
 * are marketing numbers; subtract the browser chrome before believing them.
 *
 * So the row costs 88px that the sheet does not have, and the sheet scrolls
 * ~80px to show it. That is a deliberate trade and not an oversight: the menu
 * already carries `overflow-y: auto; overscroll-behavior: contain`, every nav
 * item is still above the fold, and the partly-visible row is its own hint
 * that there is more below. What it costs is that the contact line now needs
 * a small scroll where it did not before.
 *
 * Below 700px only the PROMISES go (SA-95). This paragraph used to say the
 * whole foot went, promises included, and that was the fault Theresa reported:
 * every phone she owns is under 700, so her contact line and socials were
 * deleted on all of them. Navigation and contact both beat three claims.
 */
/* iOS zooms the whole page when a focused input is under 16px. Deliberately
   left at 780px when the MENU breakpoint moved to 1150px (SA-36): this is a
   phone-zoom fix, and forcing 16px inputs onto a 1100px desktop window is a
   different decision with no reason behind it. */
@media (max-width: 780px) {
  input:not([type=hidden]), select, textarea { font-size: 16px !important; }
}

/* A short screen, not a narrow one: a phone in landscape has this problem and
   a tall narrow one does not (the MH-1 rule, keyed on height). */
@media (max-width: 1150px) and (max-height: 700px) {
  .main-nav { padding: 10px 22px 16px; }
  .main-nav > a { padding: 2px 0; font-size: .95rem; }
  .nav-head { padding: 0 0 8px; margin-bottom: 2px; }
  .nav-head .nav-logo img { width: 118px; }
  /* SA-95: NOTHING IN THE FOOT IS HIDDEN HERE, and two rules have now been
     deleted from this spot. SA-19 dropped the whole `.nav-foot`, copying
     MH-1's drawer, so Theresa's phone number, email and socials vanished on
     every phone she owns -- an iPhone SE gives 553, a 13 mini 629, a Galaxy
     S22 691, all under this trim. SA-95 then dropped the PROMISES instead,
     which was the same mistake one size smaller: the row exists precisely so
     a phone visitor sees those three claims, because `.hero-badges` is hidden
     under 780px, and on her phones it showed them nowhere at all.

     Both rules existed because something had to go. Nothing does: this sheet
     carries `overflow-y: auto`, so the honest answer is to let it scroll.
     Measured with the promises on, every phone keeps all seven nav items
     above the fold and pays 74-88px more scroll for a foot you scroll to
     anyway (SE 212, 13 mini 136, S22 74).

     What remains here is the padding trim, which is what actually buys the
     room. Targets are never shrunk to make something fit. */
}

/* SA-22: form fields. The export had no standalone form styling beyond the
   checkout, and the contact page needs it. */
.field { margin-bottom: 14px; }
.field label { display: block; font-size: .82rem; font-weight: 700; margin-bottom: 6px; }
.field input, .field textarea, .field select {
  width: 100%; padding: 12px 14px; border-radius: var(--radius-sm);
  border: 1px solid var(--line); background: rgba(255,255,255,.75);
  color: var(--ink);
}
.field input:focus, .field textarea:focus { outline: 2px solid var(--sage); outline-offset: 1px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
.contact-layout { display: grid; grid-template-columns: 1fr 1.2fr; gap: 24px; align-items: start; }
.contact-lines { display: flex; flex-direction: column; gap: 2px; margin-top: 10px; }
.contact-lines a { min-height: 44px; display: flex; align-items: center; font-weight: 700; }
.split-feature { display: grid; grid-template-columns: 1.1fr 1fr; gap: 0; overflow: hidden;
                 border-radius: var(--radius); }
.split-feature img { height: 100%; object-fit: cover; }
/* SA-62: the header's look now lives in ONE block at the foot of this
   file. This pair used to re-add the card padding and radius AFTER the
   base rule set them, at equal specificity, which is the collision the
   first attempt at this walked into. */
.page-hero h1 + p { margin-top: 12px; }
.page-hero h1 { margin: 10px 0 12px; }
.shop-controls { display: flex; align-items: center; justify-content: space-between;
                 gap: 16px; flex-wrap: wrap; margin-bottom: 22px; }
.chip { display: inline-flex; align-items: center; padding: 9px 14px; border-radius: 999px;
        border: 1px solid var(--line); background: rgba(255,255,255,.7); font-size: .82rem;
        font-weight: 700; }
.chip.on, .chip:hover { background: var(--sage); color: #fff; border-color: var(--sage); }
.catalog-card .meta { padding: 12px 4px 0; }
.catalog-card .meta h3 { font-size: 1.15rem; }
.catalog-card .acts { display: flex; gap: 8px; align-items: center;
                      flex-wrap: wrap; padding: 10px 4px 0; }
.link-arrow { font-weight: 700; font-size: .84rem; color: var(--sage-dark); }

/* Any grid with more than one fraction needs a breakpoint in the same commit
   (the AG-3 rule). */
@media (max-width: 900px) {
  .contact-layout, .split-feature { grid-template-columns: 1fr; }
  .split-feature img { height: 260px; }
}
@media (max-width: 560px) {
  .form-grid { grid-template-columns: 1fr; }
  .page-hero .box { padding: 0 0 22px; }
  .catalog-card .acts .btn { width: 100%; justify-content: center; }
}

/* ── SA-22: what the audit measured, fixed ───────────────────────────────────
 * Every number below came from walking all 11 pages at 1440 and 390, not from
 * judging the palette by eye.
 */

/* Her sage as a button ground puts white labels at 4.27:1, under the 4.5 small
 * text needs. --sage-dark is 6.16:1 and is already in her palette, so nothing
 * new is invented. Hover keeps the darker end. */
/* SA-62: she asked for one button treatment, white wording on charcoal.
   This block is what actually wins over the base rule above, so both
   moved together or the sage would have come straight back. */
.btn-primary { background: var(--charcoal); border-color: var(--charcoal); color: #fff; }
.btn-primary:hover { background: var(--charcoal-dark); border-color: var(--charcoal-dark); }
.chip.on, .chip:hover { background: var(--sage-dark); border-color: var(--sage-dark); }

/* A caption laid straight on a photograph measured 1.36:1 on the project and
 * artist cards: unreadable, and it varies with whichever image is behind it.
 * A scrim under the text, graded so the top of the picture is untouched.
 * Tested against the LIGHTEST cover in her library, not the darkest (MH-1). */
.project-card { position: relative; overflow: hidden; }
.project-card .caption {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  padding: 40px 18px 16px; color: #fff;
  background: linear-gradient(to top, rgba(20,22,17,.88) 0%,
                              rgba(20,22,17,.72) 42%, rgba(20,22,17,0) 100%);
}
.project-card .caption b { color: #fff; display: block; }
.project-card .caption span { color: rgba(255,255,255,.88); font-size: .82rem; }

/* Her .cta-band is a DARK olive gradient with white copy. An earlier version of
 * this block repainted the paragraph in --muted on top of it, which is the
 * opposite of a fix: only the LAYOUT is overridden here, never the colours. */
.cta-band { grid-template-columns: 1fr auto; }

/* Touch targets. The floor is a max-width rule, not a phone-only one: a tablet
 * is a touch device too. Inline links inside a sentence are exempt (WCAG
 * 2.5.5) and are left alone. */
@media (max-width: 1000px) {
  .btn, .btn-sm, .chip, .icon-btn { min-height: 44px; }
  .btn-sm { padding: 11px 16px; }
  .link-arrow { min-height: 44px; display: inline-flex; align-items: center; }
}

@media (max-width: 700px) {
  .cta-band { grid-template-columns: 1fr; }
  .cta-band .btn { width: 100%; }
}

/* 4.27:1 with white type, same as the buttons were. */
.pill { background: var(--sage-dark); }
.chip.on, .chip:hover { color: #fff; }

/* WhatsApp's own green puts white at 1.98:1. Their darker brand green clears
 * 4.5, and the mark is still unmistakably WhatsApp. */
.floating-whatsapp { background: #0f7c70; }

/* --muted lands at 4.49:1 on the linen surface, a hair under the 4.5 that
 * 16px body copy needs. Measured, not adjusted by eye. */
:root { --muted: #67675e; }

/* The footer link columns are stacked lists of navigation on a phone, not
   links inside a sentence, so the 44px floor applies to them too. */
@media (max-width: 1000px) {
  .footer-grid a { min-height: 44px; display: inline-flex; align-items: center; }
  .footer-social a { width: 44px; height: 44px; justify-content: center; }
}

/* The remaining measured targets. */
.icon-btn { width: 44px; height: 44px; }
.demo-banner a { min-height: 44px; display: inline-flex; align-items: center; }
/* A card title duplicates the picture link above it, so WCAG 2.5.5's
   equivalent-target exception would cover it -- but giving it a real hit area
   is cheaper than arguing, and a 21px title is a poor thumb target regardless. */
.catalog-card .meta h3 a { display: inline-flex; align-items: center; min-height: 44px; }

/* .cta-band sets color:#fff for the band, and .cta-band .btn is a near-white
   pill -- so the button label was white on white at 1:1. */
.cta-band .btn { color: var(--ink); }

/* The desktop nav row measured 38px. The header is 88px tall, so there is room
   for a proper target and no reason to leave one short. */
.main-nav > a { min-height: 44px; display: inline-flex; align-items: center; }

/* A short word in an inline-flex link is narrower than 44px however tall it is,
   which is what left "Artists" under the floor while its longer neighbours
   passed. */
@media (max-width: 1000px) {
  .footer-grid a { min-width: 44px; }
}

/* WCAG 2.5.8 (AA) asks 24px of ANY pointer, and a footer column or an arrow
   link is a stacked navigation list, not a link inside a sentence, so the
   exemption does not cover it. A base floor at every width; the 44px touch
   floor above still applies under 1000px. */
.footer-grid a, .link-arrow {
  min-height: 32px; display: inline-flex; align-items: center;
}
.footer-social a { min-height: 0; }

/* LAST ON PURPOSE. The base 32px floor above is declared at the same
   specificity as the touch floor, so whichever comes later wins: putting the
   base rule after the media query silently undid 44px on every phone. The
   same trap as the SA-13 hero CTA -- when a rule "does nothing", count the
   selectors AND check what comes after it. */
@media (max-width: 1000px) {
  .footer-grid a, .link-arrow, .btn, .btn-sm, .chip, .icon-btn { min-height: 44px; }
  .footer-grid a { min-width: 44px; }
  .footer-social a { width: 44px; height: 44px; min-height: 44px; }
}

/* SA-23: the information pages. One layout for all seven. */
.info-picture { width: 100%; height: clamp(220px, 34vw, 420px); object-fit: cover;
                border-radius: var(--radius); margin-bottom: 26px; }
.info-body { padding: 34px 38px; border-radius: var(--radius); }
.info-row { padding: 20px 0; border-bottom: 1px solid var(--line); }
.info-row:last-child { border-bottom: 0; padding-bottom: 0; }
.info-row:first-child { padding-top: 0; }
.info-row h2 { font-size: clamp(1.3rem, 2vw, 1.7rem); margin-bottom: 8px; }
.info-row p { margin: 0; color: var(--muted); max-width: 74ch; }
@media (max-width: 640px) { .info-body { padding: 24px 20px; } }

/* SA-23: the basket. */
.price-lg { font-family: var(--serif); font-size: 2rem; margin: 6px 0 14px; }
.catalog-card .price { font-weight: 700; color: var(--sage-dark); }
.cart-row { display: grid; grid-template-columns: 78px 1fr auto auto; gap: 14px;
            align-items: center; padding: 14px 0; border-bottom: 1px solid var(--line); }
.cart-row.summary { grid-template-columns: 1fr auto; }
.cart-row img { width: 78px; height: 66px; object-fit: cover; border-radius: 12px; }
.cart-row small { display: block; }
.cart-row-actions { display: flex; align-items: center; gap: 8px; }
.cart-row-actions button { min-width: 34px; min-height: 34px; border-radius: 999px;
                           border: 1px solid var(--line); background: rgba(255,255,255,.7); }
.cart-total { display: flex; justify-content: space-between; align-items: center;
              padding-top: 16px; font-family: var(--serif); font-size: 1.4rem; }
.cart-icon-btn { position: relative; }
.cart-count { position: absolute; top: 2px; right: 2px; min-width: 18px; height: 18px;
              border-radius: 999px; background: var(--sage-dark); color: #fff;
              font-size: .68rem; font-weight: 700; display: grid; place-items: center; }

@media (max-width: 640px) {
  .cart-row { grid-template-columns: 64px 1fr; }
  .cart-row img { width: 64px; height: 56px; }
  .cart-row-actions, .cart-row > strong:last-child { grid-column: 2; }
}
/* LAST: the touch floor, after the base rules (see the note further up). */
@media (max-width: 1000px) {
  .cart-row-actions button { min-width: 44px; min-height: 44px; }
}

/* The footer link columns are a LIST. Giving them inline-flex for the target
   floor made them flow into a paragraph -- "WallpaperVisualiserMy Room" -- so
   they are block-level flex instead, which keeps the height and the line break. */
.footer-grid > div > a { display: flex; width: fit-content; }
.footer-logo { width: 170px; max-width: 60%; height: auto; }

/* SA-23: the shared social partial and its visually-hidden labels.
 *
 * These belong in the BASE stylesheet, not in components.css: the footer is on
 * every page and components.css is only loaded by the pages that use the
 * compositor. Without them the socials rendered as a bulleted list with the
 * screen-reader labels showing -- "• Smart Art on Facebook" -- which is the
 * same class of fault as the undefined custom properties: a V1 partial included
 * in V2 without the CSS it depends on. */
.vh {
  position: absolute !important; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.social {
  display: flex; align-items: center; gap: 4px; list-style: none;
  margin: 14px 0 0; padding: 0;
}
.social a {
  display: grid; place-items: center; width: 44px; height: 44px;
  border-radius: 50%; color: var(--muted);
}
.social a:hover { color: var(--sage-dark); background: rgba(110,129,80,.10); }

/* ── SA-24: the shop grid, measured ────────────────────────────────────────────
 * Three faults the audit found once the shop page was built out to match her
 * export. Declared at the END of the file, after the SA-22 touch block, because
 * a rule appended after a media query silently undoes it at equal specificity
 * -- the trap SA-22b already walked into once.
 */

/* Her sage puts a white label at 4.27:1. `--sage-dark` is 6.16 and is already
   hers, so this is the same swap SA-22b made for the buttons rather than a new
   colour. Hover keeps the lighter sage: it is a pointer state, not a label
   anyone has to read at rest. */
.shop-filter.active { background: var(--sage-dark); border-color: var(--sage-dark); }

/* "Details ->" is a standalone call to action, not a link inside a sentence, so
   WCAG 2.5.5's inline exemption does not cover it and it needs a real box. It
   measured 64x19. Same reasoning as `.link-arrow` in V1 (SA-2). */
.text-link { display: inline-flex; align-items: center; min-height: 24px; }

@media (max-width: 1000px) {
  /* The touch floor. A card's title link measured 330x42 -- two pixels under,
     which is exactly the kind of miss that only a measurement finds. */
  .text-link { min-height: 44px; }
  .catalog-card h3 { padding: 2px 0; }
  .catalog-card > a > h3, .catalog-body > a { display: block; min-height: 44px; }
}

/* SA-24: the newsletter pop-up's close control measured 40x40, which is under
   the touch floor -- and it is the ONLY way out of a dialog that covers the
   page, so it is the last control that should be hard to hit. */
@media (max-width: 1000px) {
  .modal-close { width: 44px; height: 44px; }
}


/* ── SA-91: the range tiles moved onto the home band's component ─────────────
 * `.category-tile` and `.category-cloud` are rendered by NOTHING now. The SA-30
 * block that lived here -- the cover <img>, the `has-art` scrim SA-90
 * rebalanced, the hover fill -- went with it, because the next person to tune a
 * scrim that styles no element would be tuning nothing (the SA-5 rule about ~90
 * lines of dead CSS left behind by a layout change).
 *
 * Its base rules still sit in the converted export's minified lines near the
 * top of this file. Left alone deliberately: hand-editing a minified line to
 * remove one selector is how something else in it gets broken.
 */


/* SA-32: a product card's copy is an EXCERPT, whatever the client wrote.
   Her imported descriptions are full paragraphs, so the cards ran past 900px
   and the grid went ragged. Clamped in CSS rather than truncated on import,
   because the next client's text is not ours to shorten either -- and the whole
   description is still on the product page, one click away. */
.catalog-body p {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4;
  overflow: hidden;
}

/* SA-32: with a was-price beside it the row no longer fits, and the tag was
   wrapping to two lines. The tag is two short words and should never wrap; the
   PRICE is what gets the remaining room. */
.catalog-topline { flex-wrap: wrap; }
.catalog-topline .tag { white-space: nowrap; }
.catalog-topline .price { white-space: nowrap; margin-left: auto; }

/* SA-32: the price a product WAS. Muted and lighter than the live price, so
   the eye lands on what it costs today. */
.price .was, .price-lg .was {
  color: var(--muted); font-weight: 400; margin-right: 6px;
  font-size: .85em; text-decoration-thickness: 1px;
}




/* ── SA-34: an eyebrow INSIDE the dark band ───────────────────────────────────
 * `.eyebrow` is `--sage-dark`, which is a dark green, and `.cta-band` is a dark
 * olive: putting one inside the other is the exact fault SA-22b recorded when a
 * `.cta-band` override repainted the paragraph in `--muted` on the same band.
 * White at .82 measures over 7 on that ground; the dark green measured 1.3.
 *
 * The band's own grid is `1.2fr 1fr auto` and expects THREE children. These
 * bands have two -- words, then buttons -- so they say so rather than leaving a
 * phantom middle column pulling the buttons off the right edge.
 */
.cta-band .eyebrow { color: rgba(255, 255, 255, .82); }
.cta-band h2 { margin: 6px 0 8px; }
.cta-band.two, .section-tight .cta-band { grid-template-columns: 1.5fr auto; }
.cta-band .card-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.cta-band .btn.btn-primary { background: #fff; color: var(--charcoal); border-color: #fff; }

@media (max-width: 900px) {
  .cta-band.two, .section-tight .cta-band { grid-template-columns: 1fr; }
}

/* ── SA-34: the menu grew a chevron ───────────────────────────────────────────
 * A MENU LABEL MUST NEVER WRAP, AT ANY WIDTH. SA-27 hit "Wall / Panels" and
 * "My / Room" between 820 and 1200px and fixed it with `nowrap` -- but scoped
 * inside `@media (max-width: 1280px)`, because at 1440 the bar fitted anyway.
 * Adding the Artists chevron made it not fit, so the same two labels broke at
 * 1440 where no rule was watching. A conditional fix for an unconditional rule
 * only holds until something else on the bar changes width.
 *
 * The tighter gap stays bounded to the range that needs it, and stays BOUNDED
 * BELOW at 781px: the phone sheet sets `gap: 0` inside a max-width block, and
 * an unbounded rule appended after it would put the gap back between every row
 * of the sheet.
 */
.main-nav > a, .navgroup > a { white-space: nowrap; }

@media (min-width: 781px) and (max-width: 1280px) {
  .navgroup { gap: 0; }
  .navgroup > a { font-size: .84rem; }
  .navgroup-toggle { padding: 2px; }
}


/* SA-34: `.lead` came across from V1 with the artists hero and had no rule
   here, which the SA-26 class guard caught before it shipped -- unstyled markup
   inherits legible colours and overflows nothing, so nothing else would have.
   It is a standfirst: one step up from body copy, one step down from the
   paragraph above it. */
.lead { font-size: 1.06rem; color: var(--muted); margin: 10px 0 0; max-width: 62ch; }


/* ── SA-35: the About quote, the contact hours and the consultation card ───── */
.about-pull { max-width: 60ch; margin: 0 auto; text-align: left; }

.contact-hours {
  display: flex; gap: 16px; margin-top: 20px; padding-top: 18px;
  border-top: 1px solid var(--line);
}
.contact-hours .k {
  flex: 0 0 74px; font-size: .7rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); padding-top: 3px;
}
.contact-hours .v { font-size: .92rem; }
.contact-hours small { display: block; color: var(--muted); font-size: .82rem; }

/* Nested inside the studio card, so it needs its own edge rather than a
   surface: two paper panels one inside the other read as a mistake. */
.consult-card {
  padding: 22px; border-radius: var(--radius-sm); margin-top: 22px;
  border: 1px solid var(--line); background: var(--linen);
}
.consult-card .pin {
  display: inline-block; background: var(--sage-dark); color: #fff;
  padding: 6px 14px; border-radius: 999px; font-size: .72rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
}
.consult-card p { margin: 14px 0 16px; color: var(--muted); font-size: .92rem; }

@media (max-width: 900px) {
  .contact-hours { flex-direction: column; gap: 4px; }
  .contact-hours .k { flex: none; }
}

/* ── SA-34: the three print claims, and the price promise ─────────────────── */
.specs2 {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 26px; margin-top: 26px;
}
.spec2 {
  background: #fff; border: 1px solid var(--line);
  border-radius: var(--radius); padding: 28px;
}
.spec2 .n {
  display: block; font-family: var(--serif); font-size: 1.5rem;
  color: var(--sage-dark); margin-bottom: 10px;
}
.spec2 h3 { font-size: 1.2rem; margin: 0 0 8px; }
.spec2 p { color: var(--muted); margin: 0; font-size: .92rem; }

.promise2 {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 40px; padding: 38px; border-radius: var(--radius); align-items: center;
}
.promise2 h2 { margin: 8px 0 12px; }
.promise2 > div > p { color: var(--muted); margin: 0; }
.pull2 {
  margin: 0; padding-left: 22px; border-left: 3px solid var(--sage);
  font-family: var(--serif); font-size: 1.3rem; line-height: 1.5;
}
.pull2 cite {
  display: block; margin-top: 12px; font-family: var(--sans);
  font-size: .76rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); font-style: normal;
}

@media (max-width: 900px) {
  .specs2 { grid-template-columns: 1fr; gap: 14px; }
  .promise2 { grid-template-columns: 1fr; gap: 26px; padding: 26px; }
}

/* ── SA-34: the Artists menu group ─────────────────────────────────────────────
 * V1 listed the artists by name in the nav and this design did not. A
 * hover/focus drop-down on a pointer device, and a tap-to-expand panel inside
 * the phone sheet -- `position: static` on the panel is the single declaration
 * that converts one into the other (SA-6).
 */
.navgroup { position: relative; display: inline-flex; align-items: center; gap: 2px; }
.navgroup-toggle {
  border: 0; background: none; padding: 4px; cursor: pointer;
  color: inherit; display: inline-flex; align-items: center; line-height: 0;
  /* SA-56: measured at 22x22, two pixels under WCAG 2.5.8's 24px pointer
     minimum. It is the only control on the desktop header that was, and the
     fix costs nothing because the chevron is centred in it either way. The
     phone sheet's own 44px floor still wins there. */
  justify-content: center; min-width: 24px; min-height: 24px;
}
.navgroup-toggle svg { transition: transform .18s; }
.navgroup.open > .navgroup-toggle svg { transform: rotate(180deg); }

.navdrop {
  position: absolute; top: 100%; left: -14px; z-index: 40;
  min-width: 232px; padding: 8px; display: none; flex-direction: column;
  background: #fff; border: 1px solid var(--line);
  border-radius: var(--radius-sm); box-shadow: 0 14px 32px rgba(52, 48, 40, .14);
}
.navgroup:hover > .navdrop,
.navgroup:focus-within > .navdrop,
.navgroup.open > .navdrop { display: flex; }
.navdrop a {
  display: flex; flex-direction: column; gap: 1px;
  padding: 9px 12px; border-radius: 10px; text-decoration: none;
  color: var(--ink); font-size: .9rem; white-space: nowrap;
}
.navdrop a:hover { background: var(--linen); color: var(--sage-dark); }
.navdrop a small { color: var(--muted); font-size: .72rem; }
.navdrop a.all {
  margin-top: 4px; border-top: 1px solid var(--line); border-radius: 0 0 10px 10px;
  font-weight: 700; font-size: .78rem; letter-spacing: .06em;
  text-transform: uppercase; color: var(--sage-dark);
}

/* SA-36: raised with the sheet. Left at 780 it made the Artists row the one
   left-aligned item in a centred menu between 781 and 1150px -- the group is a
   <div>, so `.main-nav > a` never matched it. */
@media (max-width: 1150px) {
  /* Inside the sheet the group is a full-width row and the panel sits in the
     flow beneath it, so it pushes the rest of the menu down instead of floating
     over it. Hover is meaningless here, so ONLY `.open` shows it. */
  .navgroup { display: flex; width: 100%; justify-content: center; }
  .navdrop {
    position: static; display: none; width: 100%; min-width: 0;
    border: 0; box-shadow: none; background: transparent; padding: 4px 0 0;
  }
  .navgroup:hover > .navdrop, .navgroup:focus-within > .navdrop { display: none; }
  .navgroup.open > .navdrop { display: flex; }
  .navdrop a { align-items: center; text-align: center; min-height: 44px;
               justify-content: center; }
  .navgroup-toggle { min-width: 44px; min-height: 44px; justify-content: center; }
}

/* ══ SA-34: V1's content, in V2's chrome ═════════════════════════════════════
 * V1's artists page was a five-part editorial piece and V2's was three cards;
 * V1's case study was a hero, a facts table, the story and a gallery, and V2's
 * was one padded box. What went missing was not styling -- the BIOS, the rights
 * badges, the rights explainer, the client name, the year and the body copy.
 *
 * Everything below is that content given V2's surfaces (paper, linen, the serif
 * display face, the sage accent) rather than V1's brass-and-bone.
 */

/* ── The stats strip under the artists hero ────────────────────────────────── */
.ar2-stats {
  display: flex; flex-wrap: wrap; gap: 34px;
  padding: 0 4px 30px; border-bottom: 1px solid var(--line);
}
.ar2-stats div { display: flex; flex-direction: column; gap: 2px; }
.ar2-stats b { font-family: var(--serif); font-size: 2rem; line-height: 1; }
.ar2-stats span {
  font-size: .68rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted);
}

/* ── One artist, picture beside words ──────────────────────────────────────── */
.ar2-row {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 40px; align-items: center;
  padding: 44px 0; border-bottom: 1px solid var(--line);
}
.ar2-row:last-child { border-bottom: 0; }
/* The alternation is the wide-screen enhancement, never something a small
   screen has to cope with (the SA-6 rule). */
.ar2-row:nth-child(even) .shot { order: 2; }
.ar2-row .shot { display: block; border-radius: var(--radius); overflow: hidden; }
.ar2-row .shot img {
  width: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block;
}
.ar2-row .copy h2 { margin: 12px 0 6px; font-size: clamp(1.6rem, 2.6vw, 2.2rem); }
.ar2-row .copy h2 a { color: var(--ink); text-decoration: none; }
.ar2-row .copy h2 a:hover { color: var(--sage-dark); }
.ar2-row .tagline { color: var(--muted); margin: 0 0 16px; }

/* A fact line: dates, origin, movement, separated rather than punctuated. */
.meta.ar2-meta, .ar2-row .meta {
  display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0 0 10px;
  font-size: .78rem; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted);
}
.ar2-row .meta span + span, .ar2-meta span + span { position: relative; }
.ar2-row .meta span + span::before, .ar2-meta span + span::before {
  content: "·"; position: absolute; left: -10px; color: var(--line);
}

/* ── The rights badge ──────────────────────────────────────────────────────── */
/* Two states with DIFFERENT meanings, so they are told apart by more than
   colour: each carries its own word. Both measured on their own ground. */
.ar2-badge {
  display: inline-block; padding: 5px 12px; border-radius: 999px;
  font-size: .66rem; font-weight: 700; letter-spacing: .11em;
  text-transform: uppercase;
}
.ar2-badge.pd { background: var(--sage-soft); color: var(--sage-dark); }
.ar2-badge.lic { background: #f6e6cd; color: #7a5312; }

/* ── The rights explainer ──────────────────────────────────────────────────── */
.ar2-explain {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 34px; padding: 34px; border-radius: var(--radius);
}
.ar2-explain h3 { margin: 8px 0 10px; font-size: 1.35rem; }
.ar2-explain p { color: var(--muted); margin: 0; font-size: .92rem; }

/* ── An artist's own page ──────────────────────────────────────────────────── */
.ar2-lede {
  display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: 40px; align-items: start;
}
.ar2-cover {
  width: 100%; border-radius: var(--radius); margin: 0 0 22px; display: block;
}
.ar2-lede .bio p { margin: 0 0 16px; }
.ar2-note {
  margin: 22px 0 0; padding: 18px 22px; border-left: 3px solid var(--sage);
  background: var(--linen); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-family: var(--serif); font-size: 1.1rem; line-height: 1.5;
}
.ar2-side { padding: 26px; border-radius: var(--radius); position: sticky; top: 96px; }
.ar2-side h2 { font-size: 1.05rem; margin: 0 0 14px; }
.ar2-side .rights { font-size: .85rem; color: var(--muted); margin: 0 0 16px; }
.ar2-side dl { margin: 0 0 20px; }
.ar2-side dl div {
  display: flex; justify-content: space-between; gap: 16px;
  padding: 9px 0; border-bottom: 1px solid var(--line);
}
.ar2-side dl div:last-child { border-bottom: 0; }
.ar2-side dt {
  font-size: .7rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted);
}
.ar2-side dd { margin: 0; font-size: .9rem; text-align: right; }
.ar2-side .btn { width: 100%; text-align: center; }

/* ── A gallery of pieces or photographs ────────────────────────────────────── */
.ar2-gallery { columns: 2 340px; column-gap: 16px; margin-top: 22px; }
/* `columns: 2 148px` for a long set: "2 columns only if each can be 148px", and
   200 does not fit twice in a 342px phone column, which is how Frans Smit's 28
   paintings became a 14,801px page in V1. */
.ar2-gallery.dense { columns: 2 148px; }
.ar2-gallery button {
  display: block; width: 100%; margin: 0 0 16px; padding: 0;
  border: 0; background: none; cursor: pointer; position: relative;
  break-inside: avoid; border-radius: var(--radius-sm); overflow: hidden;
}
.ar2-gallery img { width: 100%; display: block; }
.ar2-gallery .t {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 24px 12px 10px;
  font-size: .78rem; color: #fff; text-align: left;
  background: linear-gradient(transparent, rgba(20, 20, 16, .78));
}

/* ── Other artists ─────────────────────────────────────────────────────────── */
.ar2-others { margin-top: 46px; padding-top: 26px; border-top: 1px solid var(--line); }
.ar2-others .row { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 14px; }
.ar2-others a {
  display: flex; align-items: center; gap: 12px; text-decoration: none;
  color: var(--ink); background: #fff; border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 10px 16px 10px 10px;
}
.ar2-others a:hover { border-color: var(--sage); }
.ar2-others img { width: 54px; height: 54px; object-fit: cover; border-radius: 10px; }
.ar2-others b { display: block; font-size: .92rem; }
.ar2-others small { color: var(--muted); font-size: .74rem; }

/* ── A case study ──────────────────────────────────────────────────────────── */
.pf2-hero { position: relative; }
.pf2-hero > img {
  width: 100%; height: clamp(320px, 52vh, 560px); object-fit: cover; display: block;
}
/* The scrim reaches full strength where the CAPTION sits, not at the very
   bottom edge: a caption 34px above a gradient's end lies on whatever the
   photograph is doing there (the PBC-1 lesson). */
.pf2-hero::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(20, 20, 16, .82) 0%,
              rgba(20, 20, 16, .62) 34%, rgba(20, 20, 16, .06) 72%);
}
.pf2-hero .container { position: absolute; inset: auto 0 0 0; z-index: 1; }
.pf2-hero-copy { padding: 0 0 34px; color: #fff; }
.pf2-hero-copy h1 { color: #fff; margin: 6px 0 0; }
.pf2-hero-copy .client {
  margin: 0; font-size: .74rem; letter-spacing: .14em;
  text-transform: uppercase; color: rgba(255, 255, 255, .82);
}
.pf2-logo {
  display: inline-flex; background: #fff; border-radius: 12px;
  padding: 10px 14px; margin: 0 0 14px;
}
.pf2-logo img { height: 34px; width: auto; display: block; }

.pf2-facts {
  display: flex; flex-wrap: wrap; gap: 12px 44px; margin: 0 0 30px;
  padding: 0 0 24px; border-bottom: 1px solid var(--line);
}
.pf2-facts div { display: flex; flex-direction: column; gap: 3px; }
.pf2-facts dt {
  font-size: .68rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted);
}
.pf2-facts dd { margin: 0; font-size: .95rem; }
.pf2-body { max-width: 68ch; margin: 0 0 30px; }
.pf2-body .standfirst { font-size: 1.12rem; line-height: 1.62; }
.pf2-body p { margin: 0 0 16px; }

.pf2-nextprev {
  display: flex; justify-content: space-between; gap: 20px;
  margin-top: 40px; padding-top: 22px; border-top: 1px solid var(--line);
}
.pf2-nextprev a {
  display: flex; flex-direction: column; gap: 3px; text-decoration: none;
  color: var(--ink); font-family: var(--serif); font-size: 1.1rem;
}
.pf2-nextprev a:hover { color: var(--sage-dark); }
.pf2-nextprev a.next { text-align: right; }
.pf2-nextprev small {
  font-family: var(--sans); font-size: .68rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted);
}

@media (max-width: 900px) {
  /* Stacked: picture, then words, in that order for every row -- the
     alternation is what a wide screen gets, not something a phone unpicks. */
  .ar2-row { grid-template-columns: 1fr; gap: 20px; padding: 30px 0; }
  .ar2-row:nth-child(even) .shot { order: 0; }
  .ar2-explain { grid-template-columns: 1fr; gap: 26px; padding: 26px; }
  .ar2-lede { grid-template-columns: 1fr; gap: 26px; }
  .ar2-side { position: static; }
  .ar2-stats { gap: 22px; }
  .ar2-stats b { font-size: 1.6rem; }
  .pf2-facts { gap: 12px 26px; }
  .pf2-nextprev { flex-direction: column; }
  .pf2-nextprev a.next { text-align: left; }
}

/* ── SA-32: the wall of companies ─────────────────────────────────────────────
 * Her own portfolio page is one even grid of client marks on white plates, and
 * that is the layout Theresa chose in SA-5. This page had been reusing
 * `.style-card`, which is her export's STYLE TILE: `aspect-ratio: 1/1.12` with
 * `object-fit: cover`, so every wide wordmark was cropped to its middle and the
 * wall read "evi", "A", "al", ".DM", "oda". Those same rules also hide every
 * card past the sixth on a tablet and past the fourth on a phone, so most of
 * her brands were not on the page at all below 1200px.
 *
 * A logo is not a photograph: it is CONTAINED, never cropped, and it needs
 * quiet room around it.
 *
 * SA-33: the orange band was too loud, so it is gone and the MARKS carry her
 * colour instead -- the green from her own logo, #637f43, measured off the file
 * (28.6% of its opaque pixels; the charcoal is 63.1% and the orange 1.7%).
 *
 * Recoloured at the IMAGE layer by scripts/tint_smartart_logos.py rather than
 * with a CSS filter, which is the standing rule here (MH-1, AG-1): a filter
 * fixes today's artwork and wrecks the correct one a client supplies next
 * month. `logo_tinted()` resolves the twin by name and falls back to the
 * original, so a mark with no twin still shows.
 *
 * The plates stay WHITE, and that is where the contrast comes from: her green
 * measures 4.52 on white against 3.71 on the linen ground. WCAG 1.4.11 asks 3:1
 * of a graphical object, so the plate is what keeps the marks comfortably clear
 * rather than merely passing.
 */
.brand-band {
  background: transparent;
  color: var(--ink);
  padding: 46px 0 56px;
  margin-top: 8px;
}
.brand-band h2 { text-align: center; margin: 0; }
.brand-band .brand-sub {
  text-align: center; max-width: 68ch; margin: 12px auto 0;
  font-size: .92rem; color: var(--ink); opacity: .78;
  text-wrap: balance;
}
/* SA-36: the closing note under the logo wall, carried back from V1. Quieter
   than .brand-sub above the wall -- it is a qualifier, not a lede, and it must
   read as an aside without dropping under 4.5:1. */
.brand-band .brand-note {
  text-align: center; max-width: 60ch; margin: 30px auto 0;
  font-size: .84rem; line-height: 1.65; color: var(--ink); opacity: .7;
  text-wrap: balance;
}

/* SA-36: the consultation link on a design page. */
.product-info .consult-link { margin: 14px 0 0; font-size: .9rem; }
.product-info .consult-link a {
  color: var(--sage-dark); text-decoration: none;
  border-bottom: 1px solid currentColor; padding-bottom: 1px;
}
.product-info .consult-link a:hover { color: var(--ink); }

.brand-wall {
  display: grid;
  /* `minmax(0, 1fr)`, NOT `1fr`. A grid item's automatic minimum size is its
     content's min-content width, and a logo is a replaced element whose
     intrinsic width wins: `max-width: 100%` cannot constrain it, because the
     percentage resolves against the very column being sized. Four wide
     wordmarks pushed the wall 32px past the viewport and clipped the right-hand
     column off the orange band. Same trap as the SA-2 room strip. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 18px; margin-top: 34px;
}
.brand-plate {
  background: #fff; border-radius: 20px;
  /* Wider than tall, because a wordmark is: a square plate wastes its width on
     nothing and shrinks the mark to fit the height it does not need. */
  aspect-ratio: 3 / 2;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 8px;
  padding: 22px 26px; text-align: center; min-width: 0;
  border: 1px solid var(--line, rgba(50, 50, 40, .10));
  box-shadow: 0 6px 16px rgba(52, 48, 40, .07);
  text-decoration: none; color: var(--ink);
}
.brand-plate img {
  /* CONTAIN, and capped on BOTH axes: `max-width` alone lets a tall mark run
     out of the plate, and `object-fit` alone does nothing to an image that has
     no fixed box to fit into. */
  max-width: 100%; max-height: 100%;
  width: auto; height: auto; object-fit: contain;
}
.brand-plate .name {
  font-family: var(--serif); font-size: 1.05rem; line-height: 1.2;
}
.brand-plate .see {
  font-size: .68rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--sage-dark);
}
/* Only a plate that leads somewhere moves, and it says where without hover. */
@media (hover: hover) {
  .brand-plate.linked { transition: transform .18s, box-shadow .18s; }
  .brand-plate.linked:hover {
    transform: translateY(-3px);
    border-color: var(--sage);
    box-shadow: 0 12px 24px rgba(52, 48, 40, .14);
  }
}
.brand-plate.linked:focus-visible {
  outline: 3px solid var(--ink); outline-offset: 3px;
}

@media (max-width: 1000px) {
  .brand-wall { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 700px) {
  /* Two across on a phone, and EVERY brand still on the page -- the thing the
     borrowed style-card rules were quietly taking away. */
  .brand-wall { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  .brand-plate { padding: 16px 18px; border-radius: 16px; }
  .brand-band { padding: 40px 0 46px; }
}

/* SA-25: the selected range. Her export needs no such state, because each of
   her category pages is a separate file; ours keeps the tiles on screen while
   a range is showing, so the current one has to say so. `--sage-dark` rather
   than the `--sage` her hover uses: white on `--sage` measures 4.27, and this
   is a resting state somebody has to read, not a pointer flourish. */

/* SA-25: the link beside a section heading ("Visualise your room", "All
   wallpaper") is a standalone navigation control, not a link inside a
   sentence, so WCAG 2.5.5's inline exemption does not cover it. It measured
   111x25 on a phone. Declared after the other touch blocks on purpose. */
@media (max-width: 1000px) {
  .section-heading > a { display: inline-flex; align-items: center; min-height: 44px; }
}

/* ── SA-26: the two things V2 was rendering with V1's classes ─────────────────
 * Both looked "unstyled" for the same reason: the markup came from V1 and its
 * CSS lives in smartart.css, which this design must never load (the SA-4 rule).
 */

/* The preview and demo notices. Only a tap-target rule existed here, so they
   rendered as bare black text above the masthead, full-bleed and unstyled --
   the first thing anyone sees on a preview link. */
.demo-banner {
  background: var(--sage-dark); color: #fff;
  font-size: .82rem; text-align: center;
  padding: 9px 20px;
}
.demo-banner + .demo-banner { border-top: 1px solid rgba(255,255,255,.18); }
.demo-banner strong { font-weight: 700; }
.demo-banner a { color: #fff; text-decoration: underline; text-underline-offset: 3px; }
.demo-banner a { min-height: 44px; display: inline-flex; align-items: center; }

/* The Google reviews band (SA-21). Her export has none, so this is her request
   in this design's vocabulary rather than a conversion: the score sits on a
   linen panel beside the copy, and the whole thing is inside a container --
   which is what was actually missing, since `.shell` is V1's. */
.revband {
  display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr);
  gap: 34px; align-items: center;
  padding: 34px; border-radius: 30px;
}
/* SA-47: with the score switched off there is no left column to fill, and the
   copy landed in the 250px track with 866px of empty band beside it -- four
   words to a line under a heading, which reads as a broken section rather than
   as a deliberate one. The score is optional (SA-21 gave it its own switch),
   so the band has to be whole without it. */
.revband:not(:has(.revscore)) { grid-template-columns: minmax(0, 1fr); }

/* SA-49: her Trustindex widget sits under the band. Trustindex styles its own
   contents, so this is only the room it stands in -- and a minimum height so
   the page does not jump when a third-party script finally paints. */
/* SA-50: her own reviews, in her customers' words. A grid rather than a
   slider: a slider hides most of what it holds and needs script to show it,
   and these are six short cards. */
.rev-list {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: 16px; margin-top: 30px;
}
.rev-card {
  margin: 0; padding: 18px 20px; border: 1px solid var(--line);
  border-radius: var(--radius-sm); background: rgba(255, 255, 255, .55);
}
.rev-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.rev-head img, .rev-initial {
  width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
  background: var(--linen);
}
.rev-initial { display: grid; place-items: center; font-weight: 700; color: var(--muted); }
.rev-head b { display: block; font-size: .88rem; }
/* Darkened through the TOKEN, not replaced with a hex: --brass is her
   primary_color injected inline on <html>, so a literal would stop following
   her brand the day she changes it (the SA-21 star-row lesson). */
.rev-stars { color: color-mix(in srgb, var(--brass) 88%, #000); font-size: .82rem;
             letter-spacing: .06em; }
.rev-card blockquote {
  margin: 0; font-size: .9rem; line-height: 1.6; color: var(--ink);
  /* Their words, not ours, so they are capped rather than trimmed on the way
     in: the whole review is one click away on Google (the SA-37 rule). */
  display: -webkit-box; -webkit-line-clamp: 7; -webkit-box-orient: vertical;
  overflow: hidden;
}
.rev-reply {
  margin-top: 12px; padding-left: 12px; border-left: 3px solid var(--line);
  font-size: .82rem; color: var(--muted);
}
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.rev-widget { margin-top: 28px; }
.ti-widget:empty { min-height: 220px; }
.revscore {
  text-align: center; padding: 24px 18px; border-radius: 22px;
  background: var(--linen); border: 1px solid var(--line);
}
.revmark {
  display: inline-grid; place-items: center; width: 40px; height: 40px;
  border-radius: 50%; background: #fff; border: 1px solid var(--line);
  font-family: var(--serif); font-size: 1.25rem; color: var(--sage-dark);
}
.revnum {
  font-family: var(--serif); font-size: 2.9rem; line-height: 1;
  margin: 12px 0 6px; color: var(--ink);
}
/* Darkened through the token, not replaced with a hex: `--brass` is her
   primary colour injected inline, so a literal would stop following her brand
   the day she changes it. The plain sage measured 4.13 on this tint (SA-21). */
.revstars {
  margin: 0; font-size: 1.05rem; letter-spacing: .16em;
  color: color-mix(in srgb, var(--sage-dark) 92%, #000);
}
.revcount { margin: 8px 0 0; font-size: .8rem; color: var(--muted); }
.revcopy h2 { font-size: clamp(1.6rem, 3vw, 2.35rem); margin: 8px 0 12px; }
.revcopy p { color: var(--muted); max-width: 60ch; }
.revcopy .card-actions { margin-top: 18px; }

@media (max-width: 780px) {
  .revband { grid-template-columns: 1fr; gap: 22px; padding: 26px 22px; }
}

/* SA-27: eleven menu items wrapped the nav onto two lines inside a
   fixed-height header, anywhere from 820px to 1200px. The cost is the GAP, not
   the words: 240px of gap carrying 526px of text. Twelve gives every width in
   the band room for the logo, the menu and the three icons.

   Bounded BELOW at 781px on purpose. The phone sheet sets `gap: 0` and lives in
   a `max-width: 780px` block; an unbounded `max-width` rule appended after it
   would win at 390px and put 12px back between every row of the sheet, which is
   what made it scroll before. */
@media (min-width: 781px) and (max-width: 1280px) {
  .main-nav { gap: 12px; }
  /* And the labels themselves must not break mid-phrase. Measured at 900px,
     "Wall Panels" and "My Room" were each splitting over two lines inside the
     bar, which is what the two-row nav actually was -- not the row wrapping,
     the WORDS wrapping. */
  .main-nav > a { white-space: nowrap; }
}

/* ══ SA-28: the WhatsApp button ══════════════════════════════════════════════
 * It was a bolt-on: WhatsApp's own #25D366 (later a teal for contrast), a white
 * ring and a green-black shadow, sitting on her linen page looking like a
 * widget somebody installed.
 *
 * Her logo orange with INK on it, for the reason measured off the logo file:
 * #ffa038 is 2.03 under a white label and 7.31 under her ink. So the glyph and
 * the words are ink, and the orange does what it does in her logo -- a solid
 * mark, not type. The shadow is warm rather than green so it belongs to the
 * page, and the ring is a darker orange instead of white, which was the part
 * that made it read as pasted on.
 *
 * The glyph is unchanged and unmistakable, which is what carries the meaning;
 * the colour was never doing that work.
 */
.floating-whatsapp {
  background: #ffa038;
  color: #272823;
  border: 2px solid rgba(39, 40, 35, .10);
  box-shadow: 0 10px 26px rgba(120, 74, 20, .28);
  font-weight: 800;
  letter-spacing: .01em;
}
.floating-whatsapp:hover {
  background: #ffab4f;
  box-shadow: 0 16px 38px rgba(120, 74, 20, .34);
}
.floating-whatsapp:focus-visible {
  outline: 3px solid var(--ink); outline-offset: 3px;
}

/* ── SA-36: the header did not fit, and the LOGO was the crumple zone ─────────
 *
 * Measured with twelve items. `.main-nav` is 774px (the labels are `nowrap`,
 * correctly) and `.header-actions` is 148px, both fixed. The brand was the only
 * flexible child, so it absorbed every shortfall on its own:
 *
 *     1280px  logo 190      1100px  logo 71      860px  logo 5
 *     1200px  logo 171       900px  logo 45      820px  logo 0
 *
 * At 900px her wordmark is a pale smudge and at 820px it is GONE, on a header
 * that still reports one row and no overflow -- so every earlier sweep passed.
 * A logo is identity, not slack.
 *
 * Her export switched to the burger at 780px, which left the whole 781-1150
 * band showing a bar that does not fit, and it hid the cart, search and account
 * icons from 1050px down -- so a customer with something in their basket had no
 * way to reach it on a 1024px window, on a site whose shop is live.
 *
 * The bar needs 190 + 774 + 148 = 1112px of content inside a container that is
 * `100% - 48px`, so 1150px is where it stops fitting. That is the breakpoint,
 * derived rather than picked. (PBC-1 made exactly this mistake in the other
 * direction: a 1000px burger on a bar that needed ~1150.)
 */
.brand { flex: 0 0 auto; }
.brand img { min-width: 113px; }

@media (min-width: 781px) and (max-width: 1150px) {
  /* Show the burger, and let the sheet above do the rest. */
  .menu-toggle { display: grid; }
  /* Her export hides these from 1050px down. The cart is not decoration.
     `margin-left: auto` because with the bar out of the flow the header is
     brand | actions | burger, and evenly spaced they read as three unrelated
     things with a hole in the middle. They belong together on the right. */
  .header-actions { display: flex; margin-left: auto; }
}

/* SA-36: the BASKET must survive to the phone. Her export hides the whole
   actions group under 780px, which is defensible for search and the account
   icon and is not for the cart: the shop is live, and a customer holding two
   canvases had no way to reach them. The cart alone is 44px, which the phone
   header has. Search and account stay hidden -- both are reachable from the
   sheet and the footer, and a basket is not. */
@media (max-width: 780px) {
  .header-actions { display: flex; }
  .header-actions > *:not(.cart-icon-btn) { display: none; }
}

/* SA-47: the payment-method chooser on the checkout.

   It renders ONLY when the client has loaded credentials for a gateway, so on
   a bank-transfer-only shop none of this is on the page at all.

   `.field input` sets `width: 100%`, which turns a radio into a full-width
   bar. The override is scoped to (0,2,1) rather than left at (0,1,1) to match:
   at equal specificity this would depend on source order, and the SA-13 hero
   CTA and the SA-19 sheet links were both broken exactly that way by a rule
   appended later in the same file. */
.pay-choice { border: 0; padding: 0; margin: 0 0 14px; }
.pay-choice legend {
  padding: 0; font-size: .82rem; font-weight: 700; margin-bottom: 6px;
}
.pay-choice .pay-option {
  display: flex; align-items: center; gap: 10px;
  /* A radio is a tap target, and 44px is this site's floor for one. */
  min-height: 44px; margin: 0; padding: 0 12px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: rgba(255,255,255,.75); cursor: pointer;
  font-weight: 400;
}
.pay-choice .pay-option + .pay-option { margin-top: 8px; }
.pay-choice .pay-option input {
  width: auto; min-width: 0; padding: 0; margin: 0; accent-color: var(--sage-dark);
}
.pay-choice .pay-option:focus-within { outline: 2px solid var(--sage); outline-offset: 1px; }

/* SA-52: the catalogue pager. A 44px target either side even on a desktop --
   these are the two controls that move somebody through 1,329 designs. */
.cat-pager {
  display: flex; align-items: center; justify-content: center; gap: 22px;
  flex-wrap: wrap; margin: 40px 0 8px;
}
.cat-pager a, .cat-pager .off {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0 20px; border: 1px solid var(--line); border-radius: 999px;
  font-size: .88rem; text-decoration: none; color: var(--ink);
  background: rgba(255, 255, 255, .55);
}
.cat-pager a:hover { border-color: var(--sage-dark); color: var(--sage-dark); }
/* Present but plainly unavailable, rather than absent: a control that vanishes
   at the ends makes the row jump as you page. */
.cat-pager .off { opacity: .38; }
.cat-pager .where { font-size: .85rem; color: var(--muted); }
.cat-pager .where small { color: var(--muted); opacity: .8; }

/* ── SA-53: every card shows the design ON A WALL ────────────────────────────
 *
 * The catalogue was 1,329 swatches -- pictures of paper -- on a site whose
 * pitch is "see it on a wall before you print it". The room shot sits under
 * the swatch and the swatch lifts away.
 *
 * Inside `(hover: hover)` on purpose: a rule that does not match is never
 * fetched, so a phone downloads none of these (the SA-2 rule). A touch device
 * gets the swatch and a tap that follows the link, which is what it wants.
 */
.catalog-card .art { position: relative; overflow: hidden; }
.catalog-card .art .flat { display: block; width: 100%; height: 100%;
                           object-fit: cover; }

@media (hover: hover) {
  .catalog-card .art.has-room {
    background-image: var(--room-full);
    /* A non-retina screen takes the 700px file where image-set is supported. */
    background-image: image-set(var(--room-small) 1x, var(--room-full) 2x);
    background-size: cover;
    background-position: center;
  }
  .catalog-card .art.has-room .flat { transition: opacity .45s ease; }
  .catalog-card:hover .art.has-room .flat,
  .catalog-card:focus-within .art.has-room .flat { opacity: 0; }
}

/* The label reads BEFORE anything is hovered, so the card says it has more to
   show without demanding a pointer first. */
.catalog-card .art .peek {
  position: absolute; left: 10px; bottom: 10px; z-index: 2;
  background: rgba(28, 30, 24, .78); color: #fff;
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  padding: 5px 10px; border-radius: 999px; pointer-events: none;
}
@media (hover: hover) {
  .catalog-card .art .peek { opacity: .82; transition: opacity .3s; }
  .catalog-card:hover .art .peek { opacity: 0; }
}

/* Anything animating opacity gets the same courtesy the rest of the site does. */
@media (prefers-reduced-motion: reduce) {
  .catalog-card .art.has-room .flat,
  .catalog-card .art .peek { transition: none; }
}

/* -- SA-54: the quote dialog, in V2's own hand --------------------------------
   `quote.js` is shared machinery, not branding: it builds a <dialog class=
   "quote"> and every rule it reaches for lives in smartart.css, which V2 must
   NEVER load (the SA-4 rule). So the form opened at browser defaults: the
   design thumbnail at full width, pushing every field below the fold, and a
   bare browser button for the close control.

   The SA-26 guard could not see this one. It walks the classes V2 RENDERS in
   its templates, following includes, and this markup is built in JavaScript,
   so no template carries a single one of these class names.

   Everything is scoped under `dialog.quote` on purpose. `.field`, `.btn` and
   `.opt` are generic, and V2 already styles two of them for its contact form
   where `.field` is a PILL INPUT: unscoped, it would draw a rounded bordered
   box around each field GROUP. Scoping to (0,2,1) also beats those cleanly
   rather than by source order.

   V1's tokens (--brass, --bone-2, --rule, --ink-soft, --white) do not exist
   here, and an undefined custom property makes the WHOLE declaration invalid
   (SA-26), so every one is mapped: brass to sage-dark, bone-2 to linen, white
   to #fff, rule to the line colour, ink-soft to muted. */
dialog.quote {
  border: 0; padding: 0; width: min(660px, calc(100vw - 32px));
  max-height: min(88vh, 900px); background: var(--paper); color: var(--ink);
  border-radius: var(--radius-sm); box-shadow: 0 30px 90px rgba(39,40,35,.42);
  font-family: var(--sans);
}
dialog.quote::backdrop { background: rgba(39,40,35,.72); backdrop-filter: blur(3px); }
dialog.quote form { display: flex; flex-direction: column; max-height: inherit; }

dialog.quote .q-head { padding: 24px 30px 0; }
dialog.quote .q-head .top { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; }
dialog.quote .q-head .eyebrow { color: var(--sage-dark); }
dialog.quote .q-head h2 { font-family: var(--serif); font-size: 1.75rem; margin: 10px 0 4px; }
dialog.quote .q-head .sub { color: var(--muted); font-size: .88rem; margin: 0; }
dialog.quote .q-close {
  background: none; border: 0; cursor: pointer; font-size: 26px; line-height: 1;
  color: var(--muted); padding: 6px 8px; border-radius: 999px;
}
dialog.quote .q-close:hover { color: var(--ink); background: var(--linen); }

/* The design she chose, carried in so nobody retypes its name. The picture is
   a CHIP: unstyled it rendered at the dialog's full width and put every field
   below the fold, which is what "the popup looks broken" actually was. */
dialog.quote .q-chip {
  display: flex; align-items: center; gap: 14px; margin-top: 20px; padding: 12px;
  background: var(--linen); border: 1px solid var(--line); border-radius: 14px;
}
dialog.quote .q-chip img {
  width: 62px; height: 48px; flex: 0 0 62px; object-fit: cover; border-radius: 8px;
}
dialog.quote .q-chip .t { font-family: var(--serif); font-size: 1.1rem; line-height: 1.2; }
dialog.quote .q-chip .c {
  font-size: .66rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted); margin-top: 4px;
}

dialog.quote .q-steps { display: flex; gap: 8px; padding: 20px 30px 0; }
dialog.quote .q-steps .s { flex: 1; }
dialog.quote .q-steps .s .bar { height: 2px; background: var(--line); border-radius: 2px; }
dialog.quote .q-steps .s.done .bar,
dialog.quote .q-steps .s.now .bar { background: var(--sage-dark); }
dialog.quote .q-steps .s .lbl {
  font-size: .64rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); margin-top: 8px;
}
dialog.quote .q-steps .s.now .lbl { color: var(--ink); }

dialog.quote .q-body { padding: 26px 30px; overflow-y: auto; flex: 1; }
dialog.quote .q-panel { display: none; }
dialog.quote .q-panel.active { display: block; animation: qfade .22s ease; }
@keyframes qfade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

dialog.quote .q-legend { font-family: var(--serif); font-size: 1.3rem; margin: 0 0 4px; }
dialog.quote .q-note { color: var(--muted); font-size: .85rem; margin: 0 0 22px; }

dialog.quote .field { margin-bottom: 18px; width: auto; border: 0; background: none; border-radius: 0; }
dialog.quote .field > label,
dialog.quote .fieldset > legend {
  display: block; font-size: .72rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 8px; padding: 0; font-weight: 700;
}
dialog.quote .field .req { color: var(--sage-dark); }
dialog.quote .field input[type="text"], dialog.quote .field input[type="email"],
dialog.quote .field input[type="tel"], dialog.quote .field input[type="number"],
dialog.quote .field textarea, dialog.quote .field select,
dialog.quote .field input[type="file"] {
  width: 100%; padding: 12px 14px; border: 1px solid var(--line); border-radius: 12px;
  background: #fff; font-family: var(--sans); font-size: .95rem; color: var(--ink);
}
dialog.quote .field textarea { min-height: 92px; resize: vertical; }
dialog.quote .field input:focus, dialog.quote .field textarea:focus,
dialog.quote .field select:focus { border-color: var(--sage); outline: 2px solid var(--sage); outline-offset: 1px; }
dialog.quote .field input[aria-invalid="true"],
dialog.quote .field textarea[aria-invalid="true"] { border-color: #a33b2b; }
dialog.quote .field .err { color: #a33b2b; font-size: .78rem; margin-top: 6px; display: none; }
dialog.quote .field .err.show { display: block; }

dialog.quote .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
dialog.quote .fieldset { border: 0; margin: 0 0 18px; padding: 0; }
dialog.quote .opts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
dialog.quote .opts.two { grid-template-columns: 1fr 1fr; }
dialog.quote .opt {
  display: flex; align-items: center; gap: 9px; padding: 11px 12px;
  border: 1px solid var(--line); border-radius: 12px; cursor: pointer;
  font-size: .85rem; background: #fff;
}
dialog.quote .opt:hover { border-color: var(--sage); }
dialog.quote .opt input { margin: 0; accent-color: var(--sage-dark); }
dialog.quote .opt:has(input:checked) { border-color: var(--sage-dark); background: var(--sage-soft); }

dialog.quote .q-area {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding: 14px 16px; background: var(--linen); border: 1px solid var(--line);
  border-radius: 14px; margin-bottom: 6px;
}
dialog.quote .q-area .n { font-family: var(--serif); font-size: 1.6rem; }
dialog.quote .q-area .n small { font-size: .88rem; color: var(--muted); }
dialog.quote .q-area .k {
  font-size: .66rem; letter-spacing: .16em; text-transform: uppercase; color: var(--muted);
}
dialog.quote .q-seam {
  display: none; gap: 10px; align-items: flex-start; font-size: .82rem; color: var(--ink);
  background: var(--sage-soft); border-left: 2px solid var(--sage-dark);
  padding: 12px 14px; margin-bottom: 18px; border-radius: 0 12px 12px 0;
}
dialog.quote .q-seam.show { display: flex; }

dialog.quote .q-review { border-top: 1px solid var(--line); }
dialog.quote .q-review .r {
  display: flex; gap: 16px; padding: 11px 0; border-bottom: 1px solid var(--line); font-size: .88rem;
}
dialog.quote .q-review .r .k { color: var(--muted); min-width: 150px; }
dialog.quote .q-review .r .v { flex: 1; }

dialog.quote .q-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 18px 30px; border-top: 1px solid var(--line); background: var(--linen);
}
dialog.quote .q-foot .spacer { flex: 1; }
/* SA-63: the last sage button on the site, and it is the most important
   action on it -- Continue, then Send request. She asked for one button
   treatment in white on charcoal, and this one was scoped inside the
   dialog so the SA-62 change never reached it. White on `--sage` also
   measures 4.27, which SA-22b already recorded as too low. */
dialog.quote .q-foot #q-next { background: var(--charcoal); color: #fff; border-color: var(--charcoal); }
dialog.quote .q-foot #q-next:hover { background: var(--charcoal-dark); border-color: var(--charcoal-dark); }

dialog.quote .q-sent { text-align: center; padding: 16px 0 6px; }
dialog.quote .q-sent .tick { font-size: 40px; color: var(--sage-dark); font-family: var(--serif); }
dialog.quote .q-sent h3 { font-family: var(--serif); font-size: 1.6rem; margin: 12px 0 8px; }
dialog.quote .q-sent p { color: var(--muted); margin: 0 auto 22px; max-width: 44ch; font-size: .9rem; }
dialog.quote .q-sent .row { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

@media (max-width: 560px) {
  dialog.quote { width: 100vw; max-width: 100vw; max-height: 100dvh; height: 100dvh;
                 border-radius: 0; }
  dialog.quote .q-head, dialog.quote .q-body, dialog.quote .q-foot,
  dialog.quote .q-steps { padding-left: 18px; padding-right: 18px; }
  dialog.quote .row2 { grid-template-columns: 1fr; gap: 0; }
  dialog.quote .opts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* iOS zooms the page on focus when an input is under 16px (the SA-2 rule),
     and this form is where every enquiry on the site is captured. */
  dialog.quote .field input, dialog.quote .field textarea,
  dialog.quote .field select { font-size: 16px; }
}

/* The five states the form can be in that are not the form itself: the Back
   button, a send in flight, a send that failed, an oversize attachment, and
   the reference number on success. `.q-error` is the one that matters most --
   with no rule at all it is an unstyled div, so a send that failed says so in
   bare browser text under a form that still looks ready. */
dialog.quote .btn.ghost { background: transparent; color: var(--ink); border-color: var(--line); }
dialog.quote .btn.ghost:hover { background: var(--ink); color: var(--cream); border-color: var(--ink); }
dialog.quote .btn[aria-busy="true"] { opacity: .6; pointer-events: none; }

dialog.quote .q-error {
  display: none; margin: 16px 0 0; padding: 12px 14px; border-radius: 12px;
  border: 1px solid #b4553f; background: #fdf3f0; color: #8c3a28; font-size: .88rem;
}
dialog.quote .q-error.show { display: block; }

dialog.quote .q-file-note { font-size: .78rem; color: var(--muted); margin-top: 6px; }
dialog.quote .q-file-note.bad { color: #a6442f; }

dialog.quote .q-sent .ref {
  font-family: var(--serif); font-size: 1.9rem; letter-spacing: .02em;
  color: var(--sage-dark); margin: 14px 0 6px;
}
dialog.quote .q-sent .ref small {
  display: block; font-family: var(--sans); font-size: .72rem;
  letter-spacing: .16em; text-transform: uppercase; color: var(--muted);
}

/* ── SA-55: the hero is a live wall ──────────────────────────────────────────
   Frames sit UNDER the badges and the caption and OVER the painted background,
   so the stored still is what paints first and what shows with no JavaScript.
   Only opacity is animated: a cross-fade that also moves or scales forces a
   repaint of a full-bleed photograph every frame. */
.hero-slides {
  position: absolute; inset: 0; overflow: hidden; z-index: 0;
  border-radius: inherit;
}
.hero-slide {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  opacity: 0; transition: opacity .9s ease;
  will-change: opacity;
}
.hero-slide.on { opacity: 1; }

/* SA-97: the WORDING changes with the picture, so it cross-fades with it
   rather than snapping to the next headline mid-fade. The script's 260ms
   matches this duration -- change one, change both.
   NOT on `.hero-copy` itself: the SCRIM is painted on that element, so fading
   it would take the wash off the photograph for a quarter of a second every
   five seconds and read as a flicker. Only the three parts that actually
   change are faded, so the pill above them stays put as well. */
.hero-copy > h1,
.hero-copy > p,
.hero-copy > .hero-actions { transition: opacity .26s ease; }
.hero-copy.swapping > h1,
.hero-copy.swapping > p,
.hero-copy.swapping > .hero-actions { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .hero-copy > h1, .hero-copy > p,
  .hero-copy > .hero-actions { transition: none; }
}
/* Raise the badges over the reel WITHOUT touching `position`. The first
   version said `position: relative`, which took them out of the absolute
   positioning `.hero-badges{position:absolute;right:26px}` gives them: the
   three chips left the top-right corner and stacked down the middle of the
   picture. Only the layer needed changing. */
.hero-image > .hero-badges { z-index: 2; }

/* The design's name, over the bottom-left of the picture. It is a LINK, so
   somebody who likes what the hero is showing can go straight to it -- a hero
   that cycles through work you cannot reach is decoration. */
/* TOP-left, not bottom-left. `.product-float` pulls the five cards up over
   the foot of the hero (`margin-top:-42px`), so a caption sitting 22px from
   the bottom was sliced in half by them at every width. The badges are
   top-RIGHT, so the top-left corner of the picture is the one place on it
   that nothing else claims. */
.hero-tag {
  position: absolute; left: 22px; top: 22px; z-index: 2;
  display: inline-flex; flex-direction: column; gap: 1px;
  padding: 11px 16px; border-radius: 14px;
  /* SA-63: .62 put the 10px eyebrow and the 12px range name at 4.33 against a
     pale room shot, under the 4.5 small-text floor. The backdrop blur helps in
     reality and cannot be measured, so the scrim carries it. */
  background: rgba(39, 40, 35, .74);
  backdrop-filter: blur(6px);
  color: #fff; text-decoration: none;
  max-width: calc(100% - 44px);
  animation: tagIn .5s ease both;
}
.hero-tag:hover { background: rgba(39, 40, 35, .78); }
.hero-tag small {
  font-size: .6rem; letter-spacing: .18em; text-transform: uppercase;
  opacity: .92;
}
.hero-tag b {
  font-family: var(--serif); font-size: 1.05rem; line-height: 1.25;
  /* A stock title can run to eight lines (SA-37); a design name is short, but
     her catalogue is 1,329 rows and one of them will not be. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.hero-tag span { font-size: .72rem; opacity: .9; }
@keyframes tagIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

@media (max-width: 900px) {
  .hero-tag { left: 14px; top: 14px; padding: 9px 13px; }
  .hero-tag b { font-size: .95rem; }
}

/* ── SA-55: sections arrive rather than appear ───────────────────────────── */
.js-rise [data-rise] {
  opacity: 0; transform: translateY(18px);
  transition: opacity .7s ease, transform .7s cubic-bezier(.22, .78, .3, 1);
}
.js-rise [data-rise].risen { opacity: 1; transform: none; }

/* Honoured HERE rather than in the script, so a visitor who has asked for less
   motion still gets the class and still sees the page. The fault to avoid is a
   reveal that never fires and leaves the content invisible. */
@media (prefers-reduced-motion: reduce) {
  .js-rise [data-rise] { opacity: 1; transform: none; transition: none; }
  .hero-slide { transition: none; }
  .hero-tag { animation: none; }
}

/* The five cards over the hero, and the range tiles, lift on a pointer device.
   Inside `(hover: hover)` so iOS sticky-hover cannot half-apply a desktop
   state (the SA-5 rule). */
@media (hover: hover) {
  .product-card, .style-card, .project-card {
    transition: transform .28s ease, box-shadow .28s ease;
  }
  .product-card:hover, .style-card:hover, .project-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 18px 40px rgba(61, 54, 41, .16);
  }
}

/* ── SA-56: the catalogue and the shop stay TWO-UP on a phone ───────────────
   Her export drops `.catalog-grid` to one column under 500px, which was right
   for the eight cards it shipped with. Measured on the live site it is not:

       /designs   48 designs   29,954px      two-up  14,209px
       /shop      19 products  19,779px      two-up  10,732px

   Thirty thousand pixels is not a design decision, it is a page nobody reaches
   the end of. Two-up is what a catalogue on a phone normally is, and measured
   at 390px everything still fits: the name, the range, the price, the clamped
   description and a full-width "Request a quote" / "Add to basket". */
@media (max-width: 500px) {
  .catalog-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  /* Her 500px rule sets 1.5rem, which was for a full-width card. */
  .catalog-card h3 { font-size: 1.05rem; margin: 8px 0 6px; }
  .catalog-body { padding: 12px; }
}

/* ── SA-57: the live Google reviews, under the band ──────────────────────────
   `.rev-list` and `.rev-card` already exist for the SA-50 stored reviews, so
   only the pieces Google's attribution rules add are new: the profile link,
   the relative date, the "Read on Google" footer and the ordering notice. */
.rev-live { margin-top: 26px; }
.rev-live-head {
  font-family: var(--serif); font-size: 1.35rem; margin: 0 0 16px;
}
.rev-live-note {
  margin: 16px 0 0; font-size: .78rem; color: var(--muted); max-width: 62ch;
}

.rev-card .rev-head a {
  color: inherit; text-decoration: none; border-bottom: 1px solid var(--line);
}
.rev-card .rev-head a:hover { border-bottom-color: currentColor; }

.rev-when {
  display: block; font-size: .72rem; color: var(--muted); margin-top: 2px;
}

.rev-foot {
  display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap;
  margin-top: 12px; font-size: .76rem;
}
.rev-foot a { color: var(--sage-dark); font-weight: 700; }
/* "Read on Google" is a standalone action at the foot of a card, not a link
   inline in a sentence, so WCAG 2.5.5's inline exemption does not cover it --
   measured at 93x19 on a phone. The site's own 44px touch floor applies. */
@media (max-width: 1000px) {
  .rev-foot a {
    display: inline-flex; align-items: center; min-height: 44px;
    padding-right: 8px;
  }
}
.rev-translated { color: var(--muted); }

/* An avatar that fails to load must not leave a broken-image glyph in the
   middle of a testimonial. */
.rev-card .rev-head img {
  width: 40px; height: 40px; flex: 0 0 40px;
  border-radius: 50%; object-fit: cover; background: var(--linen);
}

/* ── SA-58: the reviews spin, slowly ─────────────────────────────────────────
   A SCROLL-SNAP track, not a transform carousel. Native touch, native
   keyboard, native focus scrolling and no layout arithmetic to get wrong at a
   breakpoint -- autoplay is one `scrollBy`. A transform carousel would have to
   reimplement all four, and this is a band that must survive any number of
   cards: five from the Places API today, ten or twenty if her Business Profile
   connects (SA-57).

   The grid rule above still applies when the band is NOT a spinner, which is
   the SA-50 stored-reviews path. Only `.rev-live .rev-list` becomes a track. */
.rev-live .rev-list {
  display: flex;
  gap: 16px;
  margin-top: 22px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* Room for the card shadow, and space for the last card to snap flush. */
  padding: 4px 4px 10px;
  /* A scrollbar under a testimonial row is noise; the arrows and dots say it
     moves, and touch has its own affordance. */
  scrollbar-width: none;
}
.rev-live .rev-list::-webkit-scrollbar { display: none; }

.rev-live .rev-card {
  scroll-snap-align: start;
  /* THREE across at desktop, two on a tablet, one on a phone -- set here
     rather than in a media query so the card is a fraction of whatever width
     the track actually has, which is what makes the same rule right inside a
     narrow column too. `min()` stops a single card from being wider than the
     track on a phone. */
  flex: 0 0 min(100%, calc((100% - 32px) / 3));
  min-width: 0;
}
@media (max-width: 1050px) {
  .rev-live .rev-card { flex-basis: min(100%, calc((100% - 16px) / 2)); }
}
@media (max-width: 700px) {
  .rev-live .rev-card { flex-basis: 100%; }
  /* One card per view, so matching every card to the TALLEST one just draws a
     short review inside a box with 250px of nothing under it. Let each card
     end where its text ends; the track keeps its own height, so the dots below
     do not move as you swipe. */
  .rev-live .rev-list { align-items: flex-start; }
  .rev-live .rev-card blockquote { flex: 0 1 auto; }
}

.rev-live-top {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}

.rev-nav { display: flex; gap: 8px; }
.rev-nav button {
  width: 40px; height: 40px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: #fff; color: var(--ink);
  font-size: 1rem; line-height: 1; display: grid; place-items: center;
  transition: background .2s ease, border-color .2s ease;
}
.rev-nav button:hover { background: var(--linen); border-color: var(--sage); }
/* Disabled rather than hidden: a control that disappears at the end of a track
   moves everything beside it, which reads as a glitch. */
.rev-nav button[disabled] { opacity: .38; cursor: default; }
.rev-nav button[disabled]:hover { background: #fff; border-color: var(--line); }
@media (max-width: 1000px) { .rev-nav button { width: 44px; height: 44px; } }

.rev-dots { display: flex; gap: 7px; justify-content: center; margin-top: 14px; }
.rev-dots button {
  /* 22px of transparent target around a 7px mark: the dot has to be small to
     read as a dot and the TARGET has to clear WCAG 2.5.8 either way. */
  width: 22px; height: 22px; padding: 0; border: 0; background: none;
  cursor: pointer; display: grid; place-items: center;
}
.rev-dots button::before {
  content: ""; width: 7px; height: 7px; border-radius: 999px;
  background: var(--line); transition: background .2s ease, transform .2s ease;
}
.rev-dots button[aria-current="true"]::before {
  background: var(--sage-dark); transform: scale(1.35);
}
@media (max-width: 1000px) { .rev-dots button { width: 44px; height: 44px; } }

/* A card of equal height whatever the review's length, so the row does not
   step up and down as it moves.

   `position: relative` is NOT cosmetic. `.visually-hidden` (the "4 out of 5"
   label behind the star glyphs) is `position: absolute`, and with no
   positioned ancestor its containing block is the PAGE -- so sitting inside
   card five at x=1869 it added 469px to the document's scrollable width. The
   cards only sit off-screen once they are in a horizontal scroller, which is
   why this appeared with the spinner and not before. Relative makes the card
   the containing block, and the card is clipped by the track. */
.rev-live .rev-card { display: flex; flex-direction: column; position: relative; }
.rev-live .rev-card blockquote { flex: 1; }

@media (prefers-reduced-motion: reduce) {
  .rev-live .rev-list { scroll-behavior: auto; }
}


/* ------------------------------------------------------------------ SA-62
   The hero is one full-bleed picture with the wording over it, and the page
   is white with charcoal buttons. Both are Theresa's calls, 7 Sep 2026.

   On the hero the button inverts: a charcoal pill on a dark photograph is
   the one place her uniform treatment cannot be read, so it becomes the same
   two colours the other way round, exactly as the olive CTA band already
   does. No third colour is introduced.                                     */
.hero .hero-actions .btn { background: #fff; color: var(--charcoal);
  border-color: #fff; }
.hero .hero-actions .btn:hover { background: #fff; }
.hero .hero-actions .btn.btn-primary { background: #fff; color: var(--charcoal);
  border-color: #fff; }
.hero .hero-actions .btn.btn-primary:hover { background: #fff; }
.hero .hero-actions .btn:not(.btn-primary) { background: transparent;
  color: #fff; border-color: rgba(255, 255, 255, .55); }
.hero .hero-actions .btn:not(.btn-primary):hover {
  background: rgba(255, 255, 255, .12); border-color: #fff; }

/* The badges keep the top right of the picture. The reel caption used to have
   the top left; the wording claims that corner now, so it moves to the bottom
   right and clears `.product-float`, which pulls the five cards up over the
   foot of the hero by 62px. */
.hero-tag { left: auto; top: auto; right: 26px; bottom: 84px; }

@media (max-width: 900px) {
  /* A horizontal scrim cannot work once the copy fills the width, so it turns
     vertical and the wording sits at the foot of the picture. */
  .hero-copy { justify-content: flex-end; padding-left: 22px;
    padding-right: 22px; padding-block: 40px 46px;
    background: linear-gradient(rgba(24, 25, 22, .22) 0%,
      rgba(24, 25, 22, .40) 42%, rgba(24, 25, 22, .82) 100%); }
  .hero-copy h1 { max-width: none; }
  .hero-copy p { max-width: none; }
  .hero-tag { right: 14px; bottom: 92px; }
  .hero-badges { top: 20px; right: 14px; }
}

/* SA-62 follow-through. Two things the white ground exposed that the cream
   had been hiding.

   The interior page header was a bordered white card, which on a cream page
   read as a raised panel and on a white one reads as a big empty box with the
   copy in its left third. It is a page header now: the same words, a rule
   underneath, no box.

   And a range with no cover image collapsed its tile entirely. */
.style-blank {
  display: block; width: 100%; aspect-ratio: 1 / 1.12; border-radius: 22px;
  background: var(--linen); border: 1px solid var(--line);
}

/* ── SA-62: the price-promise band ────────────────────────────────────────────
 * Theresa, 7 Sep 2026: "This is a quote so add quote marks, and where possible
 * use our full name. And add owner. Centre text on both sides." Plus her own
 * suggestion of a charcoal board in white text, which is worth taking: it is
 * the one band on a now-white page that carries real weight, and it uses the
 * same two colours as the buttons rather than introducing a third.
 *
 * Measured on #3f403c: white 10.4:1, the body copy at .82 is 6.9:1 and the
 * eyebrow and attribution at .72 are 5.8:1.
 */
.promise2 {
  background: var(--charcoal); color: #fff;
  border: 0; box-shadow: var(--shadow);
  text-align: center; align-items: center; justify-items: center;
}
.promise2 .eyebrow { color: rgba(255, 255, 255, .72); }
.promise2 h2 { color: #fff; }
.promise2 > div > p { color: rgba(255, 255, 255, .82); max-width: 48ch; }

/* The left rule was a left-aligned device and the band is centred now. */
.pull2 { border-left: 0; padding-left: 0; text-align: center; }
.pull2-text { margin: 0; }
/* Drawn here rather than typed into the field, so rewording the quote in the
   portal can never leave it unpunctuated or punctuated twice. */
.pull2-text::before { content: "\201C"; }
.pull2-text::after  { content: "\201D"; }
.pull2 cite { color: rgba(255, 255, 255, .72); }

/* ── SA-62: the home page shows client marks, not job photographs ──────────── */
.home-brands { margin-top: 26px; }
@media (max-width: 900px) {
  .promise2 > div > p { max-width: none; }
}

/* ── SA-62: her rating in the top bar ─────────────────────────────────────────
 * "More prominent at the top as per our old site ... this can move to this
 * banner." The bar is `space-between`, so a third item lands centred between
 * the note and the links with no layout change.
 *
 * The stars are her orange lifted toward legibility. Her orange itself sits at
 * 3.40 on the charcoal bar, which clears WCAG 1.4.11's 3:1 for a graphical
 * object by nothing at all -- and see the rule below for why 3:1 is the wrong
 * bar here. The count beside the stars is --topnote-ink at 6.90.
 */
.top-rating {
  display: inline-flex; align-items: center; gap: 8px; white-space: nowrap;
  /* SA-80b: 24px, WCAG 2.5.8's pointer minimum. It measured 20 tall, and it is
     a standalone control -- a link out to her Google listing, not a link inside
     a sentence -- so 2.5.5's inline exemption does not cover it. The whole bar
     grows by 4px because its siblings stretch, which is the intended shape.
     Fourth instance of this (SA-36's chevron, SA-57's "Read on Google", SA-79's
     reviewer name), and the first one that was on EVERY page of the site. */
  min-height: 24px;
}
.top-rating:hover { text-decoration: underline; }
.top-stars {
  /* SA-63: 3:1 is the GRAPHICAL-object floor and it is the wrong bar here.
     These stars are not decorative: with the figure hidden (her ask) they are
     the only thing on the bar carrying the rating, so SA-21's rule applies and
     they need 4.5 as TEXT.

     SA-85: this was `color-mix(var(--orange) 40%, #fff)`, a fixed lift tuned
     against the olive bar. The right percentage is a property of the
     BACKGROUND, and the background moved -- so it is derived per bar colour
     now and lands at 4.67 on her charcoal, using LESS lift than the olive
     needed and therefore staying closer to her actual orange. On a pale bar it
     blends toward black instead, so the row cannot go invisible if she picks
     one. The value comes in on the body; see `sa_bar_colors`. */
  color: var(--topnote-stars);
  letter-spacing: .12em; font-size: .82rem;
}

/* ── SA-62: the interior page header ──────────────────────────────────────────
 * It was a bordered white card holding an eyebrow, a heading and a line of
 * copy. On the cream page that read as a raised panel; on a white one it is a
 * big empty box with the words in its left third, which is the same "big
 * spaces" complaint in a different place. It is a page header now: the same
 * words, a rule underneath, no box.
 *
 * Declared last and in one place, so nothing further up the file can put the
 * card padding back.
 */
.page-hero { padding: 52px 0 26px; }
.page-hero .box {
  padding: 0 0 28px; border-radius: 0; background: none;
  border: 0; border-bottom: 1px solid var(--line); box-shadow: none;
}
.page-hero .box > * { position: relative; z-index: 1; }
.page-hero h1 { margin: 10px 0 0; max-width: 20ch; }
/* `pretty` keeps the last line off a single orphaned word. Her intro is
   just over the measure and was breaking as "...one working / day." */
.page-hero p { max-width: 62ch; text-wrap: pretty; }
@media (max-width: 780px) {
  .page-hero { padding: 30px 0 18px; }
  .page-hero .box { padding: 0 0 20px; }
  .page-hero h1 { max-width: none; }
}

/* ── SA-62: a group's own row in the phone sheet ──────────────────────────────
 * The sheet sizes its rows with `.main-nav > a`, and a group's link is
 * `.main-nav > .navgroup > a`, so it never matched: Products and Artists
 * rendered at the bar's .86rem while every other row was 1rem, and the two
 * group rows read as a different, smaller kind of item.
 *
 * SA-34 recorded this exact trap for the group's LAYOUT ("`.main-nav > a` never
 * matched it") and the typography was left behind. It has been live on Artists
 * since then and only became obvious with two groups in the menu.
 */
@media (max-width: 780px) {
  .main-nav > .navgroup > a {
    font-size: 1rem; padding: 11px 0; min-height: 44px;
    display: flex; align-items: center;
  }
  /* And the separating rule, for the same reason: it is on `.main-nav > a`,
     so the two group rows were the only ones in the sheet without one and the
     rhythm went uneven every second row. It sits on the GROUP rather than on
     its link, so an open panel is inside the row it belongs to. */
  .main-nav > .navgroup { border-bottom: 1px solid var(--line); }
  /* The rows inside an open panel stay one step down, so the panel reads as
     belonging to the row above it rather than as more top-level items. */
  .main-nav > .navgroup > .navdrop a { font-size: .92rem; }
}

/* ── SA-63: the contact and checkout forms ───────────────────────────────────
 * "Looks weird on web and mobile." It did: every field group was drawing a
 * pill AND the input inside it was drawing its own box, so each field read as
 * two nested outlines with the label floating between them. The cause is fixed
 * above by scoping her export's `.field` pill to an actual input; this is the
 * tidy-up that goes with it.
 *
 * The input takes --linen rather than white. On the cream page a white field
 * on a cream card read as a well; on a white card white-on-white left only a
 * hairline, which is the same thing that made four other elements vanish in
 * SA-62. A form field should look like somewhere to type.
 */
.field input, .field textarea, .field select {
  background: var(--linen);
  border-radius: 14px;
  padding: 13px 15px;
  transition: border-color .15s, background .15s;
}
.field input:hover, .field textarea:hover, .field select:hover {
  border-color: rgba(39, 40, 35, .28);
}
.field input:focus, .field textarea:focus, .field select:focus {
  background: #fff;
  border-color: var(--sage);
}
.field label { color: var(--ink); letter-spacing: .01em; }
.field textarea { min-height: 130px; resize: vertical; }

/* A pair of panels rather than two inline styles repeated in the template. */
.contact-layout > .paper-surface { padding: 30px; border-radius: var(--radius); }
/* The form's first row of labels was sitting directly under the heading with
   nothing between them, while the studio column had room. Both get it here. */
.contact-layout h2 { margin-bottom: 4px; }
.contact-layout form { margin-top: 22px; }
.contact-layout form .btn { margin-top: 8px; }

/* The studio column is a short list of facts, so it wants rhythm rather than
   paragraphs of equal weight. */
.contact-lines a { font-size: 1rem; }
.contact-layout .muted { color: var(--muted); }

@media (max-width: 780px) {
  .contact-layout > .paper-surface { padding: 22px; }
  .field { margin-bottom: 12px; }
  .field textarea { min-height: 112px; }
}

/* ── SA-64: the artwork pop-up has three actions now ─────────────────────────
 * Theresa's framing for the whole site is a gallery where a visitor imagines
 * the art in a room, so the pop-up leads with that rather than with the price.
 * Three identical white pills is no hierarchy at all; one primary and two
 * outlines says which one to press first. Flipping it so the quote leads is
 * one class if she would rather sell harder.
 */
.artpop-acts #artpop-wall {
  background: var(--charcoal); color: #fff; border-color: var(--charcoal);
}
.artpop-acts #artpop-wall:hover {
  background: var(--charcoal-dark); border-color: var(--charcoal-dark);
}

/* ── SA-66: open the design a range tile is showing ──────────────────────────
 * Theresa's framing is a gallery where a visitor imagines the art in a room.
 * A range tile already shows one of her designs on a wall; until now that was
 * a picture and nothing else, and the only way in was to browse the range and
 * hope to find it again.
 *
 * The chip is a SIBLING of the tile, not a child: an <a> inside an <a> is
 * invalid and the browser unnests it, which silently loses one of the links.
 * So the cell takes the grid slot and positions both.
 */
.style-cell { position: relative; min-width: 0; display: block; }
.style-cell > .style-card { width: 100%; }

.cat-try {
  position: absolute; top: 10px; right: 10px; z-index: 2;
  display: inline-flex; align-items: center;
  padding: 6px 11px; border-radius: 999px;
  background: rgba(255, 255, 255, .94);
  border: 1px solid var(--line);
  box-shadow: 0 4px 12px rgba(39, 40, 35, .14);
  font-size: .68rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--charcoal);
  white-space: nowrap;
}
.cat-try:hover { background: var(--charcoal); color: #fff;
  border-color: var(--charcoal); }
.cat-try:focus-visible { outline: 2px solid var(--sage); outline-offset: 2px; }

/* The label sits under the picture on the home band, so the chip keeps to the
   picture rather than floating over the range name. */
.style-cell .cat-try { top: 8px; right: 8px; }

/* ── SA-91: the range filters, on the home band's component ──────────────────
 * Theresa asked for `/designs` to look like the home page's "Explore by
 * Style" band, and she is right: these are her ranges in both places, so two
 * treatments of them is two things to keep in step and one more thing for a
 * visitor to decode. The tiles now use `.style-cell` / `.style-card`
 * unchanged, and the SA-30 scrim is retired -- with the label UNDER the
 * picture there is nothing to wash over the photograph.
 *
 * Only two things differ, and both are because this is a FILTER rather than a
 * link to a page:
 */

/* 1. DENSITY. The home band shows eight and steps down to one column on a
 *    phone; this page shows all twenty-two, and one column would be twenty-two
 *    rows before the first design.
 *
 *    EIGHT across, which is the home band's own density, is not a coincidence
 *    and was measured: at six the band was 1,266px before the first design,
 *    against 598px for the flat tiles it replaces. At eight it is ~600px, so
 *    her ranges get the treatment she asked for and the catalogue does not
 *    move down the page to pay for it. */
/*    SCOPED `.style-grid.cat-filter`, which is (0,2,0). A bare `.cat-filter`
 *    is (0,1,0) and TIES with `.style-grid`, so SA-79's phone block further
 *    down this file -- which drops the home band to two columns -- silently
 *    won and gave this page a 5,780px filter band at 760px. That is the trap
 *    this stylesheet keeps recording: a rule appended after a media query
 *    undoes it at equal specificity (SA-13, SA-19, SA-24, SA-36). Winning on
 *    specificity survives the next append; winning on order does not. */
.style-grid.cat-filter { grid-template-columns: repeat(8, minmax(0, 1fr)); }
@media (max-width: 1050px) { .style-grid.cat-filter { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (max-width: 780px)  { .style-grid.cat-filter { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 500px)  { .style-grid.cat-filter { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* The home band hides tiles past a count on small screens. A FILTER may never
   hide an option: that would strand somebody with no way to the range they
   want (the SA-10 rule about chips never being the only control). */
.style-grid.cat-filter .style-card,
.style-grid.cat-filter .style-cell { display: block; }

/* 2. A SELECTED STATE, which a link to a page does not need. The ring is on
 *    the picture and the label goes to her green, so the choice reads without
 *    relying on colour alone. `outline`, not `border`: a border would resize
 *    the image inside the grid track and shift every neighbouring tile. */
.cat-filter .style-card.on img,
.cat-filter .style-card.on .style-blank {
  outline: 3px solid var(--sage-dark); outline-offset: 2px;
}
.cat-filter .style-card.on strong { color: var(--sage-dark); }
.cat-filter .style-card:focus-visible img,
.cat-filter .style-card:focus-visible .style-blank {
  outline: 3px solid var(--sage); outline-offset: 2px;
}

@media (max-width: 780px) {
  /* SA-67: the tile is 110px tall, so a 44px chip would be 40% of it and would
     cover the picture it is offering. 2.5.5's 44px is AAA; 2.5.8's 24px is the
     AA floor this site targets, and `min-height` puts real headroom over it
     rather than leaving the size to whatever the padding happens to compute.
     The tile itself stays a large target for the browsing action. */
  .cat-try { top: 6px; right: 6px; padding: 8px 11px; font-size: .64rem;
    min-height: 32px; }
}

/* ── SA-67: what she sells on a surface ──────────────────────────────────────
 * A plain list of named products, which is what her own page is. Not cards:
 * there is no picture and no page behind each one yet, and a card promises
 * both. Two or three columns on a desktop so nine entries read as one group
 * rather than a long thin column, one column on a phone.
 */
.surface-types {
  list-style: none; margin: 26px 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0 28px;
}
.surface-types li {
  padding: 15px 4px; border-top: 1px solid var(--line);
  font-family: var(--serif); font-size: 1.12rem; line-height: 1.3;
}
.surface-types-cta { margin-top: 26px; }

@media (max-width: 900px) { .surface-types { grid-template-columns: 1fr 1fr; } }
@media (max-width: 620px) {
  .surface-types { grid-template-columns: 1fr; }
  .surface-types li { padding: 13px 2px; font-size: 1.05rem; }
}

/* ── SA-67: two tap targets the phone audit found ────────────────────────────
 * Privacy Policy and Terms & Conditions sit in the footer's bottom bar as a
 * pair of links separated by a middot, not as links inside a sentence, so
 * WCAG 2.5.5's inline exemption does not cover them and 2.5.8's 24px pointer
 * minimum does. They measured 14px tall on EVERY page. Padding gives them the
 * hit area without moving the type.
 *
 * SA-56 recorded these as exempt. That was the top-bar links, which ARE in a
 * sentence; these are not, and the audit measures them separately now.
 */
.footer-bottom a {
  display: inline-flex; align-items: center;
  min-height: 24px; padding: 5px 2px;
}

/* An artist's name is the way into their page and measured 29px. Standalone,
   so it takes the 44px floor on a phone rather than the 24px one. */
@media (max-width: 1000px) {
  .ar2-row h2 > a {
    display: inline-flex; align-items: center; min-height: 44px;
  }
  .footer-bottom a { min-height: 44px; padding: 11px 2px; }
}

/* ── SA-68: her free preview service on the My Room page ─────────────────────
 * This page IS the service her own site describes: photograph your wall, send
 * it in, and someone edits the picture by hand and emails it back. Saying so
 * is worth more than describing the tool again.
 *
 * The shared partial is styled in smartart.css, which V2 must never load (the
 * SA-4 rule), so anything it renders needs a rule here too.
 */
.wall-free {
  margin: 10px 0 0; font-weight: 700; color: var(--sage-dark);
}
.wall-ways { margin-top: 34px; padding-top: 26px; border-top: 1px solid var(--line); }
.wall-ways h3 { margin: 0 0 18px; }
.wall-ways-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px;
}
.wall-way b { display: block; margin-bottom: 6px; font-size: .96rem; }
.wall-way p { margin: 0; color: var(--muted); font-size: .93rem; line-height: 1.55; }
.wall-was {
  margin: 24px 0 0; padding: 16px 18px; border-radius: var(--radius-sm);
  background: var(--linen); color: var(--muted); font-size: .93rem;
  line-height: 1.55;
}
.wall-was b { color: var(--ink); }

@media (max-width: 820px) {
  .wall-ways-grid { grid-template-columns: 1fr; gap: 18px; }
}

/* ── SA-73: the Journal ───────────────────────────────────────────────────────
 * Every class the two blog templates render. V2 must never load smartart.css
 * (the SA-4 rule), so a class used here and styled only there renders with
 * browser defaults, which is exactly how the reviews band and the quote dialog
 * shipped unstyled (SA-26, SA-54).
 *
 * Only the tokens declared at the top of THIS file are used. An undefined
 * custom property makes the whole declaration invalid and nothing errors, which
 * is how the active filter chip once came out ink on ink.
 */
.journal-grid {
  display: grid; gap: 26px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.journal-card {
  display: flex; flex-direction: column;
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius-sm); overflow: hidden; box-shadow: var(--shadow);
}
/* The RATIO GOES ON THE IMAGE, not on the link. The link is a flex item in a
   column card, so its height is the main size and is computed from its content;
   an `height: 100%` image inside an aspect-ratio box then has nothing definite
   to resolve against and falls back to its intrinsic height. Result: a portrait
   cover made one card twice as tall as its neighbour. */
.journal-cover { display: block; overflow: hidden; background: var(--linen); }
.journal-cover img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 3 / 2; object-fit: cover;
}
/* A post with no cover keeps the card's shape rather than collapsing it, so a
   row of three does not go ragged the day she publishes a text-only piece. */
.journal-blank {
  display: block; width: 100%; aspect-ratio: 3 / 2;
  background:
    radial-gradient(120% 90% at 20% 0%, rgba(83,104,58,.14), transparent 62%),
    var(--linen);
}
.journal-body { padding: 20px 22px 22px; display: flex; flex-direction: column; gap: 10px; }
.journal-body h2 { font-size: 1.18rem; line-height: 1.3; margin: 0; }
/* 24px is WCAG 2.5.8's pointer minimum and an inline anchor's box is its TEXT,
   not its line box, so a one-line title measured 21px. It is a standalone
   control (a card heading), not a link inside a sentence, so 2.5.5's inline
   exemption does not cover it. inline-block is what lets min-height apply. */
.journal-body h2 a {
  color: inherit; text-decoration: none;
  display: inline-block; min-height: 24px;
}
.journal-body h2 a:hover { color: var(--sage-dark); }
.journal-meta {
  margin: 0; font-size: .8rem; color: var(--muted);
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px;
}
.journal-cat {
  text-transform: uppercase; letter-spacing: .14em; font-weight: 700;
  font-size: .68rem; color: var(--sage-dark);
}
.journal-excerpt { margin: 0; color: var(--muted); font-size: .93rem; line-height: 1.6; }

/* A post with a cover opens on it, with the title over a measured scrim. The
   floor of .58 is what keeps a white heading readable over a pale room shot;
   SA-13 measured the same band at .62 and found 4.33 on a light photograph. */
.journal-hero { position: relative; }
.journal-hero img {
  width: 100%; height: min(52vh, 460px); object-fit: cover; display: block;
}
.journal-hero-copy {
  position: absolute; inset: auto 0 0 0; padding: 34px 0 26px; color: #fff;
  background: linear-gradient(to top, rgba(20,21,17,.74), rgba(20,21,17,0));
}
.journal-hero-copy h1 { color: #fff; margin: 6px 0 0; max-width: 20ch; }
.journal-hero-copy .eyebrow { color: #e6ecd8; }

/* Centred, not left. 68ch is the right measure to read at and it is half the
   container, so hard left leaves the page looking like a column failed to
   load. The byline goes with it or it detaches from the piece it belongs to. */
.journal-article, .journal-byline, .journal-back { max-width: 68ch; margin-inline: auto; }
.journal-article { margin-top: 0; }
.journal-article p { line-height: 1.75; margin: 0 0 18px; }
.journal-lead { font-size: 1.1rem; color: var(--ink); font-weight: 500; }
.journal-byline { margin: 0 auto 22px; }
.journal-back { margin: 30px auto 0; }

@media (max-width: 1000px) {
  .journal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
}
@media (max-width: 620px) {
  /* One column here, not two: a card carries an excerpt as well as a title, so
     two-up on a phone leaves about 150px for a sentence. That is the opposite
     call from the catalogue (SA-56), where a card is a picture and a price. */
  .journal-grid { grid-template-columns: 1fr; }
  .journal-hero img { height: 42vh; }
}

/* ── SA-77: centred text that was not centred on the page ─────────────────────
 * `text-align: center` centres the WORDS inside an element's own box. It does
 * nothing about where that box sits. So a paragraph capped at 660px inside a
 * 1150px container reads 245px off centre, with its own text neatly centred
 * inside it -- which is exactly what "the text is not centred" looked like on
 * the home page.
 *
 * THE CAP IS RIGHT AND MUST STAY: 660px is a readable measure and a centred
 * paragraph running the full 1150px would be worse. What was missing is the
 * auto margin that puts the capped box in the middle.
 *
 * Measured across all 17 public pages at 1400px: three elements, all here.
 */
.section-heading.center p { margin-inline: auto; }
.promise2 > div > p { margin-inline: auto; }
.pull2 { margin-inline: auto; }

/* ── SA-78: the bespoke band is a charcoal board ──────────────────────────────
 * "Change this into something more colourful and smart, something that goes
 * with the logo colours." It was grey copy on a grey card beside a washed-out
 * mural, so it read as the quietest thing on a page whose whole argument is
 * colour on a wall.
 *
 * The board is `--charcoal`, which is 67% of the opaque pixels in her logo
 * (SA-17), and the accent is her orange, which is 2% of it and is used here the
 * way her logo uses it: a small bright mark, never a field of text. Measured on
 * the ground it sits on, #ffa038 on #3f403c is 5.1, so it clears AA as the
 * eyebrow. The same treatment the promise band already uses (SA-62), so the
 * page has two dark boards rather than one dark board and one odd one.
 */
.bespoke-brand {
  background: var(--charcoal); color: #fff;
  border: 0; border-radius: var(--radius); box-shadow: var(--shadow);
}
.bespoke-brand h2 { color: #fff; }
.bespoke-brand .eyebrow { color: var(--orange); }
.bespoke-brand .bespoke-copy > p { color: rgba(255, 255, 255, .84); }

/* The picture is a REEL now, so it is a background like the hero rather than an
   <img> centred in a box. The stored still is painted on and the frames layer
   over it, which is what keeps a no-JavaScript visitor from seeing an empty
   panel (SA-55). */
.bespoke-brand .sample-art {
  position: relative; height: auto; min-height: 300px;
  display: block; overflow: hidden;
  border-radius: var(--radius-sm);
  background-size: cover; background-position: center;
}
.bespoke-brand .sample-art .hero-tag { left: 16px; bottom: 16px; top: auto; }

/* The feature list was a white card, which on a dark board reads as a hole cut
   in it. A hairline panel keeps the grouping and stays part of the board. */
.bespoke-brand .feature-list {
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .16);
  border-radius: var(--radius-sm);
}
.bespoke-brand .feature-row { border-bottom-color: rgba(255, 255, 255, .14); }
.bespoke-brand .feature-row b { color: #fff; }
.bespoke-brand .feature-row small { color: rgba(255, 255, 255, .78); }
/* Her orange carrying dark ink, which is how the logo uses it and the one way
   it is readable at all: as text it measures 1.8 on white and 2.0 under white
   (SA-28), and it is 7.3 with dark ink on it. */
.bespoke-brand .icon-circle {
  background: var(--orange); color: #23241f; border: 0;
}

@media (max-width: 780px) {
  .bespoke-brand .sample-art { min-height: 220px; }
}

/* ── SA-79: the phone was shown one product card out of five ──────────────────
 * Her export hides them across THREE media blocks that all still apply at
 * 390px: `nth-child(n+4)` at 1050, `nth-child(3)` at 780 and `nth-child(2)` at
 * 500. Four of the five go, so a phone visitor saw "Wallpaper" and nothing
 * else: not Wood Panels, not Vinyls, not Canvas, not Projects. That row is the
 * five ways into her catalogue and it is the first thing under the hero.
 *
 * It was a reasonable call for the export, whose cards were decoration. Ours
 * are navigation.
 *
 * SPECIFICITY, not source order: `.product-card:nth-child(2)` is (0,2,0), so a
 * bare `.product-card` later in the file loses however far down it sits. These
 * match at (0,2,0) and win on order.
 *
 * Two-up rather than one, which is the SA-56 call for the catalogue and the
 * shop: measured at 390px everything still fits, and one per row turned the
 * style band into 3,515px of page for eight tiles.
 */
@media (max-width: 1050px) {
  .product-grid .product-card { display: block; }
}
@media (max-width: 780px) {
  .product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .product-grid .product-card { display: block; }
  .style-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .style-grid .style-card { display: block; }
}
@media (max-width: 500px) {
  .product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .product-grid .product-card { display: block; }
  .style-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .style-grid .style-card { display: block; }
  /* The cell wrapper carries the grid slot since SA-66, so the blank-tile
     fallback has to come with it or a range with no cover leaves a hole. */
  .style-grid .style-cell { display: block; }
}

/* ── SA-79b: two more inline anchors under the 24px pointer minimum ───────────
 * An inline anchor's box is its TEXT, not its line box, so a one-line name
 * measures whatever the font is. Both of these are standalone controls, not
 * links inside a sentence, so WCAG 2.5.5's inline exemption does not cover
 * them and 2.5.8's 24px applies. Same fix and same reason as the journal card
 * titles (SA-73c): inline-block is what lets min-height apply at all.
 *
 * The reviewer name (17px) links to that person's Google profile, which is
 * attribution Google asks for and therefore has to be tappable. The design
 * page's range name (14px) is how somebody gets from one design back to the
 * rest of its range.
 */
.rev-head b a,
.product-info .eyebrow a { display: inline-block; min-height: 24px; }

/* ══ SA-80: the mobile pass, from real phone screenshots ══════════════════════
 * A 390px harness called all thirty pages clean and a phone showed a headline
 * with a badge printed across it. OVERLAP IS NOT OVERFLOW: two things stacked
 * on each other pass every check this codebase runs, which is why nothing
 * caught it until Theresa opened the site.
 *
 * Measured on the live site at 360, 390 and 412: a badge over the headline by
 * 117x63px at every width, and the reel caption over a hero button by 47px at
 * 360.
 */

@media (max-width: 780px) {

  /* ── 1. The hero stops fighting itself ─────────────────────────────────────
   * `.hero-badges` and `.hero-tag` are both absolutely positioned inside
   * `.hero-image`, and since SA-62 the copy shares that same grid cell. On a
   * desktop there is room either side; on a phone there is one column and they
   * land on the words.
   *
   * The badges go. Their three claims are already the "How we print" section
   * further down, and the top bar above the header carries the fourth. A phone
   * hero is one picture, one headline, one sentence and two buttons.
   */
  .hero-badges { display: none; }

  /* The caption sits BELOW the buttons rather than on them, and the copy is
     padded to leave it room. */
  /* NOT `left: 50%` with a translate. `.hero-tag` carries
     `animation: tagIn .5s both`, and that keyframe ENDS on `transform: none`;
     an animation's applied value beats a normal declaration, so the translate
     was silently dropped and the caption sat half off the right edge. Spanning
     both edges needs no transform at all.

     And it clears the product cards, which float up over the foot of the hero
     by 42px (`.product-float`) and would otherwise sit on it. */
  .hero-tag {
    left: 14px; right: 14px; bottom: 60px; top: auto;
    max-width: none;
    text-align: center; align-items: center;
  }
  .hero-copy { padding-bottom: 150px; }

  /* ── 2. Centred, which is what a phone column wants ────────────────────────
   * One narrow column reads better centred, and the page was mixing the two:
   * "Spaces we're proud of." left and "Brands we have printed for" centred,
   * one under the other.
   *
   * LONG PROSE IS DELIBERATELY LEFT ALONE. A 300-word journal post centred is
   * harder to read, not prettier, because every line starts in a different
   * place and the eye has to hunt for it.
   */
  .hero-copy, .page-hero .box, .bespoke-copy, .section-heading,
  .info-row, .feature-row > div, .product-card .meta, .journal-body,
  .cta-band > div, .promise2 > div, .rev-card, .style-card,
  .surface-types li, .footer-grid > div {
    text-align: center;
  }
  .hero-copy, .page-hero .box, .bespoke-copy { align-items: center; }
  .hero-actions, .card-actions, .cta-band, .bespoke-copy .btn { justify-self: center; }
  .hero-actions { align-self: center; }
  .section-heading { justify-content: center; }
  .feature-row { grid-template-columns: 1fr; justify-items: center; gap: 8px; }
  .product-card .meta { flex-direction: column; align-items: center; gap: 6px; }
  .rev-head { justify-content: center; }
  .journal-meta { justify-content: center; }
  /* The one exception, and it is a readability call rather than an oversight. */
  .journal-article, .journal-article p { text-align: left; }

  /* ── 3. Smaller buttons, still a 44px target ───────────────────────────────
   * They were 202x53. The floor stays 44px: WCAG 2.5.8 asks 24 and this
   * codebase has held 44 on touch since SA-2, so this is smaller type and less
   * padding, not a smaller hit area.
   */
  .btn { padding: 11px 18px; font-size: .84rem; min-height: 44px; }
  .btn-sm { padding: 9px 15px; font-size: .8rem; }

  /* ── 4. Smaller headings. 42.4px on a 360px screen is four words a line. ── */
  h1 { font-size: 2.05rem; line-height: 1.14; }
  .hero-copy h1 { font-size: 2.15rem; }
  h2 { font-size: 1.6rem; }
  .page-hero h1 { font-size: 1.95rem; }
}

/* ── 5. The reviews strip comes back on a phone ────────────────────────────────
 * It was `display: none` under 780px, so the one line carrying her Google
 * rating never appeared on the device most people use. Two rows rather than
 * one, because the promise and the rating do not fit side by side at 360.
 */
@media (max-width: 780px) {
  .top-note { display: block; }
  .top-note .container {
    flex-direction: column; align-items: center; gap: 3px;
    padding: 7px 0; text-align: center; font-size: .7rem;
  }
  .top-note .top-rating { justify-content: center; }
  /* The third item is a pair of links that duplicate the menu. */
  .top-note .container > :last-child { display: none; }
}

/* ── 6. Full bleed on a phone, and the green moved to the phone's own bars ─
 * SA-82, and this REPLACES the 8px inset sheet SA-80 built.
 *
 * "Green outsides" meant the phone's chrome, not a border inside the page:
 * the strip where the time and the battery are, and the browser's bar at the
 * foot of the screen. That is `<meta name="theme-color">` and nothing else,
 * so the page goes back to edge to edge and reads as an app rather than as a
 * page in a frame.
 *
 * `body` keeps the green. Nothing of it shows at rest, but it is what the
 * rubber-band overscroll reveals at the top and bottom of a scroll, so the
 * colour is continuous with the bars rather than flashing white.
 */
@media (max-width: 780px) {
  /* SA-89: NOT --sage-dark. This is the ground the rubber-band overscroll
     reveals, and its whole job is to be continuous with the phone's own
     bars. SA-82 wrote that down and then hardcoded the green, so when the
     bar went charcoal (SA-85) the bounce at the foot of the page showed a
     GREEN band against charcoal chrome -- worse than the white it was put
     there to avoid, and reported as "the footer is still green". It now
     takes the SAME resolved value the theme-color meta tag does. */
  body { background: var(--browser-bar); }
  /* AND `main` paints paper over it. `body` green is only wanted where the
     page itself is not: the overscroll. Anywhere a gap opens INSIDE the page
     -- the footer carries a `margin-top`, so one does -- the green would show
     through as a stray band across the screen, which is what it did. */
  main { background: var(--paper); }
  /* Square, so the page meets the phone's own bars with no seam. Her export
     rounds the hero's foot; at full bleed that reads as a card edge again. */
  .hero { border-radius: 0; }
}

/* ── SA-81: a picture behind every page header ────────────────────────────────
 * SA-62 took the card away because it read as a big empty box, which was right,
 * and left a plain white header. A photograph is the other answer to the same
 * complaint and it is what she asked for.
 *
 * The picture arrives as a custom property on <body>, set once in base.html
 * from one resolver, so no page template changed and a page with no picture
 * keeps exactly the header it has. `:where()` keeps the specificity at zero so
 * the SA-62 rules further up are not fighting these.
 */
body:not([style*="--page-hero-img"]) .page-hero { /* unchanged */ }

body[style*="--page-hero-img"] .page-hero {
  position: relative;
  background-image:
    /* The scrim carries the type, and it is strongest where the words are.
       SA-13 measured a 12px eyebrow at 2.4 over a pale room shot under a mid
       gradient, so this holds ~.72 across the copy and releases past it. */
    linear-gradient(180deg, rgba(24, 25, 22, .74) 0%, rgba(24, 25, 22, .62) 62%,
                    rgba(24, 25, 22, .78) 100%),
    var(--page-hero-img);
  background-size: cover;
  background-position: center;
  padding: 68px 0 44px;
  margin-bottom: 4px;
  color: #fff;
}
/* SA-92: the SMALL tier on a small screen, which is the SA-2 rule applied to a
   CSS background. A phone draws this band at about 1,170 device px, and the
   1400px tile already covers that exactly -- so the 2000px hero would be ~400KB
   for no visible gain. Declared with the hero as its own fallback, so a page
   that emits only one URL still paints. */
@media (max-width: 900px) {
  body[style*="--page-hero-img-sm"] .page-hero {
    background-image:
      linear-gradient(180deg, rgba(24, 25, 22, .74) 0%, rgba(24, 25, 22, .62) 62%,
                      rgba(24, 25, 22, .78) 100%),
      var(--page-hero-img-sm, var(--page-hero-img));
  }
}
body[style*="--page-hero-img"] .page-hero .box {
  border-bottom: 0; padding-bottom: 0;
}
body[style*="--page-hero-img"] .page-hero h1,
body[style*="--page-hero-img"] .page-hero p { color: #fff; }
body[style*="--page-hero-img"] .page-hero .eyebrow { color: #dfe7cf; }
/* The header's own button is charcoal-on-white everywhere else; on a picture it
   inverts, which is the same call the home hero and the olive band already
   make (SA-62). */
body[style*="--page-hero-img"] .page-hero .btn-primary {
  background: #fff; color: var(--charcoal); border-color: #fff;
}

@media (max-width: 780px) {
  body[style*="--page-hero-img"] .page-hero { padding: 44px 0 34px; }
}


/* ── SA-82: the mobile footer ──────────────────────────────────────
 * Measured at 390px: 1,805px tall, one link per row, and a whole column of it
 * missing. Two separate faults.
 */

/* 1. A WHOLE FOOTER COLUMN WAS HIDDEN ON EVERY PHONE AND TABLET.
 *
 * Her export drops the fourth column under 1050px, which was a reasonable call
 * for a footer that was decoration. Ours is a sitemap: that column is Delivery
 * & Returns, Care, FAQ and the rest, and on a phone the footer is where people
 * go looking for exactly those. Same fault and the same reasoning as the five
 * product cards on the home page (SA-79), which her export also hid.
 *
 * Same selector as the rule it undoes, so it wins on source order rather than
 * on a specificity bump that would be invisible to whoever reads it next.
 */
@media (max-width: 1050px) {
  .footer-grid > div:nth-child(4) { display: block; }
}

@media (max-width: 780px) {
  /* 2. ONE LINK PER ROW MADE IT 1,805px.
   *
   * 22 links at the 44px touch floor plus their margins IS most of that
   * height, and the floor is not the thing to give up: these are standalone
   * controls, which is the call SA-67 and SA-79 both made about this footer.
   * Two-up halves the stacks and keeps every target at 44px.
   *
   * A grid rather than `columns: 2`, because the heading, the address and the
   * button have to span, and CSS columns give no way to say so. */
  .footer-grid { gap: 24px; }
  .footer-grid > div:not(:first-child) {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 12px;
    align-content: start;
  }
  .footer-grid > div:not(:first-child) > h4,
  .footer-grid > div:not(:first-child) > p,
  .footer-grid > div:not(:first-child) > .btn { grid-column: 1 / -1; }
  .footer-grid > div:not(:first-child) > h4 { margin-bottom: 6px; }

  /* The links were the one thing in the footer still left-aligned, under
     centred headings, because `width: fit-content` makes a flex item a block
     that `text-align` cannot move. Centred in its own cell now, like
     everything else on the phone layout. */
  .footer-grid > div > a { width: auto; justify-content: center; margin-bottom: 0; }
  .footer-grid > div > a.btn { margin-top: 10px; }

  /* The socials are a row, not a column: the two-up grid must not catch them. */
  .footer-grid .socials { grid-column: 1 / -1; justify-content: center; }

  /* Its own padding, not a margin: a margin here is a gap between two
     elements and shows whatever is behind them. */
  .site-footer { padding: 34px 0 16px; margin-top: 0; }
}
/* ── SA-98: the visualiser panel had an empty third ──────────────────────────
 * Theresa: "it looks so empty". `.bespoke` is `1fr 1fr 1fr`, written for the
 * bespoke band, which has three children (copy, picture, promises). This panel
 * has TWO, so the right third of the card was blank and the picture sat as a
 * 220px object floating in the middle one.
 *
 * Scoped `.bespoke.vis-panel` (0,2,0) rather than `.vis-panel` (0,1,0): the
 * phone rules for `.bespoke` live in a media block near the top of this file,
 * and a same-specificity override only wins on source order, which is the trap
 * this stylesheet has now sprung five times (SA-13, SA-19, SA-24, SA-36,
 * SA-91). Winning on specificity survives the next append.
 *
 * `padding: 0` with `overflow: hidden` is what lets the photograph run to the
 * card's own rounded edge, which is the whole difference between a picture in
 * a box and a panel with a picture in it. */
.bespoke.vis-panel {
  grid-template-columns: 1.02fr 1fr;
  gap: 0; padding: 0; overflow: hidden; align-items: stretch;
}
.bespoke.vis-panel .bespoke-copy {
  padding: clamp(30px, 3.4vw, 48px);
  justify-content: center;
}

/* The picture fills its column: `height: auto` against `.sample-art`'s 230px,
 * and a min-height so a short copy column cannot collapse it. */
.bespoke.vis-panel .sample-art {
  /* 400, not 360: a room shot is 4:3, so at ~600px wide it wants 450 to show
   * whole and 360 was cropping the ceiling and the floor off a picture whose
   * whole job is to look like a room. The copy column is 360 tall, so this is
   * what sets the panel's height. */
  position: relative; display: block; height: auto; min-height: 400px;
  overflow: hidden; border-radius: 0;
  background-size: cover; background-position: center;
}

/* THE SWATCH CARD. The panel's heading contrasts a wall with a swatch, so the
 * picture shows both: the design on the wall behind, and the flat artwork on a
 * paper card in front of it, named. Paper rather than the dark chip the other
 * two reels use -- a swatch IS a piece of printed paper, and ink on paper is
 * also the one treatment that reads over any photograph she uploads. */
.bespoke.vis-panel .vis-swatch {
  position: absolute; left: 18px; bottom: 18px; right: auto; top: auto;
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: center; column-gap: 13px; row-gap: 0;
  padding: 11px 16px 11px 11px; max-width: calc(100% - 36px);
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--line); border-radius: 16px;
  box-shadow: 0 14px 30px rgba(24, 25, 22, .20);
  text-decoration: none;
}
.bespoke.vis-panel .vis-swatch img {
  grid-row: 1 / 3; width: 54px; height: 54px;
  object-fit: cover; border-radius: 11px; display: block;
  border: 1px solid var(--line);
}
.bespoke.vis-panel .vis-swatch b {
  grid-column: 2; font-size: .95rem; line-height: 1.25; color: var(--ink);
  /* One line, and cut rather than wrapped to two: "Vibrant Botanical Blooms"
   * is 24 characters and the card sits over a photograph. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bespoke.vis-panel .vis-swatch span {
  grid-column: 2; font-size: .74rem; letter-spacing: .04em;
  color: var(--muted); opacity: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (hover: hover) {
  .bespoke.vis-panel .vis-swatch:hover {
    background: var(--paper);
    box-shadow: 0 18px 38px rgba(24, 25, 22, .26);
  }
}

/* Two columns still fit a tablet; one below that, with the picture under the
 * copy. The `.bespoke` phone rules set `padding: 26px` and `.sample-art`
 * 180px, and both are answered here rather than left to fight. */
@media (max-width: 1050px) {
  .bespoke.vis-panel { grid-template-columns: 1fr 1fr; }
  .bespoke.vis-panel .sample-art { min-height: 320px; }
}
@media (max-width: 780px) {
  .bespoke.vis-panel { grid-template-columns: 1fr; padding: 0; }
  .bespoke.vis-panel .bespoke-copy { padding: 28px 24px 24px; }
  .bespoke.vis-panel .sample-art { min-height: 260px; }
  .bespoke.vis-panel .vis-swatch { left: 14px; bottom: 14px; padding: 9px 14px 9px 9px; }
  .bespoke.vis-panel .vis-swatch img { width: 46px; height: 46px; }
}
/* ── SA-99: her Trustindex badge lands on the WhatsApp button ────────────────
 * Their "Trusted Site" badge is `position: fixed` at the bottom right and
 * carries `z-index: 2147483647`. That is INT_MAX: it cannot be out-layered,
 * so the only thing that can move is ours.
 *
 * On a phone the two overlap outright -- the badge's white pill sits across
 * the lower half of her WhatsApp button, which is the primary contact action
 * on the device most of her visitors use. Measured: badge 136x28 at the
 * bottom edge, so 38px of clearance puts the button clear of it with room for
 * the shadow.
 *
 * Only on a page that actually renders a badge (`.has-ti-badge`), so no other
 * client's button moves. */
:root.has-ti-badge .floating-whatsapp { bottom: 60px; }
@media (max-width: 780px) {
  :root.has-ti-badge .floating-whatsapp { bottom: 53px; }
}
/* ── SA-102: the studio map ──────────────────────────────────────────────────
 * Her old WordPress contact page had one and this design had none, which SA-63
 * noted as a small miss at the time.
 *
 * FULL WIDTH under the two cards, not inside the studio one: a map wants a
 * street, and a map shrunk into a column is the fault MH-5 recorded about a
 * photograph. The card sits ON it, which is the same treatment the visualiser
 * panel uses for its swatch (SA-98), so the site has one idea about pictures
 * with a label on them rather than two. */
.studio-map {
  position: relative;
  margin-top: 22px;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--linen);
  /* Wide on a desktop, taller in proportion on a phone: a 21:9 strip at 390px
   * is 167px of map, which shows a road and nothing else. */
  aspect-ratio: 21 / 8;
  min-height: 260px;
}
.studio-map iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0; display: block;
  /* Google draw their own map in their own colours; a filter here would fight
   * it and go wrong the day they change it. Only the frame is ours. */
}
.map-card {
  /* TOP RIGHT is the one corner Google leave empty. Their own chrome takes the
   * other three: the place bubble top-left, the Street View thumbnail
   * bottom-left, and the fullscreen control plus the attribution they require
   * to stay visible bottom-right. */
  position: absolute; right: 20px; top: 20px;
  max-width: min(300px, calc(100% - 40px));
  display: grid; gap: 6px;
  padding: 16px 18px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: 0 16px 34px rgba(24, 25, 22, .22);
}
.map-card .eyebrow { margin: 0; }
.map-card p { margin: 0; font-size: .92rem; line-height: 1.45; color: var(--ink); }
.map-card .btn { justify-self: start; margin-top: 4px; }

@media (max-width: 780px) {
  /* THE CARD COMES OFF THE MAP ENTIRELY. Floating it at the foot and calling
   * that "smaller" was not enough: measured at 390px it covered 50% of the
   * map, which is the same fault one size down. So on a phone the frame holds
   * a map and then a strip under it, and nothing is hidden. */
  .studio-map {
    aspect-ratio: auto; min-height: 0; margin-top: 16px;
    display: block;
  }
  .studio-map iframe { position: static; height: 240px; }
  .map-card {
    position: static;
    max-width: none;
    border: 0; border-top: 1px solid var(--line);
    border-radius: 0;
    box-shadow: none;
    background: var(--paper);
    padding: 14px 16px 16px;
  }
  .map-card p { font-size: .88rem; }
}
/* ── SA-103: one button scale, and one field shape ───────────────────────────
 * Theresa: "contact forms and buttons looks huge and not consistant".
 * Measured over twelve pages before changing anything, and she is right:
 * `.btn` was 53px and `.btn-sm` 42px with no stated rule about which belongs
 * where, the newsletter's Subscribe was 69px on EVERY page, and the same text
 * field appeared at three heights with three radii on one page.
 *
 * TWO sizes, and the difference now means something: `.btn` is a page-level
 * action (a hero, a form's submit), `.btn-sm` is an action inside a card or a
 * row. 6px apart rather than 11, so they read as one family.
 *
 * These come AFTER the base rules on purpose and match their specificity, so
 * the last word wins. The scale is here, in one place, rather than a padding
 * per component -- which is how it drifted in the first place. */
/* SA-103b: the first pass only trimmed padding and the buttons stayed 48px,
 * because `.btn` set no line-height and inherited the BODY's reading
 * line-height (about 1.6): 22px of line box plus 24px of padding. Line-height
 * is the lever, not padding. 40px and 32px. */
.btn { padding: 10px 20px; font-size: .84rem; line-height: 1.2; }
.btn-sm { padding: 7px 14px; font-size: .78rem; line-height: 1.2; }

/* The dark pill inside the top bar is the one exception and stays smaller:
 * it sits in a 40px strip and cannot take the standard height. */
.top-note .btn, .top-note .btn-sm { padding: 6px 13px; font-size: .74rem; }

/* SUBSCRIBE WAS 69px ON EVERY PAGE. `.inline-form` is a flex row, and the
 * default `align-items: stretch` grew the button to the full height of the
 * row rather than the height of a button. It is the tallest thing on the
 * site and it is in the footer of all of it. */
.inline-form, .modal-form { align-items: center; }
.inline-form .btn, .modal-form .btn { flex: 0 0 auto; align-self: center; }

/* ── Fields ──────────────────────────────────────────────────────────────────
 * One height and one shape for anything somebody types into. The contact
 * inputs were 53px of pale fill with almost no edge, which is what reads as
 * "huge": a field should look like somewhere to write, not like a slab.
 *
 * The 16px floor under 1000px stays untouched (SA-2): that is not a style
 * choice, it is what stops iOS zooming the page on focus. */
/* Radios, checkboxes and file pickers are NOT text fields: the checkout's
 * payment chooser and the quote dialog's options sit inside a `.field` too,
 * and a radio given padding and a radius stops looking like a radio. */
.field input:not([type=radio]):not([type=checkbox]):not([type=file]):not([type=hidden]),
.field textarea, .field select,
input.field, .form-input {
  font-size: .9rem;
  line-height: 1.35;
  border-radius: 10px;
  /* A firmer edge than --line (.16): the fields read as pale slabs at that
   * weight, which is half of what "huge" meant. Derived from the same ink so
   * it follows the palette rather than freezing a hex. */
  border: 1px solid color-mix(in srgb, var(--ink) 24%, transparent);
  background: var(--paper);
  padding: 9px 12px;
}
.field textarea, textarea.form-input { min-height: 110px; }
.field label { font-size: .76rem; margin-bottom: 4px; }
.field { margin-bottom: 12px; }

/* The newsletter input keeps its PILL: it is a single field on its own in a
 * band, which is the one place the shape reads as deliberate rather than as
 * a different component. Everything else about it now matches. */
input.field { border-radius: 999px; padding: 9px 16px; }

/* The shop's filter chips were 47px at 16px type, the tallest control on that
 * page. They are the same kind of control as `.btn-sm`, so the same size. */
.shop-filter { padding: 7px 14px; font-size: .78rem; line-height: 1.2; }

/* The quote dialog carries its own field rule at (0,3,2), so the shared field
 * shape above never reached it. The same selectors, restated, with the same
 * numbers. */
dialog.quote .field input[type="text"], dialog.quote .field input[type="email"],
dialog.quote .field input[type="tel"], dialog.quote .field input[type="number"],
dialog.quote .field textarea, dialog.quote .field select,
dialog.quote .field input[type="file"] {
  padding: 9px 12px; font-size: .9rem; line-height: 1.35; border-radius: 10px;
}

/* The form's submit was a 590px black banner across the card, which is most
 * of what "huge" meant. That width was an INLINE style on the button and is
 * gone from the template (and from the checkout's Place order, which had the
 * same inline style): a rule here would have been a second thing fighting the
 * first, and only `!important` could have won. */

/* ── The phone, restated LAST ────────────────────────────────────────────────
 * The block above is unscoped and comes after SA-80's phone rules at the same
 * specificity, so without this it silently undoes them: the smaller phone
 * button and the shorter phone textarea. Same trap as SA-13, SA-19, SA-91.
 *
 * On a phone a form's submit goes the full width of the form again, which is
 * the AG-3 rule: it is the thumb's target at the foot of a long form, and a
 * natural-width button there sits in one corner of a 340px card. That is a
 * layout rule in a media query, not an inline style nothing can override. */
/* SA-103b: THE TOUCH FLOOR COMES DOWN FROM 44px TO 40px, on Theresa's report
 * that the buttons were still big on her phone. Every button on a phone was
 * being forced to 44-45px by the two floor blocks near the top of this file,
 * so no padding change below could make one smaller. 40px is still well
 * clear of WCAG 2.5.8's 24px; 44px was this codebase's own preference, not a
 * requirement. Declared under 1000px like the floor it replaces, and LAST. */
@media (max-width: 1000px) {
  .btn, .btn-sm, .chip, .shop-filter { min-height: 40px; }
  .btn { padding: 9px 18px; font-size: .84rem; line-height: 1.2; }
  .btn-sm { padding: 8px 15px; font-size: .8rem; line-height: 1.2; }
}
@media (max-width: 780px) {
  .top-note .btn, .top-note .btn-sm { padding: 6px 13px; font-size: .74rem; min-height: 0; }
  /* Inputs stay at 16px here (the iOS focus-zoom rule, `!important` above),
   * so the height comes out of the padding instead: 40px. */
  .field input:not([type=radio]):not([type=checkbox]):not([type=file]):not([type=hidden]),
  .field select, input.field, .form-input { padding-top: 8px; padding-bottom: 8px; line-height: 1.3; }
  dialog.quote .field input[type="text"], dialog.quote .field input[type="email"],
  dialog.quote .field input[type="tel"], dialog.quote .field input[type="number"],
  dialog.quote .field select { padding-top: 8px; padding-bottom: 8px; line-height: 1.3; }
  .field textarea, textarea.form-input { min-height: 100px; }
  .contact-layout form .btn[type=submit], #place { width: 100%; }
}

/* ── SA-104: the phone menu, redesigned, and STILL FULL SCREEN ────────────────
 * Audited on production at five real phone viewports (553 to 833). The sheet
 * was never leaking: closed, it is `display: none` on every page. What was off:
 *
 *  1. The WhatsApp button and the Trustindex badge floated OVER the open
 *     sheet, and the badge covered its socials.
 *  2. Ten centred, bold rows each under a rule read as ten equal buttons,
 *     with the current page marked by colour alone.
 *  3. It opened on a 91px centred logo block repeating the header.
 *  4. Products and Artists: only the word was tappable.
 *  5. It ended on a pile of small things with no next step.
 *
 * NOTHING HERE CHANGES THE SHEET'S SIZE OR POSITION. `.main-nav` stays
 * `position: fixed; inset: 0; 100vw x 100dvh` from the SA-22 block, which is
 * what "keep the mobile menu full screen" means, and a test fails if a rule in
 * this block declares position, inset, top, width or height on it.
 */
.navgroup-row { display: none; }

/* A heading button on a desktop looks and behaves like the heading it was. */
.footer-col-toggle {
  appearance: none; background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; letter-spacing: inherit;
  text-transform: inherit; text-align: inherit; cursor: default;
}

/* 1. Nothing floats over an open sheet. The badge is z-index INT_MAX (SA-99),
 * so it cannot be layered under the sheet; it can only be taken away while the
 * sheet is up. `!important` because their script writes inline styles. */
body.nav-open .floating-whatsapp,
body.nav-open .ti-certificate,
body.nav-open .ti-widget:has(.ti-certificate) { display: none !important; }

@keyframes sa-nav-in { from { opacity: 0; } to { opacity: 1; } }

@media (max-width: 1150px) {
  .main-nav {
    padding: 0 20px calc(22px + env(safe-area-inset-bottom, 0px));
    background: #fff;
  }
  /* Opacity only: a transform on a fixed element would make it the containing
     block for anything fixed inside it (the SA-22 backdrop-filter trap). */
  .main-nav.open { animation: sa-nav-in .18s ease-out; }

  /* 3. The top of the sheet is the header, opened: same height, logo on the
   * left, close on the right where the burger was. Sticky, so the way out
   * stays on screen while the list scrolls. */
  .nav-head {
    position: sticky; top: 0; z-index: 2;
    justify-content: space-between;
    /* No left padding: the logo link carries 12px of its own, which puts the
       picture at x=12, exactly where the header's logo was (measured). */
    margin: 0 -20px 10px; padding: 0 12px 0 0;
    min-height: 88px; background: #fff;
    border-bottom: 1px solid var(--line);
  }
  .nav-head .nav-logo img { width: 190px; }
  .nav-close { position: static; transform: none; }

  /* 2. The rows: left-aligned, her serif, lighter, no rule under every one. */
  .main-nav > a,
  .main-nav > .navgroup > .navgroup-row {
    justify-content: flex-start; text-align: left;
    min-height: 50px; padding: 0 2px; border: 0;
    font-family: var(--serif); font-size: 1.32rem; font-weight: 400;
    line-height: 1.2; color: var(--ink); position: relative;
  }
  .main-nav > a.active,
  .main-nav > .navgroup > .navgroup-row.active { color: var(--sage-dark); }
  /* The current page carries a mark, not only a colour (WCAG 1.4.1). */
  .main-nav > a.active::before,
  .main-nav > .navgroup > .navgroup-row.active::before {
    content: ""; position: absolute; left: -20px; top: 13px; bottom: 13px;
    width: 3px; border-radius: 0 3px 3px 0; background: var(--sage-dark);
  }

  /* 4. A group is one row that opens a list under it. */
  .main-nav > .navgroup {
    flex-direction: column; align-items: stretch; border: 0;
  }
  .main-nav > .navgroup > a,
  .main-nav > .navgroup > .navgroup-toggle { display: none; }
  .main-nav > .navgroup > .navgroup-row {
    display: flex; align-items: center; gap: 10px; width: 100%;
    background: none; cursor: pointer;
  }
  .navgroup-row svg { margin-left: auto; color: var(--muted); transition: transform .18s; }
  .navgroup.open > .navgroup-row svg { transform: rotate(180deg); }
  /* The desktop drop-down gives the panel a radius and its links a COLUMN
     layout with centred items, which in the sheet drew a curved bracket down
     the left and centred every row. Both undone here. */
  .main-nav > .navgroup > .navdrop {
    flex-direction: column; align-items: stretch; gap: 0;
    margin: 0 0 8px 2px; padding: 0 0 0 14px;
    border: 0; border-left: 1px solid var(--line); border-radius: 0;
  }
  .main-nav > .navgroup > .navdrop a {
    flex-direction: row; justify-content: space-between; align-items: center;
    text-align: left; min-height: 44px; padding: 0 2px 0 4px;
    font-family: var(--sans); font-size: .98rem; font-weight: 500;
    color: var(--ink); background: none; border: 0; border-radius: 0;
  }
  .main-nav > .navgroup > .navdrop a small {
    font-size: .74rem; font-weight: 400; color: var(--muted);
  }
  .main-nav > .navgroup > .navdrop a.all {
    font-size: .78rem; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--sage-dark);
  }

  /* 5. The foot: one button, then a compact line of promises, then contact. */
  .nav-foot {
    margin-top: auto; padding-top: 22px; gap: 14px; align-items: stretch;
  }
  /* `.nav-foot a` paints every link in the foot muted, which would put grey
     type on the charcoal button; this is (0,2,1) and comes after it. */
  .nav-foot a.nav-cta {
    color: #fff; justify-content: center; min-height: 46px; font-size: .9rem;
  }
  .nav-promises { margin: 0; padding: 0; gap: 6px; }
  .nav-promises li {
    flex-direction: row; justify-content: center; align-items: center;
    gap: 6px; text-align: left;
  }
  .nav-promises svg { width: 18px; height: 18px; flex: 0 0 auto; }
  .nav-promises li > span:last-child { font-size: .68rem; line-height: 1.25; }
  .nav-contact {
    display: flex; flex-wrap: wrap; justify-content: center; column-gap: 20px;
  }
  .nav-foot .social { justify-content: center; margin-top: -6px; }
}

@media (max-width: 780px) {
  /* The phone header is 74px with a 155px logo (her export's own sizes). */
  .nav-head { min-height: 74px; }
  .nav-head .nav-logo img { width: 155px; }
}

/* A short phone: tighter rows, never hidden ones (the SA-95 rule). */
@media (max-width: 1150px) and (max-height: 700px) {
  .main-nav > a,
  .main-nav > .navgroup > .navgroup-row { min-height: 44px; font-size: 1.18rem; }
  .nav-foot { padding-top: 16px; gap: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .main-nav.open { animation: none; }
}

/* ── SA-104: the phone footer stops being the menu again ─────────────────────
 * 1,467px on the FAQ page at 390px, 44% of the page, 25 links and seven of
 * them the menu's own. On a phone: a column the menu fully covers is dropped,
 * a link the menu has is dropped, and each remaining column opens from its
 * heading. `.js-footer` is set by the script, so without it every link shows.
 */
@media (max-width: 780px) {
  .footer-grid > .footer-col.in-menu { display: none; }
  .footer-grid > .footer-col > a.in-menu { display: none; }

  .js-footer .footer-grid > .footer-col { border-top: 1px solid var(--line); }
  .js-footer .footer-grid > .footer-col > h4 { margin: 0; }
  .js-footer .footer-col-toggle {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; min-height: 50px; cursor: pointer; text-align: left;
  }
  .js-footer .footer-col-toggle::after {
    content: ""; width: 8px; height: 8px; margin-right: 4px;
    border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
    transform: rotate(45deg); transition: transform .18s;
  }
  .js-footer .footer-grid > .footer-col.open .footer-col-toggle::after {
    transform: rotate(-135deg);
  }
  .js-footer .footer-grid > .footer-col:not(.open) > a { display: none; }
  .js-footer .footer-grid > .footer-col.open { padding-bottom: 12px; }

  /* The grid's 24px row gap sat BETWEEN the section rows, so two headings read
     as two separate blocks. Measured: two 51px rows with 24px holes. They
     stack edge to edge now; the space goes around the group instead, and the
     contact block closes the last section with its own rule. */
  .js-footer .footer-grid { row-gap: 0; }
  .js-footer .footer-grid > .footer-brand { margin-bottom: 22px; }
  /* The blurb is centred on a phone (SA-80) and the logo and socials above and
     below it were not, so the brand block leaned left under a centred line. */
  .site-footer .footer-brand .footer-logo { margin-inline: auto; }
  .site-footer .footer-brand .social { justify-content: center; }
  .js-footer .footer-grid > .footer-col + div:not(.footer-col) {
    margin-top: 0; padding-top: 22px; border-top: 1px solid var(--line);
  }

  /* The sign-up card was 292px: a two-line display heading and generous
     padding in a column 366px wide. Same content, sized for the phone. */
  .site-footer .newsletter-inline { padding: 22px 20px; margin-top: 24px; }
  .site-footer .newsletter-inline h3 { font-size: 1.45rem; line-height: 1.15; margin-bottom: 6px; }
  .site-footer .newsletter-inline p { font-size: .9rem; margin-bottom: 0; }
  .site-footer .footer-bottom { margin-top: 18px; padding-top: 12px; gap: 0; }
}

/* ── SA-106: ranges in the product cards, products in the style tiles ────────
 * The cards over the hero and a second row under the Google reviews show her
 * ranges in the product-card look; the style-tile box shows the five products.
 */

/* A range with no cover keeps its card the same height as its neighbours. */
.product-card .product-blank {
  display: block; aspect-ratio: 4 / 3; border-radius: 18px; background: var(--linen);
}

/* The second row sits in the flow, not pulled over a hero, so it needs the
   heading's own spacing and the button under it. */
.home-ranges .section-heading { margin-bottom: 22px; }
.home-ranges .style-cta { margin-top: 24px; }

/* Five products in a grid written for eight would leave three empty columns,
   which is invisible to every automated check and plainly wrong on the page
   (the SA-98 empty-third lesson). Five across, and three on a phone.
   (0,2,0), so it beats the (0,1,0) `.style-grid` column rules at every width. */
.style-grid.home-products { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@media (max-width: 780px) {
  .style-grid.home-products { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.home-products .style-card small {
  display: block; margin-top: 2px; text-align: center;
  font-size: .7rem; line-height: 1.3; color: var(--muted);
}
