/* ===================================================================
   AMAN site-extra — product detail page components + lead-gen widgets
   Loaded on every page (after style.css). Reuses :root brand tokens.
   =================================================================== */

/* ---- WhatsApp: floating button (all pages) + button variant ---- */
.wa-float{position:fixed;right:20px;bottom:20px;z-index:90;width:56px;height:56px;border-radius:50%;
  background:#25d366;color:#fff;display:flex;align-items:center;justify-content:center;
  box-shadow:0 8px 24px rgba(37,211,102,.45);transition:var(--ease)}
.wa-float:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 12px 30px rgba(37,211,102,.55)}
.btn-wa{background:#25d366;color:#fff;border:1px solid #25d366}
.btn-wa:hover{background:#1fb457;border-color:#1fb457;color:#fff}

/* ---- Full-width autoplay video banner (subcategory hero) ---- */
.video-banner{width:100%;height:clamp(220px,36vw,440px);overflow:hidden;background:var(--ink);line-height:0;position:relative}
.video-banner video{width:100%;height:100%;object-fit:cover;display:block}
.video-banner::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0) 60%,rgba(0,0,0,.18));pointer-events:none}

/* ---- Key-spec pills under the product title ---- */
.pd-pills{display:flex;flex-wrap:wrap;gap:8px;margin:4px 0 18px}
.pd-pills .pill{display:inline-flex;flex-direction:column;gap:1px;background:var(--accent-soft);
  border:1px solid #ffe1c4;border-radius:10px;padding:7px 13px;font-weight:800;color:var(--ink);font-size:.95rem;line-height:1.2}
.pd-pills .pill i{font-style:normal;font-weight:600;color:var(--accent-dark);font-size:.68rem;text-transform:uppercase;letter-spacing:.04em}

/* ---- Quick info (model / brand / HS / warranty) ---- */
.pd-meta{list-style:none;display:grid;grid-template-columns:repeat(2,1fr);gap:10px 22px;padding:16px 0;margin:0 0 20px;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.pd-meta li{display:flex;justify-content:space-between;gap:10px;font-size:.92rem}
.pd-meta span{color:var(--muted)}
.pd-meta b{color:var(--ink)}

/* ---- Two-column body: content + sticky quote card ---- */
.pd-layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:44px;align-items:start;margin-top:48px}
.pd-main{min-width:0}
.pd-main h2{font-size:1.5rem;margin:1.6em 0 .5em}
.pd-main h2:first-child{margin-top:0}
.pd-aside{position:sticky;top:90px}
.quote-card{background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:22px}
.quote-card h3{margin:0 0 .2em}
.quote-card .contact-mini{list-style:none;padding:16px 0 0;margin:14px 0 0;border-top:1px solid var(--line);font-size:.9rem}
.quote-card .contact-mini li{padding:3px 0;color:var(--muted)}
.quote-card .contact-mini a{color:var(--ink);font-weight:600}

/* ---- Technical parameter table ---- */
.table-wrap{overflow-x:auto}
.param-table{width:100%;border-collapse:collapse;font-size:.94rem}
.param-table thead th{background:var(--ink);color:#fff;text-align:left;padding:11px 14px;font-weight:700}
.param-table thead th:last-child{color:var(--accent)}
.param-table td{padding:10px 14px;border-bottom:1px solid var(--line)}
.param-table td.u{color:var(--muted);white-space:nowrap}
.param-table tbody tr:nth-child(even){background:var(--bg-soft)}

/* ---- Detailed photos grid ---- */
.detail-photos{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:24px;margin-bottom:28px}
/* ★ WHY THE COPY SAT ON THE PHOTOS (0px), AND WHY THE FIX IS HERE (2026-09-20) ★
   Not a grid/flex collapse problem: the grid lives in normal block flow inside
   .pd-main / .prose, so its margins collapse with its neighbours in the ordinary way.
   The cause is that the grid declared NO vertical margin, and this site spaces blocks
   from the block ABOVE: `p{margin:0 0 1em}` has no margin-top, so a paragraph after a
   margin-less block starts on its bottom edge (cases/: 0px). Above, the only space
   was the heading's own margin-bottom (.pd-main h2 .5em = 12px, news h2 .4em of a
   clamp = 15.4 / 10.2px). Giving the component the site's media numbers (24 above,
   28 below - the same pair .media-block and .vf-portrait carry) fixes every family
   in one place, because collapse takes max(mine, neighbour's): a following h2
   (38.4) or .quote-box (30) keeps its own larger gap, a paragraph gets 28.
   Gated by E8 DEBT 1/2 in _build\tests\test_layout_left_alignment.py. */
/* height:auto is REQUIRED, not cosmetic: the news template gives each <img> width/height
   attributes (correct - they reserve space and stop layout shift), and the HTML height
   attribute lands as a presentational hint (height:2276px on a tall photo). With both
   dimensions resolved, aspect-ratio is ignored and the frame stretches to the file's
   pixel height. height:auto cancels the hint so aspect-ratio:4/3 governs again.
   Case-study pages emit the same component without the attributes - unaffected either way. */
/* 2+ photos = a picture grid: every frame 4:3 and the photo COVERS it (aimed by an inline
   object-position from data/image-focus.json) - no white strips, 老板 2026-09-27; gate
   _build/tests/test_card_images_fill.py. A lone photo keeps its own shape (rule below). */
.detail-photos img{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;background:#fff;border-radius:var(--radius-sm);border:1px solid var(--line)}
/* A gallery holding ONE photo must fill the column, not sit in a 244px track with the rest of
   the row blank beside it. The grid above is three fixed tracks whatever the photo count, so in
   the ~760px article column a lone image took (760-2*14)/3 = 244px and left ~516px of white -
   directly under a full-width hero figure. Four News articles ship exactly one photo.
   BOTH declarations are load-bearing - do not "tidy" either away:
   grid-column:1 / -1  spans every track. Written 1 / -1, NOT 1 / 4: the 560px breakpoint below
                       drops the grid to two tracks and -1 follows the track count on its own.
   aspect-ratio:auto   is NOT redundant with the rule above. That rule pins aspect-ratio:4/3
                       (with object-fit:contain until 2026-09-27, cover since); all four single-photo galleries hold ~16:9
                       landscape shots, so at full column width a forced 4:3 frame becomes
                       ~570px tall and letterboxes ~140px of white above and below the photo.
                       Deleting this line does not restore the old look - it makes a worse one.
                       auto hands sizing back to the image's own width/height attributes.
   The <img> is a direct child of .detail-photos on all three page types that emit this
   component (product / case / news), so '>' + ':only-child' matches without :has().
   Gated by _build\tests\test_single_photo_full_width.py. */
.detail-photos > img:only-child{grid-column:1 / -1;aspect-ratio:auto}

/* blank-image placeholders (products with no image in the Dawson map yet) */
.pd-noimg{aspect-ratio:1/1;display:flex;align-items:center;justify-content:center;background:var(--bg-soft);border:1px dashed var(--line);border-radius:var(--radius);color:var(--muted);font-weight:600;text-align:center;padding:20px}
.prod-thumb .noimg{aspect-ratio:4/3;width:100%;display:flex;align-items:center;justify-content:center;background:var(--bg-soft);color:var(--muted);font-size:.85rem;font-weight:600}

/* ---- Inline inquiry form (lead capture) ---- */
/* THE INQUIRY BOX IS A READING COLUMN, and as of 2026-08-18 it is capped and
   centred like every other one. It had no width of its own before, so on the ~20
   single-column pages it stretched to the full 1192px frame - and with it every
   control inside, including a 1136px-wide "Company name" field. A buyer reads a
   text input that wide as a page that was never finished, and this is the one block
   on the site where that judgement costs an inquiry.
   On the 335 product detail pages nothing moves: there the box already sat in a
   704px grid cell, which is narrower than this cap. */
.quote-box{background:var(--bg-soft);border:1px solid var(--line);border-radius:var(--radius);padding:28px;max-width:760px;margin:30px auto}
.quote-box h2{margin-top:0}
/* HISTORICAL NOTE, and the reason there is no rule here any more.
   The 2026-08-17 third patch capped three paragraphs INSIDE this box at 760
   (.quote-box > p, .form-note, .form-privacy-note) because the box itself had no
   width and they were running to 1136px. That was the right fix for a box with no
   width. The box now carries the 760 cap itself (above), so those three sit in a
   704px column automatically and the old rules became dead weight that said 760 a
   second time - i.e. the beginnings of "two layers both managing width", which is
   the exact shape of the bug this whole batch exists to remove. Deleted, not
   commented out. Do not add them back; widen or narrow the BOX instead. */
.quote-form .fr{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px}
.quote-form label{display:block;font-size:.82rem;font-weight:700;color:var(--ink-2);margin-bottom:14px}
.quote-form input,.quote-form textarea,.quote-form select{width:100%;margin-top:5px;font-family:inherit;font-size:.95rem;
  padding:11px 13px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--bg);color:var(--text)}
.quote-form select{height:44px;cursor:pointer}
/* Phones: 16px in every form field. Below 16px iOS Safari zooms the whole page in the moment a
   buyer taps a field, and he has to pinch back out to see the send button (2026-09-24, 大类页
   表单方案 §三-4 ②; measured 15.2px on the test site). All quote forms share this rule. */
@media(max-width:720px){.quote-form input,.quote-form textarea,.quote-form select{font-size:16px}}
.quote-form input:focus,.quote-form textarea:focus,.quote-form select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
/* Invalid-field highlight, but ONLY after the visitor has actually touched the field.
   THE BUG IT FIXES (site review 2026-09-19, F1; logged 2026-06-10 and twice since, never fixed):
   the Name box was red the instant the page opened, before anyone typed a character, while
   Email and Company next to it stayed grey. On the inquiry form. On every page.
   WHY the old rule did that: `:placeholder-shown` can only match an input that HAS a
   placeholder attribute. Name is the one required field with NO placeholder, so
   `:not(:placeholder-shown)` was TRUE while it was empty, and `:invalid` was true too
   (required + empty) - so it painted red immediately. Email has a placeholder, so it was
   spared; Company has no placeholder but is not required, so `:invalid` never matched.
   That is why exactly one box looked broken.
   THE FIX: `:user-invalid` is the selector that actually means what this rule wanted - it
   matches only after the user has edited/blurred the field. No placeholder text had to be
   invented to work around a CSS bug.
   Degradation: a browser that does not know `:user-invalid` drops the rule and shows no red
   highlight at all. That is the safe direction to fail - no highlight beats a false accusation
   on a field the visitor has not reached yet.
   Verified by measuring the RENDERED border colour, not by reading the CSS:
   on load Name is grey like its neighbours; after touch-and-leave-empty it goes red again. */
.quote-form input:user-invalid,.quote-form textarea:user-invalid,.quote-form select:user-invalid{border-color:#e2574c}
.quote-form .form-note{font-size:.86rem;color:var(--muted);margin:14px 0 0}
.quote-form .form-note a{color:var(--accent-dark);font-weight:700}
.quote-form .form-privacy-note{font-size:.86rem;color:var(--muted);line-height:1.5;margin:10px 0 16px}
.quote-form .form-privacy-note a{color:var(--accent-dark);font-weight:700}
/* ---- Optional file attachments (2026-09-21; markup: build.ps1 UploadField, logic: main.js AMAN_FU).
   The real <input type=file> sits inside the dashed box and covers it invisibly, so a click/tap
   anywhere on the box opens the chooser (phones: photo library / camera / files) and keyboard
   focus still lands on a real control. Box is well over 44px tall for fingers. */
.quote-form .fu{margin:0 0 14px}
.quote-form .fu-title{display:block;font-size:.82rem;font-weight:700;color:var(--ink-2);margin-bottom:5px}
.quote-form .fu-drop{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
  min-height:88px;margin:0;padding:14px 16px;border:1.5px dashed var(--line);border-radius:var(--radius-sm);
  background:var(--bg);text-align:center;cursor:pointer;font-weight:400}
.quote-form .fu-drop:hover,.quote-form .fu-drop.is-over{border-color:var(--accent);background:var(--accent-soft)}
.quote-form .fu-drop:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.quote-form .fu-drop .fu-input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;opacity:0;cursor:pointer}
.quote-form .fu-main{font-size:.95rem;font-weight:600;color:var(--text)}
.quote-form .fu-clip{font-size:1.05rem}
.quote-form .fu-browse{color:var(--accent-dark);text-decoration:underline}
.quote-form .fu-hint{font-size:.8rem;color:var(--muted);line-height:1.45}
.quote-form .fu-list{list-style:none;margin:8px 0 0;padding:0}
.quote-form .fu-list li{display:flex;align-items:center;gap:10px;padding:6px 4px 6px 12px;border:1px solid var(--line);
  border-radius:var(--radius-sm);margin-top:6px;font-size:.88rem;background:#fff}
.quote-form .fu-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.quote-form .fu-size{color:var(--muted);white-space:nowrap}
.quote-form .fu-x{flex:none;width:40px;height:40px;border:0;background:transparent;color:var(--muted);font-size:1rem;cursor:pointer;border-radius:8px}
.quote-form .fu-x:hover,.quote-form .fu-x:focus-visible{color:#c0392b;background:#fdecea}
.quote-form .fu-total{font-size:.8rem;color:var(--muted);margin:6px 0 0;text-align:right}
.quote-form .fu-err{font-size:.86rem;color:#c0392b;font-weight:600;margin:8px 0 0;line-height:1.45}
.quote-form .fu-total[hidden],.quote-form .fu-err[hidden]{display:none}

/* ---- Keyword tags (internal links) ---- */
.kw-tags{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:34px 0 0;padding-top:22px;border-top:1px solid var(--line)}
.kw-tags span{color:var(--muted);font-size:.88rem;font-weight:600;margin-right:4px}
.kw-tags a{background:var(--bg-soft);border:1px solid var(--line);border-radius:999px;padding:5px 13px;font-size:.85rem;color:var(--ink-2);transition:var(--ease)}
.kw-tags a:hover{border-color:var(--accent);color:var(--accent-dark)}

/* ---- Quick-browse / turn-key tiles (.tk-grid > .tk-item) ----
   MOVED HERE FROM home.css 2026-08-19. It has to live in a stylesheet every
   page loads, because two pages emit this markup and home.css is linked on
   index.html only:
     index.html    "Turn-key solution"        4 tiles
     products.html "Find your machine faster" 3 tiles
   On products.html the rules were simply absent, so .tk-grid fell back to
   display:block and the tiles rendered as three full-width rows of bare text
   with the tick stranded above each heading. Keep this block in a global
   sheet; do not move it back into a page-scoped one.

   COLUMN COUNT FOLLOWS THE TILE COUNT - auto-fit, not repeat(4,1fr).
   The old rule hardcoded four tracks. With products.html's three tiles that
   left the entire fourth track (~300px at 1280) blank down the right-hand
   side, which is the "right side has a lot of empty space" complaint all over
   again. auto-fit collapses the empty track and 1fr shares the width out, so
   three tiles fill the row and four still sit four-across.
   The 200px floor is chosen, not arbitrary: above the 1000px breakpoint the
   container never drops below ~910px, and 4x200 + 3x20 gap = 860px still fits,
   so index.html keeps exactly four tracks at every desktop width it had them
   before. Raising this floor would silently re-wrap the homepage.
   The two breakpoints below still hard-set the count on tablet/phone.
   Gated by _build\tests\test_component_css_linked.py. */
.tk-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:20px}
.tk-item{background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);padding:28px 24px;transition:var(--ease);display:block}
.tk-item:hover{box-shadow:var(--shadow-lg);transform:translateY(-4px);border-color:transparent}
.tk-ic{width:46px;height:46px;border-radius:12px;background:var(--accent-soft);color:var(--accent);display:flex;align-items:center;justify-content:center;margin-bottom:14px}
.tk-item h3{margin:0 0 .3em}
.tk-item p{color:var(--muted);font-size:.92rem;margin:0}
@media(max-width:1000px){ .tk-grid{grid-template-columns:repeat(2,1fr)} }
@media(max-width:560px){ .tk-grid{grid-template-columns:1fr} }

/* ---- Responsive ---- */
@media(max-width:980px){
  .pd-layout{grid-template-columns:1fr;gap:30px}
  .pd-aside{position:static}
}
@media(max-width:560px){
  .pd-meta{grid-template-columns:1fr}
  .quote-form .fr{grid-template-columns:1fr}
  .detail-photos{grid-template-columns:repeat(2,1fr)}
  .wa-float{width:50px;height:50px;right:14px;bottom:14px}
}

/* placeholder-subcategory gallery: each sample image is an inquirable card */
.ph-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:18px;margin-bottom:22px}
.ph-card{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:#fff;display:flex;flex-direction:column}
.ph-thumb{aspect-ratio:4/3;overflow:hidden;background:var(--bg-soft)}
.ph-thumb img{width:100%;height:100%;object-fit:cover;transition:var(--ease)}
.ph-card:hover .ph-thumb img{transform:scale(1.05)}
.ph-card .btn{border-radius:0;justify-content:center;font-size:.92rem;padding:11px}

/* product-page buyer-directed closing line (before Service section) */
.pd-cta-line{background:var(--accent-soft);border-left:3px solid var(--accent);border-radius:0 var(--radius-sm) var(--radius-sm) 0;
  padding:14px 18px;margin:22px 0;font-size:.97rem}
.pd-cta-line a{font-weight:800;color:var(--accent-dark);white-space:nowrap}
.pd-cta-line a:hover{text-decoration:underline}

/* ===== RFQ basket: header link + count badge ===== */
.basket-link{position:relative;display:inline-flex;align-items:center;gap:6px;margin-left:auto;
  color:var(--ink);font-weight:700;font-size:.9rem;padding:6px 4px;flex-shrink:0;white-space:nowrap}
.basket-link:hover{color:var(--accent)}
.basket-link svg{width:24px;height:24px;flex-shrink:0}
.basket-link .basket-label{display:none}
@media(min-width:600px){.basket-link .basket-label{display:inline}}
.basket-count{position:absolute;top:-3px;left:15px;min-width:18px;height:18px;padding:0 5px;
  display:inline-flex;align-items:center;justify-content:center;background:var(--accent);color:#fff;
  font-size:.68rem;font-weight:800;border-radius:999px;line-height:1;box-shadow:0 0 0 2px #fff}
.basket-count[hidden]{display:none !important}

/* Add-to-basket button feedback states */
.btn.added{background:#1f9d55 !important;border-color:#1f9d55 !important;color:#fff !important}
.btn.in-basket{background:var(--ink) !important;border-color:var(--ink) !important;color:#fff !important}
.btn.in-basket:hover{background:#000 !important;color:#fff !important}

/* ===== Basket / RFQ page ===== */
.basket-grid{display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:40px;align-items:start;padding:44px 0}
.basket-aside{position:sticky;top:96px}
.basket-list{list-style:none;padding:0;margin:0;display:grid;gap:14px}
.bk-item{display:grid;grid-template-columns:96px 1fr auto;gap:16px;align-items:center;
  border:1px solid var(--line);border-radius:var(--radius);padding:12px;background:#fff}
.bk-thumb{display:block;width:96px;height:72px;border-radius:var(--radius-sm);overflow:hidden;background:var(--bg-soft)}
.bk-thumb img{width:100%;height:100%;object-fit:contain;background:#fff}
.bk-noimg{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:.72rem;color:var(--muted)}
.bk-info{min-width:0}
.bk-info h3{font-size:.98rem;margin:0;line-height:1.35}
.bk-info a{color:var(--ink)}
.bk-info a:hover{color:var(--accent)}
.bk-model{display:inline-block;margin-top:5px;font-size:.8rem;color:var(--muted)}
.bk-remove{background:0;border:1px solid var(--line);color:var(--muted);width:34px;height:34px;border-radius:8px;
  font-size:1.4rem;line-height:1;cursor:pointer;transition:var(--ease);flex-shrink:0}
.bk-remove:hover{border-color:#e2574c;color:#e2574c;background:#fff5f4}
.basket-empty{text-align:center;padding:54px 20px;border:1px dashed var(--line);border-radius:var(--radius);
  background:var(--bg-soft);color:var(--muted)}
.basket-empty p{font-size:1.05rem;margin:0 0 18px}
.quote-form.is-disabled{opacity:.55;pointer-events:none}
/* RFQ submit button text ("Send RFQ for Selected Machines") is long enough to overflow the
   360px basket-aside column at narrow widths (.btn is white-space:nowrap sitewide by design -
   scoped to #rfq-form only so no other page's shorter CTA buttons are affected). */
#rfq-form .btn{width:100%;justify-content:center;white-space:normal;text-align:center}
/* visually-hidden text for screen readers (badge context etc.) */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media(max-width:860px){
  .basket-grid{grid-template-columns:minmax(0,1fr);gap:28px}
  .basket-aside{position:static;min-width:0}
  .bk-item{grid-template-columns:72px 1fr auto;gap:12px}
  .bk-thumb{width:72px;height:56px}
}

/* product-page "other sizes" chips (same machine type, other container sizes) */
.pd-sizes{margin:16px 0 4px}
.pd-sizes-label{display:block;font-size:.72rem;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);font-weight:700;margin-bottom:9px}
.size-chips{display:flex;flex-wrap:wrap;gap:8px}
.size-chip{display:inline-flex;align-items:center;padding:8px 15px;border:1px solid var(--line);border-radius:999px;font-weight:600;font-size:.9rem;color:var(--ink-2);background:#fff;text-decoration:none;transition:border-color .2s,color .2s,background .2s}
a.size-chip:hover{border-color:var(--accent);color:var(--accent)}
.size-chip.active{background:var(--accent);border-color:var(--accent);color:#fff;cursor:default}

/* ---- category landing page: products-first layout ---- */
.cat-hook{background:linear-gradient(180deg,#fff,var(--bg-soft));border-bottom:1px solid var(--line);padding:26px 0 38px}
.cat-hook .crumb{margin-bottom:16px}
.hook-badge{display:inline-block;font-size:.7rem;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--accent-dark);background:var(--accent-soft);border-radius:6px;padding:5px 11px;margin-bottom:15px}
.cat-hook h1{font-size:clamp(1.6rem,2.6vw,2.05rem);line-height:1.18;letter-spacing:-.01em;font-weight:800;margin:0;max-width:600px}
.cat-hook h1 em{color:var(--accent);font-style:normal}
.hook-sub{font-size:1.1rem;color:var(--ink-2);max-width:64ch;margin:.7em 0 0}
.hook-cta{display:flex;gap:12px;flex-wrap:wrap;margin-top:24px}

/* dark stat bar: orange icon + white number + grey label (matches the sample) */
.cstats{background:var(--ink);color:#eef1f5;border-bottom:3px solid var(--accent)}
.cstats .container{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;padding:18px 24px}
.cstat{display:flex;align-items:center;gap:11px;min-width:150px}
.cstat .ic{color:var(--accent);display:inline-flex;flex-shrink:0;font-size:27px;line-height:1}
.cstat .num{font-size:1.35rem;font-weight:800;color:#fff;line-height:1.05}
.cstat .lbl{font-size:.78rem;color:#aeb6c2}
/* sample (Tabler) icon sizing */
.topbar i{font-size:14px;vertical-align:-2px}
.tb-wa{display:inline-flex;align-items:center;gap:5px}
.fb-label i{font-size:15px;vertical-align:-2px;margin-right:1px}
.why4 .feat .ic i{font-size:26px}
.hook-cta .btn i{font-size:17px;vertical-align:-2px;margin-right:3px}

.findbar{background:#fff;border-bottom:1px solid var(--line)}
.findbar .container{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:14px 24px}
.findbar .fb-label{font-size:.74rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-right:4px;display:inline-flex;align-items:center;white-space:nowrap}
.fchip{display:inline-flex;align-items:center;padding:7px 13px;border:1px solid var(--line);border-radius:999px;font-weight:600;font-size:.85rem;color:var(--ink-2);background:var(--bg-soft);transition:border-color .2s,color .2s,background .2s;font-family:inherit;cursor:pointer;white-space:nowrap}
.fchip:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
.fchip.is-extra{display:none}
.findbar.chips-expanded .fchip.is-extra{display:inline-flex}
.fchip.more{background:transparent;border-style:dashed;color:var(--accent-dark)}

/* ===== .eyebrow BASE RULE (2026-08-19, third QA pass) =====================
   ★ A CLASS HAS TO RENDER AS ITSELF EVERYWHERE, NOT WHERE SOMEBODY REMEMBERED. ★
   Until now `.eyebrow` had no styling of its own: seven different context rules
   (.models-head [deleted by V4, see below] / .cat-about / .seo-below / .choose /
   .vs / .sec-head and, added
   earlier today, .body-sec > .container) each re-declared the same six lines. The
   eighth context nobody wrote is `.showcase`, and its two kickers on the HOMEPAGE
   rendered at 16px / weight 400 / near-black / no caps - a small grey sentence
   sitting above a headline, reading like a stray half-line of body copy.
   Two rounds of QA in a row found an unstyled `.eyebrow`. The first was patched by
   adding a seventh context rule; that is why there was still an eighth. So this
   time the class gets a DEFAULT and the pattern ends: any `.eyebrow` anywhere is a
   kicker unless a more specific rule deliberately says otherwise.
   Safe by measurement, not by hope: all 79 `.eyebrow` on the site were computed
   before this landed - 52 at .82rem/800, 25 at .78rem/800, and exactly 2 inheriting
   nothing. The seven context rules all out-specify this one (0,1,0), so those 77 do
   not move by a pixel; only the 2 broken ones change.
   Gated by R11, which now refuses to accept ANY element as a heading unless it
   renders like one - see test_layout_left_alignment.py. Mutation N17 removes this. */
.eyebrow{display:block;color:var(--accent);font-weight:800;font-size:.78rem;
  letter-spacing:.1em;text-transform:uppercase}

/* ★ `.models-head` IS DELETED (2026-08-19, ruling V4). DO NOT PUT IT BACK. ★
   It was the headline of the machine grid on the four main category pages: a flex
   row with the heading on the left and a 「View all N machines →」 button on the
   right, under justify-content:space-between. That block is now written as the
   ordinary `.sec-head` in _build\build.ps1, because:

   1. WHAT IS UNDER IT IS THE SAME BLOCK AS EVERYWHERE ELSE. A full-bleed `.prod-grid`
      card wall - the same component appears under a centred `.sec-head` on 342 other
      pages. Four pages wrote its headline differently for no reason a reader can see,
      and those four are Extrusion Blow Molding / Injection Molding / Injection Blow
      Molding / Moulds: the main nav entry points.
   2. text-align:center ALONE COULD NOT FIX IT, and this was measured rather than
      assumed. The heading sat in a flex item 369-526px wide, so centring the type
      centred it inside that item and the h2's left edge did not move off 44px at all,
      against 632 for every other headline on the page. A flex row cannot be fixed
      with a text property; the structure had to go.
   3. THE BUTTON WAS A DUPLICATE, so removing it costs nothing. The identical
      「View all N machines →」 already sits centred UNDER the card wall (`$moreNote`),
      and assets/js/main.js removes every `.js-show-all` on the site when either one
      is clicked - the two were always one control drawn twice.
   The 「8 moulds」 counter that took the button's place on the Moulds page went with
   it: eight cards are in view; counting them for the reader is not information. */


.prod-card.is-extra{display:none}

/* ===== .cat-about / .seo-below: LEFT-ALIGNED (boss ruling 2026-08-17) =====
   Both blocks used to carry text-align:center on the section and margin:0 auto on
   the 760px .prose inside it. That is what the boss saw as 「有的靠中间,有的靠左边」:
   twelve pages of running body copy set centred, and - worse - the centred .prose
   sat 46px in from the container while any <figure> beside it stayed on the
   container's left edge, so a photo and its own paragraph never lined up.
   text-align:center is gone and margin is 0, so text, media and caption all start
   on the container's left edge. Headings and eyebrows follow the copy left, which
   is the point: they are the top of one column, not a separate centred sign.
   The only things still centred site-wide are the signboards - .sec-head, .cta,
   .statband, .trust. Enforced by _build\tests\test_layout_left_alignment.py. */
.cat-about{background:#fff;border-top:1px solid var(--line);padding:52px 0 12px}
.cat-about .eyebrow{display:block;color:var(--accent);font-weight:800;font-size:.78rem;letter-spacing:.1em;text-transform:uppercase}
.cat-about h2{font-size:clamp(1.45rem,2.5vw,2rem);margin:5px 0 20px}
.cat-about .prose{max-width:760px;margin-left:auto;margin-right:auto}
.cat-about .prose p{margin:0 0 1.05em;line-height:1.75}
.cat-about table{margin-left:auto;margin-right:auto;text-align:left}
.why4{display:grid;grid-template-columns:repeat(4,1fr);gap:22px;margin-top:38px;text-align:left}
/* subcat / group "products-first" text block, same left column as .cat-about */
.seo-below .eyebrow{display:block;color:var(--accent);font-weight:800;font-size:.78rem;letter-spacing:.1em;text-transform:uppercase}
.seo-below h2{font-size:clamp(1.45rem,2.5vw,2rem);margin:5px 0 20px}
.seo-below .prose{max-width:760px;margin-left:auto;margin-right:auto}
.seo-below .prose p{line-height:1.75}
.seo-below table{margin-left:auto;margin-right:auto;text-align:left}

/* ---- Media block: the ONE writing for an in-body video/photo + its caption ----
   Emitted by MediaBlock() in _build\build.ps1 - read the comment there for the
   history. Geometry lives here so all 426 pages change together and no page can
   quietly acquire its own spacing:
     24 / 28         space above the frame / below the whole block
     10              frame to caption
     .media-cap      caption sits at the frame's left edge and is capped to the
                     frame's own width by the inline max-width MediaBlock writes
                     (a 70ch caption under a 330px photo reads as two columns).

   ★ WHERE IT SITS SIDEWAYS - 口径 REPLACED 2026-09-23 BY THE BOSS ★
   ------------------------------------------------------------------
   NEW 口径 (boss, 2026-09-23, looking at the mould page):
     「得确别扭。标题居中,图居中。」
     ★ 画面的对齐跟着它正上方那个标题走。标题居中的,画面居中;
       标题靠左的,画面照旧靠左。★
   The site's heading 口径 is unchanged (ruling V4, commit 2b4ff06f):
   h2 居中 / h3 h4 靠左 / 四家族(横幅、两栏拼版、卡片、页脚)不参与.
   So "which way does this picture face" is READ OFF the heading above it and is
   not a second ruling to keep in step by hand.

   OLD 口径, superseded - do not "restore" it (2026-08-19, docs\设计规范-竖版媒体
   块对齐-V1-20260819.md):
     媒体块的左缘 = 它所在那一栏的左缘 (a 340 frame was an item IN the column, so
     it started where the column started).
   WHY IT CHANGED: under a CENTRED h2 that left-flush 340 frame reads as a picture
   that slid off to one side - 「得确别扭」 - because the heading over it is the
   nearest thing the eye lines it up with, not the paragraph two blocks down. The
   old口径 is still exactly right under an h3/h4, and that half is kept.

   The line above this one used to read 「margin-left 0 - flush with the body
   column. NEVER auto」 while the declaration under it said `margin:24px auto
   28px`. Both halves of that contradiction have now been tried on the live site
   and BOTH give a <760 block the wrong left edge:
     margin-left:0     lands at 44  - outside the column entirely, the
                       「又窄又靠左」 shape v1.0 forbids. Written 2026-08-17.
     margin-left:auto  lands at 470 - centred INSIDE the 760 column while every
                       paragraph beside it starts at 260. Written 2026-08-18.
   Each was pinned as the rule, each shipped, each caused the thing the boss
   reported - 「有的靠中间,有的靠左边」. Arguing about which keyword to write is
   what kept producing wrong answers, because a keyword cannot know how wide the
   parent is and the correct offset depends on exactly that.

   ★ 判据是两条,按上方标题选一条(2026-09-23)。★
     标题居中 -> 量出来的中线等不等于同栏正文的中线;
     标题靠左 -> 量出来的左缘等不等于同栏正文的左缘(旧判据,原样留着)。
   Any writing that satisfies the one that applies is fine. Gate R2/R3/R5 in
   _build\tests\test_layout_left_alignment.py measure the number and pick the
   criterion from the heading above the frame, so this declaration cannot pass a
   block for saying an approved word while landing in the wrong place.

   HOW THIS PARTICULAR WRITING GETS THERE - AND WHY ONE `auto` COVERS BOTH WIDTHS.
   `margin-left:auto;margin-right:auto` centres the block in whatever holds it,
   and everything that holds it is itself centred on the page's one centre line
   (640 at 1280), so:
     760 block inside a .prose (760)      no room to move   -> 260, unchanged ✓
     760 block straight in .container     (1192-760)/2 =216 -> 260, unchanged ✓
     340 block inside a .prose (760)      (760-340)/2 =210  -> 470, centred ✓
     340 block straight in .container     (1192-340)/2 =426 -> 470, centred ✓
     phone, column narrower than the block  auto computes 0 -> flush ✓
   The two 340 lines land on the SAME number from two different containers, which
   is the whole reason one declaration can serve both: the containers share a
   centre line even though they do not share a width. The 114 frames at 760 do not
   move by a pixel - `auto` and the old `max(0,(100%-760)/2)` are the same number
   when the block IS the column.
   THE OLD `max()` WAS LOAD-BEARING FOR THE PHONE and `auto` inherits that job for
   free: when the container is narrower than the block, `auto` resolves to 0 by
   definition, so nothing is dragged off screen.
   FOUR SEPARATE SIDES, NOT `margin:` SHORTHAND: a shorthand rewrites
   margin-left too, which is how the 08-18 regression got in, and R9 already
   bans the same habit in inline styles.
   TWO OPT-OUTS, both later in the file and both narrow:
     .pd-layout / .shell / .basket-grid - the cell IS the column, and its heading
        is set LEFT there, so its pictures stay left. 166 frames, 0 change.
     .media-block.mb-left - one frame on the whole site (the 340 clip on
        products/im-pet-preform-injection-molding-machine.html) whose heading is an
        h3, i.e. left. MediaBlock() writes the class; see build.ps1. */
/* ★ --reading-col IS HALF-DONE ON PURPOSE, AND THIS IS THE OTHER HALF (2026-08-19).
   ★ 「两条规则写同一个数字,就是这个站最后有两个数字的原因」 - so the reading column
   is named once, here. But TODAY IT ONLY FEEDS THE MEDIA-BLOCK OFFSET BELOW. The
   column width itself is still a literal 760 in style.css, in every one of
     .prose  .faq  .sec-head  .quote-box  .cat-about .prose  .seo-below .prose
   so the two halves are decoupled: change this variable and the media blocks move
   while the columns they are supposed to line up with stay put.
   ⚠ WHY THAT IS NOT LEFT NAKED, AND WHY IT IS NOT BEING FIXED IN THIS BATCH
   (lead's ruling, 2026-08-19). Gate R8 allows exactly three widths on this site -
   1240 / 760 / 340 - so the drift QA was worried about cannot happen silently:
   anyone editing .prose to 800 is red at R8 the moment they run the gate, and
   anyone editing this variable to 800 is red the same way. The debt is real but it
   is covered. Closing it properly means editing style.css as well, which is a
   different batch touching different files - and 「小改动别串单线」 says do not
   smuggle it into this one. Written down so it stays a known debt instead of
   becoming a surprise. */
:root{--reading-col:760px}
.media-block{
  margin-top:24px;
  margin-bottom:28px;
  margin-right:auto;
  margin-left:auto;
}
/* ★ THE 「标题靠左」 HALF OF THE 2026-09-23 口径. ★ A frame whose nearest heading is
   an h3/h4 keeps the old behaviour: its left edge sits on the reading column's left
   edge. This is the pre-09-23 declaration, word for word, moved behind a class -
   nothing about that case changed, so nothing about its writing changed either.
   Written by MediaBlock() in _build\build.ps1, never by hand on a page. One frame
   carries it today; the gate reds any frame that carries it under a centred heading
   and any frame that lacks it under a left one, so the count is not a thing anyone
   has to remember. */
.media-block.mb-left{margin-left:max(0px, (100% - var(--reading-col, 760px))/2)}
/* text-align:left IS THE POINT, not an oversight. The FRAME is centred; the words
   under it are not. A caption here is routinely two or three lines and often holds
   a link (the two "New to the process?" guide hand-offs live in one), and centred
   multi-line type is ragged on both edges - harder to read than the body copy it
   explains. Gate R1 fails centred body copy and a caption is body copy.
   居中的是那一栏,不是每一行字。 */
.media-block>.media-cap{margin:10px auto 0;font-size:.92rem;line-height:1.6;text-align:left}
/* A link inside a caption is still a body link and must still LOOK clickable
   (boss ruling 2026-08-04: same colour + no underline as the surrounding text =
   the link is wasted). Two real links live here - the "New to the process?" guide
   hand-offs on products/injection-molding.html and
   products/extrusion-blow-molding.html. Before this batch that caption was a bare
   <p> directly under .container, so it was covered by the plan's
   `main > section > .container > p a` selector; giving every media block ONE
   wrapper moved it one level down and out of that selector's reach.
   Declarations are copied EXACTLY from the approved 2026-08-04 block at the end of
   assets\css\style.css - deep orange, 1.5px underline at 3px offset, no bold, no
   :visited state. It lives here rather than there because that block is pinned
   verbatim AND pinned as the last thing in style.css
   (_build\tests\test_body_link_visibility.py G1.1/G1.2), so it cannot be extended;
   site-extra.css loads after it, so this rule simply continues the same scope.
   NOTE FOR THE LEAD: the plan's own counter therefore reads 112 links / 34 pages
   instead of the pinned 114 / 36. Nothing became invisible - the two links are
   styled by this rule - but the pinned numbers need a decision, so they were left
   untouched and reported rather than quietly edited. */
.media-block>.media-cap a:not(.btn){
  color: var(--accent-dark);
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
}
.media-block>.media-cap a:not(.btn):hover{
  color: var(--accent);
  text-decoration-thickness: 2px;
}
figure.media-block{padding:0}
figure.media-block>img{width:100%;height:auto;display:block;border-radius:10px;margin-left:0}
/* Narrow frames fill the column once the column itself is narrower than the frame.
   340px is a DESKTOP number - it exists so a 9:16 phone clip or a portrait photo is
   not blown up to 760px. On a 430px-wide phone the reading column is 382px, so a
   340px frame stops 42px short of the text above it and the block reads as indented.
   .video-facade.vf-portrait already got this treatment in the first batch (its
   breakpoint was moved 400 -> 640 for exactly this reason); the wrapper did not, and
   the wrapper wins - a 340px .media-block clamps the .vf-portrait inside it and
   silently undoes that fix. So the rule belongs on .media-block, which covers both
   shapes it comes in: <figure> (the three preform photos) and <div> (portrait video
   blocks). 640 is the same breakpoint, deliberately: one number, one reason.
   !important is required, not decorative: MediaBlock writes the width as an inline
   style="max-width:340px" (it is per-call, so it cannot live in this file), and an
   inline declaration outranks a stylesheet rule at every breakpoint without it. The
   caption carries the same inline cap and has to be released with the frame, or the
   picture widens and the line under it stays short. */
@media(max-width:640px){
  .media-block{max-width:100% !important}
  .media-block>.media-cap{max-width:100% !important}
}

/* ---- decision-copy category pages: multi-row find fast ---- */
.findbar-rows .container{display:block;padding:12px 24px}
.findbar-rows .frow{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:5px 0}
.findbar-rows .flab{min-width:120px;font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}

/* how-to-choose decision section */
.choose{background:#fff;border-top:1px solid var(--line);padding:52px 0}
.choose .eyebrow{display:block;color:var(--accent);font-weight:800;font-size:.78rem;letter-spacing:.1em;text-transform:uppercase}
.choose h2{font-size:clamp(1.45rem,2.5vw,2rem);margin:5px 0 0}
/* 880 -> 760 (2026-08-18 morning) -> NO WIDTH OF ITS OWN AT ALL (2026-08-18 second
   patch). 880 was about 110 characters a line, past the 45-90 band this site's column
   is chosen for; flush left that was invisible (it over-ran only on the right),
   centred it also landed 60px left of every other column on the page. It was missed
   for a year because the gate's R5 only scanned div/figure/table and this is a bare
   <p> - that hole closed in the same batch, see COLUMN_TAGS.
   The width is gone entirely now because this paragraph moved INSIDE .sec-head, which
   is already 760 and centred. Two rules setting the same number is how a site ends up
   with two numbers - one of them gets edited and the other does not. Only the type
   settings stay here, and they stay because .sec-head p would otherwise restyle a
   lead nobody asked to restyle. */
.choose .choose-lead{font-size:1rem;color:var(--ink-2);margin-top:14px;line-height:1.7}
.guide-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:30px}
.guide{background:var(--bg-soft);border:1px solid var(--line);border-radius:var(--radius);padding:24px;display:flex;flex-direction:column}
.guide .gh{display:flex;align-items:center;gap:10px;margin-bottom:14px}
.guide .gh i{font-size:24px;color:var(--accent)}
.guide .gh h4{font-size:1.05rem;font-weight:700}
.guide ul{list-style:none;display:flex;flex-direction:column;gap:9px;margin:0;padding:0}
.guide li{font-size:.92rem;color:var(--ink-2);display:flex;gap:8px;align-items:flex-start}
.guide li i{color:var(--accent);font-size:.95rem;margin-top:4px;flex-shrink:0}
.guide-links{margin-top:auto;padding-top:16px;display:flex;flex-direction:column;gap:7px;align-items:flex-start}
.guide-link{display:inline-flex;align-items:center;gap:6px;font-size:.86rem;font-weight:700;color:var(--accent-dark)}
.guide-link:hover{text-decoration:underline}
.choose .closing{margin-top:30px;background:var(--accent-soft);border-radius:var(--radius);padding:22px 26px;display:flex;justify-content:space-between;align-items:center;gap:20px;flex-wrap:wrap}
.choose .closing p{font-size:.98rem;color:var(--ink-2);max-width:760px;margin:0}
.choose .closing .btn{white-space:nowrap}

/* "vs" comparison block */
.vs{background:var(--bg-soft);border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:52px 0}
.vs .eyebrow{display:block;color:var(--accent);font-weight:800;font-size:.78rem;letter-spacing:.1em;text-transform:uppercase}
/* 62ch -> 760px (2026-08-18 morning) -> no width here at all (second patch). `ch`
   could not be checked: the layout gate has to guess a px-per-character figure to
   compare a ch box with a px box, so a ch column is measured on a ruler nobody
   calibrated. Gate R8 refuses ch on a reading column for exactly that reason.
   The 760 that replaced it is gone in turn, because this heading moved INSIDE
   .sec-head - which is 760 and centred already. Same reasoning as .choose-lead
   above: one width, written once, in the block that owns it. */
.vs h2{font-size:clamp(1.35rem,2.3vw,1.85rem);margin:5px 0 0}
.vs-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:24px}
.vs-col{background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:24px}
.vs-col h4{font-size:1.05rem;font-weight:700;margin-bottom:10px;color:var(--accent-dark)}
.vs-col p{font-size:.95rem;color:var(--ink-2);line-height:1.7}
.vs-concl{margin-top:20px;font-size:1rem;color:var(--ink-2);background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:18px 24px;line-height:1.7}
.vs-concl a{color:var(--accent-dark);font-weight:600;text-decoration:underline}
@media(max-width:900px){.guide-grid{grid-template-columns:1fr}.vs-grid{grid-template-columns:1fr}}

/* footer social (entity sameAs corroboration) */
.foot-social{margin-top:16px}
.foot-social .fs-h{display:block;font-size:.8rem;font-weight:700;color:#fff;margin-bottom:8px}
.foot-social a{display:inline-block;margin-right:16px;font-size:.88rem}
.why4 .feat{padding:24px}
.why4 .feat h3{font-size:1.02rem}
.why4 .feat p{font-size:.9rem}
@media(max-width:900px){.why4{grid-template-columns:repeat(2,1fr)}}
@media(max-width:560px){.why4{grid-template-columns:1fr}}

/* ===== customer case studies + click-to-load video facade (2026-07) ===== */
.case-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:26px}
.case-card{display:flex;flex-direction:column;background:#fff;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;text-decoration:none;color:inherit;transition:box-shadow .2s,transform .2s}
.case-card:hover{box-shadow:0 10px 30px rgba(0,0,0,.10);transform:translateY(-3px)}
.case-card .cc-img{aspect-ratio:16/10;overflow:hidden;background:var(--bg-soft)}
.case-card .cc-img img{width:100%;height:100%;object-fit:cover}
.case-card .cc-body{padding:20px 22px 22px;display:flex;flex-direction:column;gap:10px}
.case-card h3{font-size:1.06rem;line-height:1.45;margin:0}
.case-card p{font-size:.92rem;color:var(--ink-2);line-height:1.65;margin:0}
.cc-more{font-weight:600;font-size:.9rem;color:var(--accent-dark)}
.cc-chips{display:flex;flex-wrap:wrap;gap:8px}
.cc-chips span{display:inline-block;font-size:.78rem;font-weight:600;background:var(--bg-soft);border:1px solid var(--line);border-radius:99px;padding:4px 12px;color:var(--ink-2)}
.case-results{list-style:none;padding:0;margin:0 0 1.2em}
.case-results li{display:flex;gap:10px;align-items:flex-start;padding:9px 0;border-bottom:1px dashed var(--line);line-height:1.6}
.case-results li svg{flex:none;margin-top:4px;color:var(--accent-dark)}
.case-callout{background:var(--bg-soft);border:1px solid var(--line);border-left:4px solid var(--accent);border-radius:var(--radius);padding:22px 26px;margin:1.6em 0}
.case-callout .cc-kick{display:block;font-size:.78rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--accent-dark);margin-bottom:8px}
.case-callout h3{margin:0 0 8px;font-size:1.12rem;line-height:1.4}
.case-callout h3 a{color:inherit;text-decoration:none}
.case-callout h3 a:hover{text-decoration:underline}
.case-callout p{margin:0 0 14px;color:var(--ink-2);line-height:1.65;font-size:.95rem}
.video-facade{position:relative;border-radius:var(--radius);overflow:hidden;cursor:pointer;border:1px solid var(--line);aspect-ratio:16/9;background:#000;margin-top:24px;margin-bottom:28px}
/* Same root cause as .detail-photos above: a BARE landscape frame (cases/, news/)
   declared no vertical margin, so the paragraph after it touched its bottom edge
   (news/: 0px) and the heading above left only 12 / 15.4px. 24/28 is the site's
   media unit, the pair .vf-portrait already carries. A frame that is PART of a
   larger unit must stay at zero, and each such context resets it explicitly:
   inside .media-block (the wrapper owns the 24/28 - see the reset just below;
   gate E7b requires 0 there) and inside .video-strip (a grid, `margin:0` below). */
.media-block .video-facade{margin-top:0;margin-bottom:0}
.video-facade img{width:100%;height:100%;object-fit:cover;opacity:.85;transition:opacity .2s,transform .3s}
.video-facade:hover img{opacity:1;transform:scale(1.02)}
.video-facade .vf-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:72px;height:72px;border-radius:50%;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;box-shadow:0 6px 24px rgba(0,0,0,.35);transition:transform .2s}
.video-facade:hover .vf-play{transform:translate(-50%,-50%) scale(1.08)}
.video-facade .vf-label{position:absolute;left:16px;bottom:14px;color:#fff;font-weight:600;font-size:.92rem;text-shadow:0 1px 6px rgba(0,0,0,.6)}
.video-facade iframe,.video-facade video{position:absolute;top:0;left:0;width:100%;height:100%;border:0}
/* once playing, hide poster + play button so letterboxed (vertical) videos sit on black, not on the poster image */
.video-facade.vf-playing img,.video-facade.vf-playing .vf-play,.video-facade.vf-playing .vf-label{display:none}
@media(max-width:900px){.case-grid{grid-template-columns:1fr 1fr}}
@media(max-width:560px){.case-grid{grid-template-columns:1fr}}

/* honeypot field for the self-hosted inquiry endpoint: visually removed, present for bots */
.hp-field{position:absolute !important;left:-9999px !important;top:auto !important;width:1px;height:1px;overflow:hidden}

/* category hero: two-column, text left / machine photo right (was full-width text with dead space) */
.hook-grid{display:grid;grid-template-columns:1.05fr .95fr;gap:44px;align-items:center}
.hook-visual img{width:100%;max-height:330px;object-fit:contain;background:#fff;padding:10px;box-sizing:border-box;border-radius:var(--radius);border:1px solid var(--line);box-shadow:0 14px 34px rgba(0,0,0,.10);display:block}
@media(max-width:900px){.hook-grid{grid-template-columns:1fr;gap:22px}.hook-visual img{max-height:230px}}
/* footer: collapse columns on narrow screens (fixed 4-col grid caused horizontal scroll at 390px) */
@media(max-width:900px){.foot-grid{grid-template-columns:1fr 1fr;gap:28px}}
@media(max-width:560px){.foot-grid{grid-template-columns:1fr;gap:24px}}

/* case-study "Project facts" sidebar: clean single-column stacked list
   (the product-page .pd-meta 2-col grid produced ugly gaps with long case values) */
.case-facts{list-style:none;margin:0 0 4px;padding:0}
.case-facts li{display:flex;flex-direction:column;gap:3px;padding:11px 0;border-bottom:1px solid var(--line)}
.case-facts li:last-child{border-bottom:0}
.case-facts li span{font-size:.72rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-3,#8a8a8a)}
.case-facts li b{font-size:.96rem;font-weight:600;color:var(--ink);line-height:1.4}

/* click-to-enlarge lightbox - shared by every enlargeable picture (main.js; enlarges in place, never links to the file) */
.detail-photos img,.photo-grid .ph{cursor:zoom-in}
.img-lightbox{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.9);display:flex;align-items:center;justify-content:center;padding:24px;cursor:zoom-out}
.img-lightbox img{max-width:96vw;max-height:92vh;object-fit:contain;border-radius:6px;box-shadow:0 12px 44px rgba(0,0,0,.55);cursor:default}
.ilb-close{position:fixed;top:14px;right:20px;font-size:36px;line-height:1;background:none;border:0;color:#fff;cursor:pointer;padding:4px 10px}
.ilb-nav{position:fixed;top:50%;transform:translateY(-50%);width:48px;height:64px;font-size:44px;line-height:1;background:rgba(0,0,0,.35);border:0;border-radius:6px;color:#fff;cursor:pointer;padding:0}
.ilb-prev{left:10px}
.ilb-next{right:10px}
.ilb-count{position:fixed;bottom:14px;left:0;right:0;text-align:center;color:#fff;font-size:.9rem;opacity:.85;pointer-events:none}

/* About page: exhibitions & customer-plant video strip (YouTube facades, 3-up) */
.video-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
.video-strip .video-facade{margin:0}
@media (max-width:820px){.video-strip{grid-template-columns:1fr}}

/* Megamenu group headings are now links to their group landing pages: keep the
   original heading look, accent only on hover */
.mega-col h4 a{color:inherit;text-decoration:none}
.mega-col h4 a:hover{color:var(--accent)}

/* Subcategory conversion hook bar (moved up from sidebar bottom) */
.subcat-hook{display:flex;align-items:center;gap:18px;flex-wrap:wrap;
  background:var(--accent-soft,#fdf0e6);border:1.5px solid var(--accent,#F07A1A);
  border-radius:12px;padding:16px 20px;margin-bottom:20px}
.subcat-hook .sh-txt{flex:1;min-width:230px}
.subcat-hook .sh-txt b{display:block;font-size:1.02rem;margin-bottom:2px}
.subcat-hook .sh-txt span{font-size:.86rem;color:var(--muted)}
.subcat-hook .sh-acts{display:flex;gap:10px;flex-wrap:wrap}
.btn-wa{background:#25d366;color:#fff}
.btn-wa:hover{background:#1eb457;color:#fff}
@media(max-width:560px){.subcat-hook{flex-direction:column;align-items:stretch}
  /* .btn is white-space:nowrap site-wide, so a long label inside this bar sets its own
     minimum width and drags the whole document wider than the phone - that is how
     /pet-preform-mould/ came to scroll sideways at 390 (QA 复走查 2026-09-23). Let the
     label wrap here instead; min-width:0 is the half that actually releases the flex item. */
  .subcat-hook .sh-acts .btn{flex:1;justify-content:center;min-width:0;white-space:normal;text-align:center}}

/* Flagship / "Popular" pinned card */
.prod-card.is-flagship{border:2px solid var(--accent,#F07A1A);
  box-shadow:0 6px 20px rgba(240,122,26,.16)}
.prod-flag{position:absolute;top:8px;left:8px;z-index:2;background:var(--accent,#F07A1A);
  color:#fff;font-size:.68rem;font-weight:800;letter-spacing:.03em;padding:3px 9px;border-radius:20px}

/* Portrait / short-video frame for phone-shot 9:16 clips (don't letterbox them in 16:9)

   margin:0 (2026-08-17, boss ruling 全部靠左). It was margin:0 auto, and that ONE
   word centred a 340px video inside a column of left-aligned text on 70 product
   detail pages - the single widest-reaching case of what the boss reported. Every
   surrounding paragraph started at the column's left edge and the clip did not.

   The breakpoint moved 400px -> 640px at the same time. 400px was too narrow to
   cover the phones people actually hold: on a 430px iPhone 15 Pro Max the heading
   sat at x=24 and the video at x=38, a 14px stagger that read as a mistake because
   it WAS one. Below 640px the frame simply fills the column, so it is flush on both
   sides; above it, it is 340px flush left. Enforced by
   _build\tests\test_layout_left_alignment.py (R2/R3, measured at 1280 and 390). */
/* ★ 口径 REPLACED 2026-09-23: IT FOLLOWS THE HEADING ABOVE IT. ★
   Boss, looking at the mould page: 「得确别扭。标题居中,图居中。」 A 340 frame under
   a CENTRED h2 is centred; under an h3/h4 (or inside the two-column family, whose
   headings are set left) it keeps the left edge it has always had. Full reasoning at
   the .media-block rule near the top of this file - one 口径, written once.
   This selector reaches the TWO bare portrait frames (news + cases) plus the 74 that
   sit inside a .media-block. The wrapped 74 do not move: their wrapper IS 340 wide,
   so `auto` and `0` are the same number inside it. The news frame centres (its h2 is
   centred); the cases frame stays left through the two-column opt-out written into
   the block at the foot of this file - it is the WRAPPER-class opt-out that could
   not reach a frame with no wrapper, so the frame's own class was added there.

   ---- the 2026-08-19 ruling this replaces, kept because its arithmetic is still
   ---- the 「标题靠左」 half and must not be re-derived from scratch:
   ★ IT MUST SHARE A LEFT EDGE WITH THE 760 COLUMN ABOVE IT (2026-08-19). ★
   依据:docs\设计规范-竖版媒体块对齐-V1-20260819.md §3/§6.
   The previous version of this comment said the opposite - 「it does NOT share a
   left edge with the 760 column above it - 470 vs 260 - and it is not supposed
   to: gate R3 compares centre lines for exactly this pair」 - and R3 was changed
   to match it on 08-18. The designer overturned that on 08-19: a 340 clip is not
   a column of its own, it is an item IN the column, ranked with a paragraph or a
   table, so it starts where they start. Two boxes of different widths that share
   a centre line CANNOT share a left edge, and what the eye follows down a page of
   left-set type is the left edge, not the centre. R3 compares LEFT EDGES again.

   (2026-08-19 wrote `margin-left:0` here; 2026-09-23 makes it `auto`. The paragraph
   below is the 08-19 argument for `0`, left standing because it still explains why
   `auto` was wrong THEN and why the cases-page frame needs its own opt-out NOW.)
   Which is why this was `margin-left:0` and not `0 auto`: this rule only reaches
   the TWO bare portrait frames on the site (the ones with no .media-block
   wrapper, on the news and cases pages - every other one is wrapped and takes its
   position from the .media-block rule above). `auto` centred them inside their
   own column: 470 on the news page against copy at 260, and 254 inside a
   .pd-layout cell whose copy is at 44. That second one also slipped through the
   `.pd-layout .media-block{margin-left:0}` opt-out at the foot of this file,
   because that opt-out is written on the WRAPPER class and these two have no
   wrapper. 0 is right here for the same reason `max()` is right up there: it is
   the value that puts the frame on its own column's left edge.
   Written as separate sides, no `margin:` shorthand - see the media block above.

   340 stays 340: at 9:16 that is 604px tall, one laptop screen. 440 would be 782 and
   a buyer would have to scroll to see one video.

   ★ 24 / 28 ABOVE AND BELOW - THE SAME TWO NUMBERS THE WRAPPED COMPONENT USES
   (2026-08-19, 依据 docs\设计规范-竖版媒体块对齐-V1.1-20260819.md 补2③). ★
   Until today these two frames carried NO vertical margin at all, and nothing else
   in their neighbourhood supplied one either. Measured in Chromium:
     news article   15.4px above (390: 10.2), and ZERO below - a paragraph sitting
                    flat against the bottom edge of a 604px video, no gap at all
     case study     12px above, 38.4 below
   against 24 / 28 everywhere the wrapped `.media-block` is used. Three spacings for
   one component across four pages. 「对齐修好了、文字却贴着视频,那叫技术全绿、页面
   不能看。」 The 24 above and 28 below are not new numbers - they are the ones the
   .media-block rule at the top of this file has always used; these two frames were
   simply never given them, because they have no wrapper to inherit them from.

   ⚠ THE WRAPPED FRAMES MUST NOT GET THEM TWICE. This selector reaches EVERY
   portrait frame, and 72 of the 74 sit inside a .media-block that already spaces
   itself 24/28. Adding these numbers there too would double the gap on 72 pages
   while fixing 2 - the trap the ruling names by hand. The rule under this one puts
   the wrapped ones back to zero; it is more specific and later, so it wins on both
   counts, and gate E7 measures every inner frame on every page to prove it (740
   inner-frame checks per run, all required to read 0).
   The RENDERED gap is what was wrong here and the rendered gap is what is now
   gated: E8 measures the space between boxes, not these declarations, because
   `max(mine, my neighbour's)` is a number neither declaration holds.
   A gap LARGER than 28 below is normal and correct - a heading that follows brings
   its own margin-top:2.4rem and 154 wrapped frames render 38.4 that way. The case
   study still reads 38.4 below after this change, and that is it agreeing with the
   other 154, not disagreeing with the standard. */
.video-facade.vf-portrait{aspect-ratio:9/16;max-width:340px;margin-top:24px;margin-bottom:28px;margin-left:auto;margin-right:auto;background:#0a0a0a;border-radius:16px}
/* No double spacing: the wrapper already owns the 24/28 for the 72 frames it holds. */
.media-block .video-facade.vf-portrait{margin-top:0;margin-bottom:0}
.video-facade.vf-portrait img{object-fit:cover}
.video-facade.vf-portrait video,.video-facade.vf-portrait iframe{object-fit:contain;background:#000}
.video-facade.vf-portrait .vf-play{width:60px;height:60px}
@media(max-width:640px){.video-facade.vf-portrait{max-width:100%}}

/* ===== Jerry Can main page: persistent floating quote CTA (Aria design, 2026-07) =====
   .quote-float is a geometry-only shell (position/width/z-index/bottom/right + the fade
   transition) - visuals inherit from .quote-card (desktop mini-card) and .btn.btn-primary
   (button); colours/radius/shadow/font-size all come from those existing components and :root.
   Positioning copies the .wa-float pattern above (fixed + z-index between wa-float's 90 and
   the header's 100 + the env(safe-area-inset-bottom) precedent at style.css:357).
   Desktop: floats above .wa-float. Mobile (<=900px, same breakpoint as the rest of the site):
   becomes a full-width bottom bar, button only - main.js toggles .is-visible past the hero. */
.quote-float{position:fixed;right:24px;bottom:92px;width:280px;z-index:95;
  opacity:0;pointer-events:none;transform:translateY(10px);transition:var(--ease)}
.quote-float.is-visible{opacity:1;pointer-events:auto;transform:none}
@media(max-width:900px){
  .quote-float{left:0;right:0;bottom:env(safe-area-inset-bottom,0px);width:100%}
  .quote-float .quote-card{padding:0;border:0;border-radius:0;box-shadow:none}
  .quote-float h3,.quote-float p{display:none}
  .quote-float .btn{width:100%;min-height:58px;border-radius:0}
  /* required avoidance: the bottom bar sits under .wa-float's corner and over the footer/
     inquiry form unless both are pushed clear - scoped to this page only via .has-quote-float
     (main.js adds it unconditionally when .quote-float is present; plain class instead of
     :has(.quote-float) so this still works on older Android WebViews without :has() support),
     no other page emits this markup so no other page is affected */
  body.has-quote-float .wa-float{bottom:calc(72px + env(safe-area-inset-bottom,0px))}
  body.has-quote-float{padding-bottom:calc(58px + env(safe-area-inset-bottom,0px))}
}

/* ==========================================================================
   窄栏居中 — THE SECOND LAYER  (boss ruling 2026-08-18: 居中)
   ==========================================================================
   THE WHOLE RULE, in one line:
     a block either FILLS the 1240 page frame, or it is 760 wide AND centred.
     「又窄又靠左」 does not exist anywhere on this site.

   THE SITE HAS EXACTLY TWO WIDTH LAYERS. A third is a bug, every time:
     layer 1  .container   max-width:1240  margin:0 auto   <- NOT TOUCHED, EVER
     layer 2  the reading column, 760, centred             <- this block

   ★ AUTO GOES ON THE COLUMN, NEVER ON THE SHELL. ★  The 2026-08-17 second patch
   deleted 16 hand-written `class="container" style="max-width:900px"` shells and
   got that count to zero. It has to stay zero. Narrow the shell instead of the
   column and the shell centres itself while the product grid, the inquiry box and
   the FAQ stay on the 1240 frame - one page, two centre lines 170px apart, which
   is verbatim what the boss complained about on 08-17.
   To narrow one section, put its content in .prose. Do not wrap it in a new box.

   WHY 760 AND NOT 820 while we are in here anyway: measured in a browser on six
   representative pages, a 760 column runs 79-91 characters a line and the readable
   band is 45-90, so 760 is already at the ceiling. 820 would be 85-98, over it -
   and the argument that persuaded the boss to accept centring at all was that
   over-long lines make the eye skip. Widening it in the same breath would undo the
   reason he said yes.

   ONE MORE TIME, because it has been misread once already and cost three rounds:
   ★ 居中的是那一栏,不是每一行字。★ Box centred + type flush left = right.
   text-align:center on a paragraph = wrong, and gate R1 fails it.

   Enforced by _build\tests\test_layout_left_alignment.py (R1-R9).
   ========================================================================== */
.prose{margin-left:auto;margin-right:auto}

/* The tutorial pages' closing CTA card. It carried an inline max-width:520 (and one
   page 560) and sat flush left - two more numbers, and the narrow-and-left shape the
   ruling forbids. The inline widths are deleted in _build\build.ps1; this puts the
   card on the one column width like everything else. */
.quote-card{max-width:760px;margin-left:auto;margin-right:auto}

/* --------------------------------------------------------------------------
   BARE COPY: the blocks that have no column of their own
   --------------------------------------------------------------------------
   ★ THIS IS THE PART THAT CENTRING *CREATES*, and it is the one most easily
   missed. ★ A number of headings, paragraphs, lists and tables sit straight in the
   1240 frame with no max-width and no .prose around them. While everything was
   flush left they were invisible - the columns were at 44px and so were they. The
   moment the columns move to 260px, these are the only things still at 44 and the
   page has "some centred, some left" all over again: the boss's exact complaint,
   reappearing somewhere new the same day it was fixed.

   WHY A CLASS ON THE SECTION AND NOT ON EACH ELEMENT. Threading every one of these
   through .prose means editing several dozen template sites in build.ps1, including
   regions another batch is mid-way through rewriting. One marker on the section is
   six template edits. This is the fix for THIS batch, not the end state; the end
   state is still "content lives in .prose". Gate R8 is what stops this rule from
   having to be right forever - anything that escapes it is failed by name, rather
   than quietly inheriting a stray width.

   ★ 2026-08-18, SAME DAY, SECOND PATCH: THE SELECTOR IS NOW OPT-IN. ★
   The first cut of this rule was written structurally - `main > section >
   .container > p` - under a comment claiming 「every block that is SUPPOSED to fill
   the frame is a <div>/<section> with a class, so a bare tag can only be body copy;
   一个都不会被误收」. That claim was FALSE, and it was false on the homepage's first
   screen. `.hero` and `.cta` put their running copy STRAIGHT into `.container`:

       <section class="hero"><div class="container"><p>…the lead paragraph…</p>

   so this rule out-specified `.hero p{max-width:54ch}` - (0,1,3) beats (0,1,1) - and
   dragged the hero lead to left 260 while the badge above it (44), the H1 above that
   (44) and the button row below it (44) all stayed put. 靠左、靠左、突然缩进去、靠左:
   the exact shape the boss reported on 08-17, put back on the most important screen
   on the site the same day it was fixed. On 71 more pages it squeezed `.cta h2` from
   1192 to 760 and `.cta p` from 622 to 760 - invisible to the eye only because `.cta`
   carries text-align:center, so the band still LOOKED symmetrical.

   SO A SECTION NOW ASKS FOR THIS RULE BY NAME: it carries `.body-sec`.
   A negative list - :not(.hero):not(.cta):not(.page-head)… - would fix today and rot
   tomorrow, because the next signboard class anyone invents is silently back in
   scope and the same bug returns wearing a different class name. Opt-in cannot rot
   that way: a new signboard simply never asks.

   ★ AND THE BANNER EXCLUSION IS NOW PROVED RATHER THAN LUCKY. ★ `.page-head` (419
   pages, 77 of them carrying a lead paragraph) never matched the old selector - but
   only because it happens to be written `<div class="page-head">` and the selector
   demanded `section`. Anybody "tidying" that div into a semantic <section> would
   have swung all 77 banner leads into the middle of the page overnight with the gate
   still green. Gate R10 no longer trusts the tag: it re-runs the match with every
   banner and signboard block RENAMED to <section> and requires zero hits either way.

   Enforced by R10 in _build\tests\test_layout_left_alignment.py, which reads these
   selectors OUT OF THIS FILE rather than keeping a copy of them.
   -------------------------------------------------------------------------- */
.body-sec > .container > p,
.body-sec > .container > ul,
.body-sec > .container > ol,
.body-sec > .container > h2,
.body-sec > .container > h3,
.body-sec > .container > h4,
.body-sec > .container > figure,
.body-sec > .container > table,
.body-sec > .container > blockquote,
.body-sec > .container > .video-facade,
.body-sec > .container > .eyebrow,
.body-sec > .container > .hook-badge{
  max-width:760px;
  margin-left:auto;
  margin-right:auto;
}
/* ★ display:block IS NOT OPTIONAL HERE. ★ .eyebrow is a <span>, and margin:auto does
   NOTHING to an inline box - it neither centres it nor errors, it is simply ignored.
   The first candidate stylesheet for this batch left it out and rendered a small
   orange kicker still jammed against the left margin with its own headline already
   centred underneath it: worse-looking than not changing anything at all.
   Gate R2 fails an inline element outright rather than pretending to measure one. */
/* ★ AND IT HAS TO LOOK LIKE A KICKER, NOT JUST BE CALLED ONE ★ (2026-08-19, QA #2)
   Every other context on this site re-declares the kicker styling for its own eyebrow -
   .sec-head, .seo-below, .cat-about, .choose, .vs (and `.models-head` until V4
   deleted it) all carry the same six
   declarations. A bare `.eyebrow` sitting straight in a `.body-sec` container matched
   none of them, so on /pet-preform-mould/ 「Start here」 and 「How we check the fit」
   rendered at 16px / weight 400 / plain black: identical to running body copy.
   That was survivable while they were flush left. The V3 rule below CENTRES them, and a
   centred line of what looks exactly like body copy is the one thing R1 exists to
   forbid - so the gate would have been enforcing the letter of V3 while producing the
   defect R1 was written against, because it trusted a CLASS NAME to tell it what the
   reader sees. Styling them is the fix; R7 now also refuses to accept anything as a
   heading unless it renders like one. */
.body-sec > .container > .eyebrow{display:block;color:var(--accent);font-weight:800;
  font-size:.78rem;letter-spacing:.1em;text-transform:uppercase}

/* ★ BARE HEADINGS TAKE CENTRED TYPE (2026-08-19, ruling V3) ★
   ------------------------------------------------------------------------------
   The block above gives these elements a 760 box on the page's centre line. That
   was enough while `.sec-head col` existed, because a heading and its column were
   only ever judged against EACH OTHER. Under V3 a body-section headline has one
   shape site-wide - 760 box, CENTRED type - and these bare headings are headlines
   just as much as `.sec-head` is; `.cat-about` and `.seo-below` write them without
   the component, that is the only difference.
   Leaving them out was not an option: `.cat-about` is the MIDDLE of the three-way
   fold the designer found on the category pages (video LEFT / .choose CENTRED /
   .cat-about LEFT / .vs CENTRED / FAQ LEFT). Fixing the two ends and not the
   middle would have turned a three-fold page into a one-fold page and called it
   done - which is how this defect has now survived three batches.

   ONLY THE HEADINGS. p / ul / ol / table / figure / blockquote in the list above
   are BODY COPY and stay flush left: 居中的是那一栏,不是每一行字. R1 still fails
   anyone who centres a paragraph, and that rule has not moved since July.
   h3 and h4 are in here on purpose and it is not scope creep - the two bare <h3>
   on products/im-pet-preform-injection-molding-machine.html each head a media
   block, i.e. they are section headlines in every sense except the class name.
   Centring h2 and leaving them left would have put a fresh fold on that one page.
   Enforced by R7 in _build	ests	est_layout_left_alignment.py; mutation N14
   removes this rule and requires the red. */
/* ★ h3 AND h4 CAME OUT OF THIS RULE (2026-08-19, ruling V4). ★ V3 put them in and
   defended it as 「they head a media block, i.e. section headlines in every sense
   except the class name」. The designer looked at the result on the page and ruled the
   other way: 「把三个字的小节标题居中吊在一段左对齐正文上方，才是难看」. The two bare
   <h3> on products/im-pet-preform-injection-molding-machine.html were the whole reach
   of that clause, and V3 had created a NEW fold with them - a centred three-word
   sub-heading over left-set copy. Under V4 the tag decides: h2 centred, h3/h4 left.
   Removing them here is what turns those two back. */
.body-sec > .container > h2,
.body-sec > .container > .eyebrow{text-align:center}

/* ★ THE THIRD SHAPE (added 2026-08-19 after QA re-walked the batch) ★
   ------------------------------------------------------------------------------
   The rule above reaches a heading that is a DIRECT CHILD of `.container`. QA found
   that the site has a third way of writing a section heading: wrapped in a box of
   its own, one or two levels down. On /pet-preform-mould/ the heading 「Mould steel
   and cavity life」 is written inside its own `.prose`, so it stayed flush left while
   the three headings around it - SAME 32px/800 type - were centred. Not a hierarchy
   difference the reader can rationalise: same size, different alignment, one screen.

   Scoped to `.body-sec` ON PURPOSE, and this is the whole reason the opt-in marker
   was introduced on 08-18. `.prose > h2` also occurs on the long-form pages (guides
   and other article pages, 12 of them), whose sections carry NO `.body-sec` - and
   those pages are internally consistent, every heading flush left, so whether a
   long-form article is a different page type with its own rules is a question for
   the designer, NOT something to settle by writing a wider selector. Opting in by
   name keeps the two apart without anybody having to guess.
   Reach today: exactly 1 page. Verified by markup scan, not assumed.

   h3/h4 are deliberately NOT included here. Inside a `.prose` flow they are
   sub-headings of a running article (「Neck finish」「How many cavities」…), which is
   a different role from a section heading - QA agreed the roles differ and asked for
   the distinction to be WRITTEN DOWN in the spec rather than inferred from depth.
   That is a designer ruling and it is pending; R11 below counts and PRINTS every
   heading nobody has ruled on, so the debt cannot go quiet again. */
/* ★ V4: EVERY BODY-FLOW h2 IS CENTRED, WHATEVER BOX IT IS WRITTEN IN. ★
   ------------------------------------------------------------------------------
   The V3 version of this line read `.body-sec .prose > h2` and its comment argued
   that long-form articles (guides, news, privacy) might be a page TYPE of their own
   with its own alignment, and that only the designer could say. The designer said:
   there is no page type. A page type is a variable, a variable needs a lookup, and
   「查表才知道」 is the thing that produced five rounds of rework. So the marker is
   dropped and the rule follows the tag.

   NOT TAKEN ON TRUST - RENDERED AND LOOKED AT. Five tutorials, five news articles
   and the privacy page were filled with real copy and read at 1280 and 390 with the
   h2 centred: they come out looking like the category pages, and nothing reads worse.
   It also removes a fold that exists on the shipped news pages today, where two
   flush-left h2 sit above a centred CTA band inside one article.

   REACH, COUNTED: 13 pages (12 `div.prose` + privacy's `article.prose`), 39 h2.
   `.quote-box` joins on the same line for the same reason - it is 760 and centred
   already, and on ibm-pet / im-pet-preform / pet-preform-mould its h2 is the ONLY
   flush-left thing on the page, i.e. it IS the fold. There is deliberately no
   「form card」 exception: the orange inquiry band on 71 other pages has had a
   centred heading, centred copy and a centred button since the site launched.

   THE TWO-COLUMN FAMILY OPTS BACK OUT on the next line, and it has to be written
   out rather than left to specificity: `.prose` and `.quote-box` also appear inside
   `.pd-layout` (335 product + 6 case pages) and `.shell` (10 subcategory pages),
   where the cell IS the column (spec 1.10). Without the opt-out this one line would
   have swung 341 product pages nobody asked to change.
   Gated by R7 (tag-name ruling) in _build\tests\test_layout_left_alignment.py;
   mutations N15 and N18 remove each half and require the red. */
.prose > h2,
.quote-box > h2{text-align:center}
.pd-layout .prose > h2,.shell .prose > h2,.basket-grid .prose > h2,
.pd-layout .quote-box > h2,.shell .quote-box > h2,
.basket-grid .quote-box > h2{text-align:left}

/* --------------------------------------------------------------------------
   TWO-COLUMN PAGES: the cell IS the column - do not centre a second time
   --------------------------------------------------------------------------
   Product detail (335) + cases (6) use .pd-layout, subcategory (10) uses .shell,
   the basket uses .basket-grid. These pages are NOT centred and that is deliberate:
     1. Re-centring a 760 column inside an 828 cell moves it 34px and gains nothing,
        while knocking the article out of line with the full-width imagery above it.
     2. The complaint this whole batch answers - 「明明右边有很多空位」 - cannot
        happen here. What is on the right is the rail.
   A buyer stepping from a category page (column at 260) into a product page (column
   at 44) does see a sideways shift; a two-column page announces itself at a glance,
   and every B2B catalogue on the internet works this way.
   -------------------------------------------------------------------------- */
.pd-layout .prose,.pd-layout .faq,.pd-layout .quote-box,.pd-layout .quote-card,
.pd-layout .media-block,.pd-layout .media-block > .media-cap,.pd-layout table,
.shell .prose,.shell .faq,.shell .quote-box,.shell .quote-card,
.shell .media-block,.shell .media-block > .media-cap,
.basket-grid .quote-box{margin-left:0;margin-right:0}
/* ★ THE BARE PORTRAIT FRAME JOINS THE OPT-OUT (2026-09-23). ★ Every line above is
   written on a WRAPPER class, and one frame on the site has no wrapper: the 9:16 clip
   on cases/hdpe-daily-chemical-bottle-lines-household-manufacturer-bangladesh.html,
   which sits straight in a .pd-layout cell. It slipped through this mesh once before
   (08-19, it centred itself at 254 while its own copy sat at 44) and it would slip
   through again now that the shared .vf-portrait rule says `auto`. Written on the
   frame's own class so it cannot.
   margin-right stays `auto`, not 0: this is a 340 frame in an 828 cell, and 0 on both
   sides would leave the used value to the browser rather than pinning the left edge.
   Two-column headings are set left (.pd-layout .prose > h2 above), so a left frame
   here IS the 09-23 口径, not an exception to it. */
.pd-layout .video-facade.vf-portrait,
.shell .video-facade.vf-portrait,
.basket-grid .video-facade.vf-portrait{margin-left:0;margin-right:auto}

/* ==========================================================================
   MOULD SECTION (2026-09-22) - products/moulds.html, the mould category pages,
   the single-tool pages and the two mould case pages.
   Written from the boss-approved mock-ups
   docs/design/2026-09-22-模具总页-效果图-v1.html and
   docs/design/2026-09-22-翻盖模单品页-效果图-v1.html.
   Every class is prefixed `m-` so nothing here can reach an existing page.
   Colours, radii, shadows and buttons come from the variables in style.css -
   no second visual system. Widths are only 1240 / 760 / 340 / 100% (gate R8 in
   _build/tests/test_layout_left_alignment.py refuses any other number, and
   refuses `ch` outright, which is why the mock-up's 18ch / 52ch caps are gone).
   ========================================================================== */

/* picture / video box: one 4:3 box, one 16:9 box, nothing else */
.m-shot{position:relative;aspect-ratio:4/3;overflow:hidden;border-radius:var(--radius-sm);
        background:var(--bg-soft);border:1px solid var(--line)}
.m-shot.wide{aspect-ratio:16/9}
.m-shot.fit{background:#fff}
.m-shot.tall{aspect-ratio:3/4;background:#0a0a0a}
/* a portrait box on its own takes the picture's own shape (--mr, build.ps1 MouldTallShape): no strips beside it. Rows / card grids override. 老板 2026-09-27 */
.m-shot.tall{aspect-ratio:var(--mr,3/4)}
/* its hairline is drawn over the picture, not around it, so the 1px border does not eat into the
   aspect-ratio box and leave a 1px strip (same trick as .m-row) */
.m-shot.tall{border:0}
.m-shot.tall::after{content:"";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 1px var(--line);pointer-events:none}
/* the hero picture/clip beside the intro takes its own shape too (a 4:3 photo in the 16:9 box
   left strips). At the column's full width a portrait clip would stand ~970px, taller than a
   laptop screen, so the box is capped at the height the old 3:4 box had (~720px) and narrows to
   the picture's own width at that height, centred in its column */
.hook-visual .m-shot{aspect-ratio:var(--mr,16/9);max-width:calc(var(--mr,1.7778) * 720px);margin-inline:auto}
.m-shot.tall video,.m-shot.tall img{object-fit:contain}
.m-shot.fit img{object-fit:contain}
.m-shot img{width:100%;height:100%;max-height:none;object-fit:cover;padding:0;border:0;
            border-radius:0;box-shadow:none;background:transparent}
.m-shot video{width:100%;height:100%;object-fit:cover;display:block;background:#0a0a0a}
/* a drawing's "open the full file" link fills the box, so the picture inside sizes exactly as without it */
.m-shot > .m-zoom{display:block;width:100%;height:100%;cursor:zoom-in}
.m-cap{font-size:.85rem;color:var(--muted);margin-top:10px;line-height:1.5}

/* first screen: rolling video banner (overview page) */
.m-vbanner{position:relative;overflow:hidden;background:#0a0b0e;
           min-height:clamp(430px,44vw,600px);display:flex;align-items:center}
.m-vbanner>video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0}
.m-vbanner::after{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(90deg,rgba(8,9,12,.88) 0%,rgba(8,9,12,.70) 46%,rgba(8,9,12,.40) 100%)}
.m-vbanner .container{position:relative;z-index:2;padding-top:46px;padding-bottom:46px}
.m-vbanner .crumb,.m-vbanner .crumb a{color:#c3c8d1}
.m-vbanner .crumb a:hover{color:#fff}
.m-vbanner .hook-badge{background:rgba(240,122,26,.20);color:#ffb877}
.m-vbanner h1{color:#fff;font-size:clamp(1.9rem,3.6vw,3rem);line-height:1.14;margin:0 0 16px}
.m-vbanner h1 em{color:var(--accent);font-style:normal}
.m-vbanner .m-sub{color:#d2d6de;font-size:1.08rem;line-height:1.7;margin:0 0 28px}
.m-vbanner .btn-ghost{color:#fff;border-color:rgba(255,255,255,.5)}
.m-vbanner .btn-ghost:hover{border-color:#fff;background:rgba(255,255,255,.08)}

/* first screen on the category / single-tool / case pages. The band, the crumb, the
   badge and the two-column split are the site's existing .cat-hook / .hook-grid /
   .hook-text / .hook-visual - same shape, same family name, nothing new to exempt.
   Only the picture side differs: a click-to-play clip in a 16:9 or 3:4 box instead
   of a bare <img>. */
.m-sub{font-size:1.08rem;color:var(--ink-2);margin:.7em 0 0;line-height:1.7}
.hook-visual > figure{margin:0;width:100%}

/* the two real lead-time numbers */
.m-leadbar{background:var(--ink);color:#fff;border-top:3px solid var(--accent)}
.m-leadbar .container{display:grid;grid-template-columns:1fr 1fr;gap:24px;padding:30px 24px}
.m-leadbar.one .container{display:flex;gap:28px;align-items:center;justify-content:space-between;
                          flex-wrap:wrap;padding:26px 24px}
.m-cell{display:flex;flex-direction:column;gap:4px}
.m-cell .k{font-size:.78rem;letter-spacing:.11em;text-transform:uppercase;color:#aeb6c2;font-weight:800}
.m-cell .v{font-size:clamp(1.5rem,3vw,2.1rem);font-weight:900;color:#fff;line-height:1.1}
.m-cell .v em{font-style:normal;color:var(--accent)}
.m-cell .n{font-size:.88rem;color:#aeb6c2}

/* one-line spec strip on a single-tool page */
.m-facts{list-style:none;padding:0;margin:26px 0 0;display:grid;grid-template-columns:1fr 1fr;gap:1px;
         background:var(--line);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
.m-facts li{background:#fff;padding:15px 18px}
.m-facts .k{display:block;font-size:.72rem;font-weight:800;letter-spacing:.09em;text-transform:uppercase;
            color:var(--muted)}
.m-facts .v{display:block;font-size:1.02rem;font-weight:700;color:var(--ink);margin-top:3px}
.m-facts .v.ask{color:var(--accent-dark)}

/* routing cards: by the PART the buyer sells, not by the process */
.m-routes{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;align-items:stretch}
/* every routing card the same shape: one media ratio, cards stretch to the tallest in the row */
.m-routes .m-route{height:100%}
/* the card picture box (one shape per card group, contain, white round the picture) is set
   in the equal-height block at the end of the mould section - one rule for all four card
   grids. The four .m-routes-only lines that used to sit here (16:9 / 4:3 black / cover) are
   gone with it (2026-09-23, 老板选方案① + 卡片选乙). */
/* named index of every mould page - no "other" bucket */
.m-index{margin-top:26px;border-top:1px solid var(--line);padding-top:18px}
.m-index h3{font-size:1rem;margin:0 0 10px}
.m-index ul{list-style:none;padding:0;margin:0;display:grid;grid-template-columns:repeat(4,1fr);gap:6px 18px}
.m-index a{font-size:.9rem;color:var(--ink-2);text-decoration:none;border-bottom:1px solid transparent}
.m-index a:hover{color:var(--accent-dark);border-bottom-color:currentColor}
@media(max-width:1080px){.m-routes{grid-template-columns:repeat(2,1fr)}.m-index ul{grid-template-columns:repeat(2,1fr)}}
/* one card per row on a phone. Two columns left each media cell 157x87 CSS px, and a portrait
   picture inside one of those is 49x87 - there is nothing to see (QA 复走查 2026-09-23 第 4
   项). Full width is 350x197 for a landscape cell and 262x197 for a portrait one. */
@media(max-width:640px){.m-routes{grid-template-columns:1fr;gap:14px}.m-index ul{grid-template-columns:1fr}}
.m-route{background:#fff;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;
         display:flex;flex-direction:column;position:relative;transition:var(--ease)}
.m-route:hover{box-shadow:var(--shadow-lg);transform:translateY(-4px)}
.m-route .rb{padding:18px 20px 22px;display:flex;flex-direction:column;flex:1}
.m-route h3{font-size:1.05rem;margin-bottom:6px}
.m-route p{font-size:.88rem;color:var(--muted);margin:0 0 14px;line-height:1.6}
.m-route .guide-link{margin-top:auto}
.m-route:hover .guide-link{text-decoration:underline}
/* same 2px orange ring as before, drawn as 1px border + 1px shadow ring so the lead card keeps
   the same inner width as its neighbours - with a 2px border its picture box came out 2px
   narrower and 1.5px shorter than the rest of the row (card group equal height, 2026-09-23) */
.m-route.is-lead{border:1px solid var(--accent);box-shadow:0 0 0 1px var(--accent),0 10px 30px rgba(240,122,26,.16)}
.m-route .flag{position:absolute;top:12px;left:12px;z-index:2;background:var(--accent);color:#fff;
               font-size:.7rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
               padding:5px 11px;border-radius:999px}

/* the three kinds of tooling we are asked for most */
.m-edges{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin-top:6px}
.m-edge{background:#fff;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;
        display:flex;flex-direction:column;transition:var(--ease)}
.m-edge:hover{box-shadow:var(--shadow-lg);transform:translateY(-4px);border-color:transparent}
.m-edge .m-shot{border:0;border-radius:0}
.m-edge .eb{padding:20px 22px 24px;display:flex;flex-direction:column;flex:1}
.m-edge h3{font-size:1.1rem;margin-bottom:8px}
.m-edge p{font-size:.93rem;color:var(--muted);margin:0 0 16px;line-height:1.65}
.m-edge .guide-link{margin-top:auto}
.m-edge:hover .guide-link{text-decoration:underline}

/* the shot-count question band */
/* ★ THE WHOLE BAND IS CENTRED, NOT JUST ITS HEADLINE (boss, 2026-09-23). ★
   He pointed at this block - centred headline, two lines of copy set left, orange
   button set left - and said 「标题居中,文字和按钮也要居中。」 So the 口径 is stated
   ONCE on the band and the heading, the line of copy and the button all inherit it,
   instead of one `text-align:center` on the h2 and nothing under it (which is what
   produced the shape he objected to).
   ⚠ THIS IS NOT A LICENCE TO CENTRE RUNNING PROSE. 「居中的是那一栏,不是每一行字」
   still holds for article copy, and gate R1 still fails it. What makes this band
   different is its shape: one heading + at most one line + a button, read as a sign,
   not as a paragraph - which is why `m-ask` joins the SIGNBOARD list in
   _build\tests\test_layout_left_alignment.py rather than R1 being loosened. The 133
   places on the site where a centred heading stands over real paragraphs were listed
   and handed to the boss, NOT changed - docs\reviews\2026-09-23-画面对齐口径改版-盘点.md §3B.
   Only alignment changed here. Font size, button style, colour, spacing and wording
   are untouched, and the gate's R14 checks alignment only. */
.m-ask{background:var(--accent-soft);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.m-ask .inner{max-width:760px;margin-left:auto;margin-right:auto;padding:34px 0;text-align:center}
.m-ask .btn{margin-top:18px}
/* Looks redundant next to the band's own text-align, and is not: these are TWO
   rulings. The h2 is centred because ruling V4 says 「h2 居中」 for every heading in the
   body flow (gate R7 requires it); the BAND is centred because of 2026-09-23. Written
   separately so that knocking one out cannot silently take the other with it - the
   self-test's N21e deletes the band's line and requires the copy and the button to red
   while the headline stays centred, which is the exact shape the boss objected to. */
.m-ask h2{font-size:clamp(1.25rem,2.2vw,1.6rem);margin:0 0 6px;text-align:center}
.m-ask p{margin:0;color:var(--ink-2);font-size:.98rem}

/* three answers make the quote exact */
.m-three{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;max-width:1240px}
.m-q{background:var(--bg-soft);border:1px solid var(--line);border-radius:var(--radius);padding:24px}
.m-q .no{width:34px;height:34px;border-radius:50%;background:var(--ink);color:#fff;font-weight:800;
         display:grid;place-items:center;font-size:.9rem;margin-bottom:14px}
.m-q h4{font-size:1.02rem;margin-bottom:8px}
.m-q p{font-size:.91rem;color:var(--muted);margin:0;line-height:1.65}

/* the work book: parts that came off our tools */
.m-works{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.m-work{background:#fff;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;
        transition:var(--ease)}
.m-work:hover{box-shadow:var(--shadow-lg);transform:translateY(-4px)}
.m-work .m-shot{border:0;border-radius:0}
.m-work .wb{padding:16px 18px 18px}
.m-work .wt{font-weight:700;font-size:.98rem;color:var(--ink);margin-bottom:6px}
.m-work .wm{font-size:.84rem;color:var(--muted)}

/* tool detail (2 up) and finished part (3 up) grids */
/* no column count here since 2026-09-23: 2+ cells are cut into .m-row rows (equal-height block
   at the end of this section); .one below sets its own single 760 column */
.m-details{display:grid;gap:26px;max-width:1240px}
/* Only one clip in the section: a two-up grid would park it on the left with ~700px of
   white beside it, out of line with the centred heading and the 760 body column.
   Single 760 column, centred - the picture then starts on the same left edge as the prose. */
.m-details.one{grid-template-columns:minmax(0,760px);justify-content:center;max-width:760px;margin-inline:auto}
/* The figure fills the 760 column, so `auto` and `0` put it in the same place; what this
   line is really for is cancelling the 40px inset every <figure> carries by default, which
   would stagger it against the prose by 40px - exactly the kind of "nearly lines up" the
   alignment rule exists to stop. Written `auto` since 2026-09-23 so the whole block states
   one 口径 (跟着标题走) instead of two keywords a reader has to reconcile. */
.m-details.one > figure{margin-inline:auto}
/* A portrait clip stretched to the full 760 column stands 905px tall - taller than one
   screen, so the caption and the player controls fall off the bottom, so it narrows to the
   site's one portrait width, 340.
   ★ WHERE IT SITS SIDEWAYS - CHANGED 2026-09-23. ★ This is the exact picture the boss was
   looking at when he said 「得确别扭。标题居中,图居中。」 - the section heading above it is
   a centred h2 (MouldSecHead) and the clip was hugging the column's left edge, ~210px off
   the heading's centre line. It now follows the heading, like every other picture on the
   site (see the .media-block and .vf-portrait rules above for the one 口径).
   The width did not change and must not: 340 is still 340. */
.m-details.one .m-shot.tall{max-width:340px;margin-inline:auto}
/* ⚠ PRE-EXISTING RED, FOUND 2026-09-23 AND FIXED HERE - not part of the boss's ruling.
   Narrowing the clip to 340 (commit ffd88647) left its <figcaption> at the figure's
   full 760, so gate R4 「caption no wider than its own media frame」 has been failing on
   these six pages since that commit: 「760px caption under a 340px picture」 at 1280 and
   342-under-340 at 390. R4 is an approved 口径 from August; this restores it rather than
   changing anything the boss asked about. The caption follows the frame it belongs to -
   same width, same position - so the pair still reads as one unit.
   Nothing else about the caption changes: font, colour, spacing and the left-set type
   inside it are untouched (「居中的是那一栏,不是每一行字」). */
.m-details.one .m-shot.tall + .m-cap{max-width:340px;margin-inline:auto}
.m-parts{display:grid;gap:24px}   /* columns: see the equal-height block below */

/* nearby tools */
.m-sibs{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.m-sib{background:#fff;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;
       transition:var(--ease);display:block}
.m-sib:hover{box-shadow:var(--shadow-lg);transform:translateY(-4px)}
.m-sib .m-shot{border:0;border-radius:0}
.m-sib .sb{padding:16px 18px 18px}
.m-sib .st{font-weight:700;font-size:1rem;color:var(--ink)}
.m-sib .sm{font-size:.85rem;color:var(--muted);margin-top:4px}

/* send us your drawing */
.m-rfq{background:var(--bg-soft);border-top:1px solid var(--line)}
.m-rfqbox{max-width:760px;margin-left:auto;margin-right:auto;background:#fff;border:1px solid var(--line);border-radius:var(--radius);
          padding:34px;box-shadow:var(--shadow)}
.m-rfqbox h2{font-size:clamp(1.35rem,2.4vw,1.9rem);margin-bottom:10px;text-align:center}
.m-rfqbox>p{color:var(--ink-2);font-size:1rem}
.m-rfqlist{list-style:none;padding:0;margin:20px 0 26px;display:grid;gap:11px}
.m-rfqlist li{display:flex;gap:11px;align-items:flex-start;font-size:.95rem;color:var(--ink-2)}
.m-tick{flex-shrink:0;width:20px;height:20px;border-radius:50%;background:var(--accent-soft);
        color:var(--accent-dark);display:grid;place-items:center;font-size:.72rem;font-weight:900;
        margin-top:2px}
.m-rfqnote{font-size:.86rem;color:var(--muted);margin-top:16px}

@media(max-width:1080px){
  .m-routes{grid-template-columns:repeat(3,1fr);gap:22px}
}
@media(max-width:900px){
  .m-routes{grid-template-columns:1fr 1fr}
  .m-edges,.m-three,.m-works,.m-sibs{grid-template-columns:1fr 1fr}
}
@media(max-width:640px){
  /* PHONE BANNER (QA 复走查 2026-09-23 第 9 项). The clip is 1920x760. Behind a 390x663
     text block on object-fit:cover a phone saw 23% of the frame - the mould was off both
     sides. On a phone the clip therefore stops being a background and becomes a band across
     the top, ~1.2:1, with the heading, the paragraph and the two buttons underneath it on
     the band's own dark colour. Nothing is dropped from the copy and the desktop layout is
     untouched; 1.2 is the ratio asked for, written as 100vw/1.2 so it holds at 360 and 430
     as well. */
  .m-vbanner{min-height:0;display:block;padding-top:calc(100vw / 1.2)}
  .m-vbanner>video{height:calc(100vw / 1.2)}
  .m-vbanner::after{inset:0 0 auto 0;height:calc(100vw / 1.2);
    background:linear-gradient(180deg,rgba(8,9,12,.30) 0%,rgba(8,9,12,.15) 45%,rgba(8,9,12,.55) 100%)}
  .m-vbanner .container{padding-top:26px;padding-bottom:32px}
  .m-leadbar .container{grid-template-columns:1fr;gap:20px}
  .m-edges,.m-three,.m-works,.m-sibs{grid-template-columns:1fr}
  .m-routes{grid-template-columns:1fr;gap:16px}
  .m-ask .inner{padding:30px 0}
  .m-ask .btn,.m-rfqbox .btn{width:100%;justify-content:center}
  .m-rfqbox{padding:24px 20px}
}

/* ------------------------------------------------------------------ equal-height rows, 2026-09-23
   老板选方案①「一样高、宽窄不同」. Design: docs/design/2026-09-23-模具页多格画面等高-方案.html
   (开发说明 5); review N1: docs/reviews/2026-09-23-反方评审-等高设计方案.md.
   Only .m-details with 2+ cells and .m-parts. The build (MouldRowsHtml) cuts them into .m-row
   rows and writes --r (width/height, measured from the real file) on every figure and
   --sum/--n on every row. Each figure grows by r, so every picture in a row is the same
   height and keeps its own shape: contain, nothing cropped, no bars. Rows stack; each row
   works out its own height, capped at 604 (= 340 x 16/9, one screen) and then centred;
   a row holding a portrait wraps before that portrait falls under 200px wide.
   Sits AFTER every older .m-details/.m-parts rule and states grid-template-columns:1fr with
   the higher-weight selectors, so neither the old column counts nor the old gaps can win.
   .m-details.one (single cell) and the card grids are untouched.
   604 / 456 / 900 are team-chosen numbers (方案已标明). */
.m-details:not(.one),.m-parts.m-parts{display:grid;grid-template-columns:1fr;gap:26px}
.m-parts.m-parts{gap:24px}
/* width:100% is not in the design's CSS and is needed: a grid item with margin-inline:auto
   does not stretch, it shrinks to its content, and a row of flex-basis-0 cells would
   collapse. It fills the track; the 604 cap is now on each figure (max-width below) and
   justify-content:center centres a line that stops short of the track. */
.m-row{--gap:26px;--cap:604px;display:flex;flex-wrap:wrap;justify-content:center;gap:var(--gap);
  align-items:flex-start;margin-inline:auto;width:100%}
.m-parts > .m-row{--gap:24px}
/* grow must be r x 100, not r: when a row's r add up to less than 1 (two narrow portrait
   clips) the browser hands out only part of the free space and the row shrinks into the
   middle. margin:0 clears the 40px every <figure> carries by default. */
/* basis = --b x r (only rows holding a portrait carry --b, = 200px / narrowest r): the row
   wraps exactly where the narrowest portrait would fall under 200px wide (team-chosen, QA
   2026-09-23), at any screen width; basis and grow both scale with r, so every wrapped line
   is still one height. max-width = 604 x r caps each line at 604 tall (340 wide for a 9:16
   clip, the site's portrait width) and justify-content centres a line that stops short. */
.m-row > figure{flex:calc(var(--r) * 100) 1 calc(var(--b, 0px) * var(--r));max-width:calc(var(--cap) * var(--r));
  min-width:0;margin:0}
.m-row .m-shot,.m-row .m-shot.tall,.m-row .m-shot.wide,.m-row .m-shot.fit{
  aspect-ratio:var(--r);background:transparent;border:0}
/* the hairline is drawn over the picture instead of around it, so it takes no size */
.m-row .m-shot::after{content:"";position:absolute;inset:0;border-radius:inherit;
  box-shadow:inset 0 0 0 1px var(--line);pointer-events:none}
.m-row .m-shot img,.m-row .m-shot video{object-fit:contain}
/* card grids: equal-width cards, one 4:3 box for every card (--frame, written by the build -
   MouldCardFrameOf), and the picture COVERS it, aimed at its subject by an inline
   object-position from data/image-focus.json. No white strips beside a portrait or above a
   landscape picture (老板 2026-09-27「图框统一、图一律铺满、对准主体」; supersedes 09-23 选乙
   「接受白边」). Gate: _build/tests/test_card_images_fill.py. */
:is(.m-sibs,.m-works,.m-edges,.m-routes) .m-shot,
:is(.m-sibs,.m-works,.m-edges,.m-routes) .m-shot:is(.tall,.wide,.fit){
  aspect-ratio:var(--frame,16/9);background:#fff}
:is(.m-sibs,.m-works,.m-edges,.m-routes) .m-shot :is(img,video){object-fit:cover;background:#fff}
/* 900 and below: the three-up finished-parts rows become one cell per line */
@media(max-width:900px){
  .m-parts > .m-row{flex-direction:column;align-items:center;max-width:none}
  .m-parts > .m-row > figure{flex:none;width:min(100%,calc(456px * var(--r)))}
}
/* phone: one cell per line, each in its own shape, at most 456 tall */
@media(max-width:640px){
  .m-details:not(.one) > .m-row{flex-direction:column;align-items:center;max-width:none}
  .m-details:not(.one) > .m-row > figure{flex:none;width:min(100%,calc(456px * var(--r)))}
}

/* ------------------------------------------------------------------ mould section, 2026-09-22
   Two additions from the four-way read-through of the mould pages:
   (1) .m-promise - the three things this section actually promises, one row of three, directly
       under the first screen. They used to be scattered through paragraph two, paragraph three
       and the FAQ, i.e. invisible to anyone who did not read the whole page.
   (2) .m-rfqacts - the submit button and the WhatsApp button side by side at the bottom of the
       real enquiry form. Full width and stacked on a phone, because on a phone these two are the
       only things on the page that matter. */
.m-promise{background:var(--bg-soft);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.m-pgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;padding:22px 24px}
.m-pcell{display:flex;flex-direction:column;gap:4px;padding-left:26px;position:relative}
.m-pcell::before{content:"\2713";position:absolute;left:0;top:1px;font-weight:900;color:var(--accent-dark)}
.m-pk{font-weight:800;color:var(--ink);font-size:.98rem;line-height:1.3}
.m-pv{font-size:.88rem;color:var(--ink-2);line-height:1.5}

.m-rfqform{margin-top:6px;text-align:left}
.m-rfqacts{display:flex;gap:12px;flex-wrap:wrap;margin-top:18px}
.m-rfqacts .btn{flex:1 1 220px;justify-content:center}

@media(max-width:860px){
  .m-pgrid{grid-template-columns:1fr;gap:14px;padding:18px 20px}
}
@media(max-width:720px){
  /* the two actions are the whole point of the page on a phone: one per row, full width.
     Same height for both, too: the WhatsApp button carries an icon and came out 64px while
     the submit button was 57px, so the pair read as two unrelated controls (QA 复走查
     2026-09-23 第 6 项). One number, on the row, not on either button. */
  .m-rfqacts{flex-direction:column}
  .m-rfqacts .btn{width:100%;flex:1 1 auto;min-height:64px}
}

/* The enquiry form already has a WhatsApp button inside it, and the floating bubble sits on
   top of the right-hand end of the submit button while that form is on screen (QA 复走查
   2026-09-23 第 7 项). main.js adds/removes the class as the form enters and leaves the
   viewport; with JavaScript off nothing changes and the bubble simply stays, as before. */
body.m-rfq-onscreen .wa-float{opacity:0;transform:scale(.85);pointer-events:none}

/* ── 只有一两张卡的产品卡片网格:整组居中 (老板 2026-09-23「标题居中,图居中」) ──
   .prod-grid 用的是 repeat(auto-fill, minmax(250px,1fr)):卡片多的时候铺满一行没问题,
   只有一两张时 auto-fill 会留出两三条空轨道,卡片挤在左边、右边一大片白,
   和上面居中的标题对不上 —— 老板在 /pet-preform-mould/「The preform moulds we build」
   那一节指出来的。全站同形的还有 10 页(案例页、模头单品页、桌椅页)。
   只对不满三张的网格生效:换成 auto-fit(空轨道塌掉)+ 整组居中;卡片宽度封顶 300,
   跟平时一张卡的宽度一样,不会被撑成两个大方块。卡片满一行的类目页一个像素不变。 */
/* 只套在 .sec-head(居中标题)后面的网格上 —— 列表页主栏里那种上面是靠左计数行的网格
   (im-tables-chairs、mould-injection-moulds)标题靠左,照旧靠左,不许被居中(代码评审+leadcheck 查出)。
   卡片宽度按 1fr 伸缩、只在电脑端封顶,手机上和别的页一样整宽。 */
.sec-head + .prod-grid:not(:has(> :nth-child(3))){
  grid-template-columns:repeat(auto-fit,minmax(250px,1fr));
  justify-content:center
}
@media (min-width:768px){
  .sec-head + .prod-grid:not(:has(> :nth-child(3))){grid-template-columns:repeat(auto-fit,minmax(250px,300px))}
}

/* 模具页一段展示区块末尾的询盘栏(2026-09-23,build.ps1 MouldCtaBar,门禁 test_mould_section_cta.py)。
   一行:一句话 + 一颗主按钮(跳本页询价表单)+ 一个小 WhatsApp 文字链接。不做色块,只一条细线隔开画面;
   区块标题居中,栏跟着居中。一行放不下时句子自己占一行,按钮和链接在下一行,仍居中。
   按钮不折行(.btn 本来就是 nowrap,"Send your drawing" 在 390 下一行放得下);
   小链接可点区域至少 44px 高;手机上三样竖排居中,离右下角浮动 WhatsApp 按钮够远。 */
.m-cta{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:8px 18px;
  max-width:1240px;margin:26px auto 0;padding-top:20px;border-top:1px solid var(--line);text-align:center}
.m-cta-t{flex:0 1 auto;max-width:760px;color:var(--ink-2);font-size:.98rem;line-height:1.5}
.m-cta-wa{display:inline-flex;align-items:center;justify-content:center;gap:5px;min-height:44px;padding:0 6px;
  font-size:.92rem;font-weight:600;color:#1a9e4f;text-decoration:none}
.m-cta-wa:hover{text-decoration:underline}
@media (max-width:720px){
  .m-cta{flex-direction:column;gap:10px}
  .m-cta-t{flex:none;max-width:none}
}

/* FAQ answer links: match body-copy link style so buyers can see they are
   clickable. .faq-a wraps its answer text in either a bare <div> or a <p>
   depending on which of the several FAQ-emitting call sites in build.ps1 built
   the page, and the existing .faq-a p a / .faq-a li a rules in style.css only
   catch the <p>/<li> case - links sitting directly in the bare <div> stayed
   unstyled (grey, no underline). This rule covers every .faq-a link regardless
   of wrapper. Colours/weights copied verbatim from the .prose body-link rule
   in style.css so FAQ links look identical to every other in-copy link. */
.faq-a a:not(.btn){
  color: var(--accent-dark);
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
  transition: color var(--ease);
}
.faq-a a:not(.btn):hover{
  color: var(--accent);
  text-decoration-thickness: 2px;
}

/* card groups: a short last row sits in the middle, under the centred heading */
:is(.m-sibs,.m-works,.m-edges,.m-routes){display:flex;flex-wrap:wrap;justify-content:center;
  align-items:stretch;gap:var(--g,24px);--cols:3}
:is(.m-sibs,.m-works,.m-edges,.m-routes)>*{min-width:0;
  flex:0 0 calc((100% - (var(--cols) - 1) * var(--g,24px)) / var(--cols) - var(--mx,0px))}
/* .m-work is a <figure>: keep its default 40px side margins, so take them out of the card width */
.m-works{--mx:80px}
/* height:100% would stop flex from stretching; auto lets every card in a row match the tallest */
.m-routes .m-route{height:auto}
.m-routes{--g:20px}
@media(max-width:1080px){.m-routes{--g:22px}}
@media(max-width:900px){:is(.m-sibs,.m-works,.m-edges,.m-routes){--cols:2}}
@media(max-width:640px){:is(.m-sibs,.m-works,.m-edges,.m-routes){--cols:1}.m-routes{--g:16px}}

/* buyer-facing links inside the category-hero first paragraph and the p.muted intro
   paragraphs must look clickable like the rest of the body copy - same colour + underline
   as the pinned body-link rule at the end of style.css (boss ruling 2026-08-04). .btn is
   excluded so button-styled anchors are untouched. */
main .hook-sub a:not(.btn),main p.muted a:not(.btn){
  color: var(--accent-dark);
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
}
main .hook-sub a:not(.btn):hover,main p.muted a:not(.btn):hover{
  color: var(--accent);
  text-decoration-thickness: 2px;
}

/* ---- "Which machine for your container" pick table (2026-09-26, 引流新增内容 英文定稿) ----
   Used on the EBM process guide and the extrusion blow molding catalogue page only (class
   .pick-table / .pick-block, nothing else reads these). Desktop: same look as .param-table,
   inside the 760 reading column, centred. Phone (<=600px): no sideways scroll - every row
   folds into its own small card and each cell shows its column name from data-label. */
.pick-block{max-width:760px;margin:34px auto 0}
.pick-block > h3{margin:0 0 8px}
.pick-block > p{margin:0 0 14px}
.pick-block > p:last-child{margin:14px 0 0}
.pick-table{width:100%;border-collapse:collapse;font-size:.94rem;text-align:left}
.pick-table thead th{background:var(--ink);color:#fff;text-align:left;padding:11px 14px;font-weight:700;vertical-align:bottom}
.pick-table td{padding:10px 14px;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:break-word}
.pick-table tbody tr:nth-child(even){background:var(--bg-soft)}
.pick-table td a:not(.btn){color:var(--accent-dark);text-decoration:underline;text-decoration-thickness:1.5px;text-underline-offset:3px}
.pick-table td a:not(.btn):hover{color:var(--accent);text-decoration-thickness:2px}
@media (max-width:600px){
  .pick-table thead{display:none}
  .pick-table,.pick-table tbody,.pick-table tr,.pick-table td{display:block;width:100%}
  .pick-table tr{border:1px solid var(--line);border-radius:var(--radius-sm);margin-bottom:12px;padding:6px 0;background:#fff}
  .pick-table tbody tr:nth-child(even){background:#fff}
  .pick-table td{border-bottom:0;padding:6px 14px}
  .pick-table td::before{content:attr(data-label);display:block;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:2px}
}
