/* ===== Scroll Reveal ===== */
/* Matched against the Figma-site prototype's own compiled CSS
   (fall-straw-33948831.figma.site, 2026-09-05) rather than guessed —
   .fade-section is a whole-block fade+rise; .fade-children staggers
   each direct child in 0.1s steps (only wired up to 6 children below,
   matching what the reference itself covers). script.js toggles
   .visible via IntersectionObserver when a block scrolls into view. */
/* --fade-offset：讓某個 section 把自己的淡入整批往後延，用來排「先播
   別的動畫、再淡入內容」的順序（目前用在 .stats-section，等
   .section-slash 的斜線轉完）。預設 0s，等於沒有這個機制時的行為。 */
.fade-section {
  opacity: 0;
  transform: translateY(32px);
  transition: opacity var(--sys-motion-duration) var(--sys-motion-easing),
    transform var(--sys-motion-duration) var(--sys-motion-easing);
  transition-delay: var(--fade-offset, 0s);
}

.fade-section.visible {
  opacity: 1;
  transform: translateY(0);
}

.fade-children > * {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--sys-motion-duration) var(--sys-motion-easing),
    transform var(--sys-motion-duration) var(--sys-motion-easing);
}

.fade-children.visible > :nth-child(1) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--fade-offset, 0s) + 0s);
}
.fade-children.visible > :nth-child(2) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--fade-offset, 0s) + 0.1s);
}
.fade-children.visible > :nth-child(3) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--fade-offset, 0s) + 0.2s);
}
.fade-children.visible > :nth-child(4) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--fade-offset, 0s) + 0.3s);
}
.fade-children.visible > :nth-child(5) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--fade-offset, 0s) + 0.4s);
}
.fade-children.visible > :nth-child(6) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--fade-offset, 0s) + 0.5s);
}

/* .fade-logos — logo 牆專用，錯開的單位是每一張 logo，不是每一排。
   .fade-children 以直接子元素為單位，logo 牆的直接子元素只有 3 個
   __row，整排會一起出現。
   排間用固定 --row-delay 而不是累加前面排的張數：CMS 之後增減 logo
   時不會整組錯位，最多只是相鄰兩排的節奏稍微重疊。
   步進 0.06s：目前 16 張跑完約 0.9s，用 .fade-children 的 0.1s 要
   拖到 1.5s。 */
.fade-logos img {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--sys-motion-duration) var(--sys-motion-easing),
    transform var(--sys-motion-duration) var(--sys-motion-easing);
}

.fade-logos.visible img {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--row-delay, 0s) + var(--i, 0) * 0.06s);
}

.fade-logos > :nth-child(2) {
  --row-delay: 0.3s;
}
.fade-logos > :nth-child(3) {
  --row-delay: 0.6s;
}
.fade-logos > :nth-child(4) {
  --row-delay: 0.9s;
}

.fade-logos img:nth-child(2) {
  --i: 1;
}
.fade-logos img:nth-child(3) {
  --i: 2;
}
.fade-logos img:nth-child(4) {
  --i: 3;
}
.fade-logos img:nth-child(5) {
  --i: 4;
}
.fade-logos img:nth-child(6) {
  --i: 5;
}
.fade-logos img:nth-child(7) {
  --i: 6;
}
.fade-logos img:nth-child(8) {
  --i: 7;
}

/* ===== Shared Tab Indicator ===== */
/* Sliding bottom-bar shared by every tab bar that toggles .is-active
   between siblings (our-clients-tabs, ubiquiti-tabs ×2, news-tabs,
   dealership-tabs). Replaces each item's own border-bottom-width jump
   (1px -> 4-6px on .is-active), which shifted the button's text up on
   activation — .xxx-tabs__item now keeps a constant 1px border-bottom
   track, and this positioned span slides/resizes over it via
   JS-computed left+width (script.js, createTabIndicator). */
/* tab, state=hover（node 4033:8969）：底線從 neutral/200 換成
   primary/500（1px，寬度不變），文字維持 neutral/600 跟 default 同色
   ——hover 只動線。四個 tab 列共用這一條；每個 .xxx-tabs__item 自己
   已經宣告了 border-color 的 transition（layout.css）。作用中的那個
   由下面 .tabs-indicator 的 4px 紅線蓋著，hover 與否看不出差別，
   不用另外排除。 */
.our-clients-tabs__item:hover,
.ubiquiti-tabs__item:hover,
.news-tabs__item:hover,
.dealership-tabs__item:hover {
  border-bottom-color: var(--color-primary-500);
}

.tabs-indicator {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 4px;
  width: 0;
  background: var(--color-primary-500);
  transform: translateX(0);
  transition: transform var(--sys-motion-duration) var(--sys-motion-easing),
    width var(--sys-motion-duration) var(--sys-motion-easing);
  pointer-events: none;
}

/* ===== Service Section ===== */
.service-card {
  position: relative;
  background: var(--color-neutral-900);
  display: flex;
  gap: 16px;
  padding: 20px 20px 24px;
  width: 100%;
  max-width: 448px;
}

/* The "red border" is a 5s auto-advance progress bar, not a static
   line (Tina, 2026-09-05) — script.js restarts the animation (remove +
   reflow + re-add .is-animating) every time the slide changes, whether
   from the timer or a manual pagination/arrow click. Animates
   transform:scaleX (GPU-composited) instead of width — width forces a
   layout recalc on every frame for the full 5s, repeating every cycle
   for as long as the page stays open (Tina flagged the page feeling
   slow; this was the one animation actually running continuously). */
.service-card__progress {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 3px;
  background: var(--color-primary-500);
  transform: scaleX(0);
  transform-origin: left;
}

.service-card__progress.is-animating {
  animation: service-card-progress 5s linear forwards;
}

@keyframes service-card-progress {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

.service-card__num {
  background: var(--color-primary-500);
  color: var(--color-white);
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: var(--fs-20);
  flex-shrink: 0;
}

.service-card__body {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.service-card__title {
  color: var(--color-white);
  font-weight: 700;
  font-size: var(--type-card-title-lg);
}

.service-card__desc {
  color: var(--color-neutral-050);
  font-size: var(--type-card-desc-sm);
}

.pagination-dot {
  width: 40px;
  height: 40px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-white);
  color: var(--color-neutral-800);
  border: none;
  cursor: pointer;
  font-family: var(--font-zh);
  transition: background-color var(--sys-motion-duration) var(--sys-motion-easing);
}

.pagination-dot.is-active {
  background: var(--color-neutral-800);
  color: var(--color-white);
}

/* pagination-item, state=hover (node 7060:10790) — corrected from an
   earlier guess (icon-button/ghost's #ffebeb): this is the actual
   pagination-dot component and its hover is neutral/050 #f2f2f2. */
.pagination-dot:hover:not(.is-active) {
  background: var(--color-neutral-050);
}

.pagination-arrow {
  width: 40px;
  height: 40px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: var(--fs-20);
  transition: background-color var(--sys-motion-duration) var(--sys-motion-easing),
    color var(--sys-motion-duration) var(--sys-motion-easing);
}

/* icon-button/outline, color=dark, state=hover (node 4018:13093) */
.pagination-arrow:hover {
  background: var(--color-neutral-800);
  color: var(--color-white);
}

/* ===== Security Section ===== */
/* Mobile-first default: 2x2 grid (Figma <768 and >=768 both use this;
   only >=1200 desktop reverts to a single 4-across row — see responsive.css) */
.security-cards {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  width: 100%;
}

/* "Sweep to Right" (Tina's spec, 2026-09-04 — same fixed-direction
   effect as .btn, not the direction-aware version): ::before grows in
   from the left edge on hover, retreats back the same way on
   mouse-leave. position/isolation/overflow set up the same
   ::before-behind-content stacking trick used on .btn. */
.security-card {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  flex: 1;
  background: var(--color-white);
  border: 1px solid var(--color-neutral-200);
  padding: 60px 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  text-align: center;
  transition: color var(--sys-motion-duration) var(--sys-motion-easing);
}

.security-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--color-primary-600);
  transform: scaleX(0);
  transform-origin: 0% 50%;
  transition: transform var(--sys-motion-duration) var(--sys-motion-easing);
}

.security-card img {
  width: 32px;
  height: 32px;
  transition: filter var(--sys-motion-duration) var(--sys-motion-easing);
}

.security-card__title {
  font-weight: 700;
  font-size: var(--type-card-title-def);
}

.security-card__desc {
  color: var(--color-neutral-600);
  transition: color var(--sys-motion-duration) var(--sys-motion-easing);
  /* preserves the first card's manual line break ("遵循法規‧強化資安 /
     保護資料‧企業永續") — harmless for the other cards' single-line text */
  white-space: pre-line;
}

/* card-security, state=hover (node 7043:6169): bg -> primary/600, text ->
   white, via the directional sweep. Icon swap to the white illus asset
   isn't feasible via CSS src-swap, so the icon is forced white with a
   filter instead (visual match, not asset-accurate). Uniform across all
   4 cards — the earlier "first card stays permanently highlighted"
   .is-highlight modifier was removed per Tina (2026-09-04): all cards
   are now plain .security-card with the same default/hover behavior. */
.security-card:hover {
  color: var(--color-white);
}

.security-card:hover::before {
  transform: scaleX(1);
}

.security-card:hover .security-card__desc {
  color: var(--color-white);
}

.security-card:hover img {
  filter: brightness(0) invert(1);
}

/* ===== Brand Section ===== */
.brand-logos {
  display: flex;
  flex-direction: column;
  gap: var(--grid-gutter-sm);
  align-items: center;
}

.brand-logos__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--grid-gutter-sm);
  justify-content: center;
}

/* Mobile-first default (<768px); tablet/desktop sizes in responsive.css */
.brand-logos__row img {
  width: 80px;
  height: 80px;
  border-radius: 999px;
  object-fit: cover;
  background: var(--color-white);
}

/* ===== Stats (numSec) ===== */
.stat-item {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.stat-item__label {
  color: var(--color-neutral-200);
  font-weight: 700;
}

.stat-item__number {
  display: flex;
  align-items: flex-end;
  gap: 12px;
}

/* "huge num 2" style — deliberately tight line-height:1, not the
   1.25/1.5 convention other text styles use (that's this style's own
   value in Figma, not a mistake to normalize away). <768 uses 56px;
   >=1200 not re-verified this pass, kept at the pre-existing 80px. */
.stat-item__number strong {
  font-family: var(--font-en);
  font-weight: 700;
  font-size: var(--fs-56);
  color: var(--color-primary-500);
  line-height: 1;
}

.stat-item__number span {
  color: var(--color-white);
  font-weight: 700;
  padding-bottom: 16px;
}

.stat-item__title {
  color: var(--color-white);
  font-weight: 700;
  font-size: var(--type-card-title-def);
}

.stat-item__sub {
  color: var(--color-neutral-200);
}

/* ===== Clients Section ===== */
/* Mobile-first default (<768px) height; desktop overrides in responsive.css */
/* "Sweep to Right" (Tina's spec, 2026-09-04 — same fixed-direction effect
   as .btn/.security-card): the red tint is a ::before layer, fully
   covering by default (scaleX:1). To keep the reveal reading as a
   left-to-right sweep (the uncovered edge travels rightward, same
   direction as the fill-in effect on .btn/.security-card), it's anchored
   at the right edge and shrinks away on hover — its left edge retreats
   rightward, revealing the photo underneath progressively from left to
   right. isolation:isolate keeps the ::before/__bg's negative z-indexes
   scoped to this card. */
.client-card {
  position: relative;
  isolation: isolate;
  height: 180px;
  border: 1px solid var(--color-primary-050);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  overflow: hidden;
  transition: border-color var(--sys-motion-duration) var(--sys-motion-easing);
}

.client-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--color-primary-600);
  transform: scaleX(1);
  transform-origin: 100% 50%;
  transition: transform var(--sys-motion-duration) var(--sys-motion-easing);
}

.client-card:hover {
  border-color: var(--color-white);
}

.client-card:hover::before {
  transform: scaleX(0);
}

.client-card__bg {
  position: absolute;
  inset: 0;
  object-fit: cover;
  z-index: -2;
}

.client-card img.icon {
  width: 24px;
  height: 24px;
}

.client-card__title {
  font-weight: 500;
  font-size: var(--type-card-title-def);
}

/* ===== News Section ===== */
/* Mobile-first default: fixed width while the list can scroll.
   Tablet+ overrides to flex:1 in responsive.css. */
.news-card {
  display: flex;
  flex-direction: column;
}

.news-card__img {
  aspect-ratio: 16/9;
  overflow: hidden;
}

.news-card__img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--sys-motion-duration) var(--sys-motion-easing);
}

/* card-news, state=hover (node 392:1152): image zooms in within its
   clipped frame (Figma expresses this as an oversized, offset image;
   with object-fit:cover already in play, a uniform scale is the CSS
   equivalent). Text/label are unchanged on hover per the design. */
.news-card:hover .news-card__img img {
  transform: scale(1.15);
}

.news-card__meta {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-top: 12px;
}

/* label/sm/m */
.news-card__tag {
  background: var(--color-primary-500);
  color: var(--color-white);
  font-size: var(--type-label-sm);
  line-height: 1.25;
  padding: 4px 8px;
}

/* label/sm/r */
.news-card__date {
  flex: 1;
  text-align: right;
  color: var(--color-neutral-600);
  font-size: var(--type-label-sm);
  line-height: 1.25;
}

/* label/md/m */
.news-card__title {
  font-weight: 500;
  font-size: var(--type-card-title-def);
  line-height: 1.5;
  color: var(--color-neutral-800);
  margin-top: 8px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ===== CTA Section ===== */
.cta-cards {
  display: flex;
  gap: 16px;
  width: 100%;
}

.cta-card {
  flex: 1;
  background: var(--color-dark-40a);
  backdrop-filter: blur(12px);
  padding: 20px;
  display: flex;
  align-items: flex-end;
  gap: 16px;
  transition: background-color var(--sys-motion-duration) var(--sys-motion-easing);
}

/* cta, size=lg, state=hover (node 7051:7046): overlay darkens 40a -> 60a */
.cta-card:hover {
  background: var(--color-dark-60a);
}

.cta-card__text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  flex: 1;
  min-width: 0;
}

.cta-card__icon {
  width: 24px;
  height: 24px;
  background: var(--color-primary-500);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.cta-card__icon img {
  width: 20px;
  height: 20px;
}

.cta-card__title {
  color: var(--color-white);
  font-weight: 700;
  font-size: var(--type-card-title-lg);
}

.cta-card__desc {
  color: var(--color-neutral-100);
}

/* Text/arrow "roll" reveal on .cta-card hover (Tina) — same mechanism
   as .dealership-banner__cta-roll (see that rule's own comment for why
   the duplicate-child/top:100% trick needs no hardcoded line-height):
   each wrapper holds the real line + an aria-hidden duplicate that
   slides up into view on hover. Kept as its own component-scoped class
   rather than sharing .dealership-banner__cta-roll directly, since
   that one's hover trigger is scoped to .dealership-banner__box. */
.cta-card__roll {
  position: relative;
  display: block;
  overflow: hidden;
}

.cta-card__roll > * {
  display: block;
  transition: transform var(--sys-motion-duration) var(--sys-motion-easing);
}

.cta-card__roll > *:last-child {
  position: absolute;
  left: 0;
  top: 100%;
}

.cta-card:hover .cta-card__roll > * {
  transform: translateY(-100%);
}

.cta-card__arrow-roll {
  flex-shrink: 0;
}

.cta-card__arrow {
  width: 36px;
  height: 36px;
}

/* ===== 02 About Page ===== */

/* Shared sub-section heading (經營理念 / 客戶肯定): 6px left border,
   24px inset — reused as-is since Figma gives it the exact same
   markup/values both times. Not the same component as .section-title
   (which centers with a divider bar instead of a left border) — a
   page-specific heading style, but shares its font-size step via
   --type-sec-title (Tina, 2026-09-08: was a fixed fs-28
   constant across all breakpoints; standardized to the same 24->32
   step as .section-title instead of staying a one-off). */
.about-section-title {
  border-left: 6px solid var(--color-primary-500);
  padding-left: 24px;
  font-family: var(--font-zh);
  font-weight: 700;
  font-size: var(--type-sec-title);
  line-height: 1.5;
  color: var(--color-neutral-800);
  width: 100%;
}

/* Centered heading flanked by two short accent bars (實例影片 /
   口碑好評) — visually distinct from .section-title--center's own
   1px neutral/200 dividers (these are 5px thick and colored), so a
   dedicated component rather than reusing that class. --flank-color
   defaults to primary/500 (口碑好評); 實例影片 overrides it to
   primary/400 via .about-flanked-title--accent (matches imgLine2 vs
   imgLine3's different stroke colors in Figma). Font-size now shares
   --type-sec-title with .section-title (Tina, 2026-09-08:
   was its own fs-24→fs-28 step, standardized to the same token). */
.about-flanked-title {
  --flank-color: var(--color-primary-500);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 40px;
  font-family: var(--font-zh);
  font-weight: 700;
  font-size: var(--type-sec-title);
  line-height: 1.5;
  color: var(--color-neutral-800);
}

/* Used on 實例影片's dark bg — needs white text, not just the
   red-vs-lighter-red flank-color swap. */
.about-flanked-title--accent {
  --flank-color: var(--color-primary-400);
  color: var(--color-white);
}

.about-flanked-title::before,
.about-flanked-title::after {
  content: "";
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 5px;
  height: 20px;
  background: var(--flank-color);
}

.about-flanked-title::before { left: 0; }
.about-flanked-title::after { right: 0; }

/* "Sweep to Right" hover on video/CTA links wrapping .about-video-card
   and .about-testimony__photo-less CTA — reuses the same fixed-direction
   pattern as .btn/.security-card so hover language stays consistent
   site-wide, scoped to the link wrapper rather than the card itself so
   the whole clickable area responds. */
.about-video-card-link {
  display: block;
  width: 100%;
  /* Sizing lives on the link (not .about-video-card) because
     .about-videos centers its children (align-items:center) — a
     max-width on a display:contents-wrapped child needs the width
     constraint on the actual flex item, which is this <a>. */
  max-width: 642px;
  text-decoration: none;
}

.about-video-card-link:hover .about-video-card {
  background: var(--color-dark-60a);
}

/* ===== Shared Form Fields =====
   First form on the site (06 dealership「成為經銷商」) — kept generic
   (not prefixed .dealer-form-*) since 07 聯絡我們 will reuse the exact
   same input/textarea/radio look (PRD 章節六 both forms are WPForms).
   Static appearance only this round: no validation/submit wiring
   beyond native `required` (PRD 章節六 — WPForms owns real submission). */
.form-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  /* reset for when this class is used on a <fieldset> (radio group) */
  border: none;
  padding: 0;
  margin: 0;
}

/* CAUTION: same legend/flex-gap quirk as .form-select-group above —
   `gap` above does nothing for the legend's own spacing when .form-field
   is a <fieldset> (dealership.html 是否需要協助設定？), needs an
   explicit margin instead. */
.form-field > legend {
  margin: 0 0 8px;
}

.form-field__label {
  font-family: var(--font-zh);
  font-weight: 500;
  line-height: 1.5;
  color: var(--color-neutral-800);
  padding: 0;
}

.form-required {
  color: var(--color-primary-500);
}

.form-input,
.form-textarea {
  width: 100%;
  background: var(--color-white);
  border: 1px solid var(--color-neutral-400);
  padding: 12px;
  font-family: var(--font-zh);
  font-weight: 400;
  line-height: 1.5;
  color: var(--color-neutral-800);
  transition: border-color var(--sys-motion-duration) var(--sys-motion-easing),
    box-shadow var(--sys-motion-duration) var(--sys-motion-easing),
    background-color var(--sys-motion-duration) var(--sys-motion-easing);
}

.form-input::placeholder,
.form-textarea::placeholder {
  color: var(--color-neutral-400);
}

.form-input {
  height: 40px;
}

.form-textarea {
  height: 120px;
  resize: none;
}

/* ===== Field states (Tina, 2026-09-08: "元件的樣式，包含元件狀態變化")
   No Figma export exists for any of these — WPForms owns the actual
   submit/validation behavior (見 PRD 章節六), this CSS only needs to be
   ready for whatever state WPForms puts the field in. Colors reuse
   existing tokens rather than introducing new ones: primary/500 is
   this site's only strong accent (no separate blue "focus" color in
   the palette), so it does double duty for focus and error, same as
   the existing .form-required asterisk. */
.form-input:hover,
.form-textarea:hover {
  border-color: var(--color-neutral-600);
}

.form-input:focus,
.form-textarea:focus {
  outline: none;
  border-color: var(--color-primary-500);
  box-shadow: 0 0 0 3px var(--color-primary-050);
}

.form-input:disabled,
.form-textarea:disabled {
  background: var(--color-neutral-050);
  border-color: var(--color-neutral-200);
  color: var(--color-neutral-400);
  cursor: not-allowed;
}

.form-input:disabled::placeholder,
.form-textarea:disabled::placeholder {
  color: var(--color-neutral-400);
}

/* 錯誤狀態的三個來源都套同一個樣式：
   - `:user-invalid`：靜態原型用原生驗證擋下送出時，瀏覽器自己加的狀態
   - `.has-error`：WPForms 上架後自己加的 class
   - `[aria-invalid="true"]`：WPForms 的另一種標記方式
   厚邊框（2px，不只換色）讓錯誤不依賴顏色也看得出來。 */
.form-input:user-invalid,
.form-textarea:user-invalid,
.form-input.has-error,
.form-textarea.has-error,
.form-input:invalid[aria-invalid="true"],
.form-textarea:invalid[aria-invalid="true"] {
  border-width: 2px;
  border-color: var(--color-primary-500);
}

/* 錯誤訊息文字樣式。靜態原型用原生驗證泡泡，不產生這個元素；
   保留給 WPForms 上架後把它加在自己的錯誤訊息節點上。 */
.form-error-message {
  font-family: var(--font-zh);
  font-weight: 400;
  font-size: var(--type-body-sm);
  line-height: 1.5;
  color: var(--color-primary-500);
}

.form-radio-group {
  display: flex;
  flex-wrap: wrap;
  gap: 16px 24px;
}

.form-radio-option,
.form-checkbox-option {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Custom circle matching Figma's radio/default (node 7006:20705):
   20px circle, 1px #999 stroke, no fill when unchecked. Figma gives no
   checked-state export — the filled dot on :checked below reuses the
   site's own primary color, the same interaction language as every
   other selection state on this site. */
.form-radio-option__input {
  appearance: none;
  width: 20px;
  height: 20px;
  margin: 0;
  border-radius: 50%;
  border: 1px solid var(--color-neutral-400);
  flex-shrink: 0;
  position: relative;
  cursor: pointer;
  transition: border-color var(--sys-motion-duration) var(--sys-motion-easing),
    box-shadow var(--sys-motion-duration) var(--sys-motion-easing);
}

.form-radio-option__input:hover:not(:disabled) {
  border-color: var(--color-neutral-600);
}

.form-radio-option__input:checked {
  border-color: var(--color-primary-500);
}

.form-radio-option__input:checked:hover:not(:disabled) {
  border-color: var(--color-primary-600);
}

.form-radio-option__input:checked::after {
  content: "";
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: var(--color-primary-500);
}

.form-radio-option__input:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--color-primary-050);
}

.form-radio-option__input:disabled {
  border-color: var(--color-neutral-200);
  cursor: not-allowed;
}

.form-radio-option__input:disabled:checked::after {
  background: var(--color-neutral-400);
}

.form-radio-option:has(.form-radio-option__input:disabled) .form-radio-option__label {
  color: var(--color-neutral-400);
  cursor: not-allowed;
}

.form-radio-option__label,
.form-checkbox-option__label {
  font-family: var(--font-zh);
  font-weight: 400;
  line-height: 1.25;
  color: var(--color-neutral-600);
  cursor: pointer;
}

/* Custom checkbox matching Figma's checkbox/default (node 9033:4335 /
   7006:20719): 20px square, 1px #999 stroke, no fill when unchecked —
   same "no checked-state export" situation as .form-radio-option__input
   above, so the checked look (fill + tick) reuses the same primary-color
   selection language rather than a separately-invented style. */
.form-checkbox-option__input {
  appearance: none;
  width: 20px;
  height: 20px;
  margin: 0;
  border: 1px solid var(--color-neutral-400);
  flex-shrink: 0;
  position: relative;
  cursor: pointer;
  transition: border-color var(--sys-motion-duration) var(--sys-motion-easing),
    background-color var(--sys-motion-duration) var(--sys-motion-easing),
    box-shadow var(--sys-motion-duration) var(--sys-motion-easing);
}

.form-checkbox-option__input:hover:not(:disabled) {
  border-color: var(--color-neutral-600);
}

.form-checkbox-option__input:checked {
  background: var(--color-primary-500);
  border-color: var(--color-primary-500);
}

.form-checkbox-option__input:checked:hover:not(:disabled) {
  background: var(--color-primary-600);
  border-color: var(--color-primary-600);
}

.form-checkbox-option__input:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--color-primary-050);
}

.form-checkbox-option__input:disabled {
  border-color: var(--color-neutral-200);
  cursor: not-allowed;
}

.form-checkbox-option__input:disabled:checked {
  background: var(--color-neutral-400);
  border-color: var(--color-neutral-400);
}

.form-checkbox-option:has(.form-checkbox-option__input:disabled) .form-checkbox-option__label {
  color: var(--color-neutral-400);
  cursor: not-allowed;
}

.form-checkbox-option__input:checked::after {
  content: "";
  position: absolute;
  left: 6px;
  top: 2px;
  width: 5px;
  height: 10px;
  border: solid var(--color-white);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

/* ===== Select-list field groups (07 聯絡我們) =====
   A label above a wrapped list of checkboxes/radios — 報價需求／場域
   類型／預計導入設備品牌／預計設備數量／何處得知. The label itself
   reuses .form-field__label verbatim (identical typography in Figma);
   only this wrapper's own label-to-list gap and the list's item gap
   step at ≧768 (base values below are the <768 symbol's numbers,
   responsive.css carries the ≧768 step). Written as <fieldset>/<legend>
   in the markup since each is a group of same-name checkboxes/radios,
   with .form-field__label reused on the <legend> for the shared look.

   CAUTION: a <legend> inside a display:flex fieldset is NOT a flex
   item — browsers render it in its own anonymous box outside the
   flex formatting context, so `gap`/`row-gap` on the fieldset has NO
   effect on the space around it (verified: computed row-gap showed
   12/16px but the rendered legend-to-list distance was 0px). The
   label-to-list spacing has to be a margin-bottom on the legend
   itself instead. */
.form-select-group {
  display: flex;
  flex-direction: column;
  width: 100%;
  border: none;
  padding: 0;
  margin: 0;
}

.form-select-group > legend {
  margin: 0 0 12px;
}

.form-select-list {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  width: 100%;
}

/* 勾「其他」才展開的補充輸入欄（script.js 切 .is-open）。上架後由
   WPForms 的 conditional logic 接手同樣行為，這裡先把互動做出來。
   展開動畫用 grid-template-rows 0fr→1fr，不用 max-height：max-height
   要先猜一個夠大的值，收合時前半段動畫是空跑的，速度感跟展開對不
   起來；0fr→1fr 剛好就是內容的實際高度。
   收合狀態用 visibility:hidden 不是 display:none——一樣移出 tab 順序
   與無障礙樹，但留得住過場（跟 .mobile-menu 同一個處理方式）。
   與選項清單之間的 16px 留白掛在外層的 margin 上一起過渡，不能放在
   inner 的 padding：padding 不會被 0fr 壓縮，收合後會留一條 16px 的
   空白。
   只做形狀變化、不加 opacity 過場，跟 navbar 下拉／手機選單同一套
   語彙（PRD 章節九 9.3）。 */
.form-other-field {
  display: grid;
  grid-template-rows: 0fr;
  width: 100%;
  margin-top: 0;
  visibility: hidden;
  transition: grid-template-rows var(--sys-motion-duration) var(--sys-motion-easing),
    margin-top var(--sys-motion-duration) var(--sys-motion-easing),
    visibility 0s linear var(--sys-motion-duration);
}

.form-other-field.is-open {
  grid-template-rows: 1fr;
  margin-top: 16px;
  visibility: visible;
  transition: grid-template-rows var(--sys-motion-duration) var(--sys-motion-easing),
    margin-top var(--sys-motion-duration) var(--sys-motion-easing),
    visibility 0s;
}

/* min-height:0 是 0fr 能真的收到 0 的前提：grid item 預設 min-height
   auto，會被內容撐開。 */
.form-other-field__inner {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
  overflow: hidden;
}

/* ===== File upload (環境平面圖上傳, 07 聯絡我們) =====
   Figma gives only this one dropzone visual (no drag-over/filled state)
   — PRD 章節六 marks this field optional, static appearance only, so no
   selected-filename readout is implemented this round. The dropzone
   itself is the <label> (wraps a visually-hidden file input) so the
   whole box is clickable, same accessible-label pattern as every other
   field on this form. */
.form-upload {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 100%;
  padding: 24px;
  background: var(--color-neutral-050);
  border: 1px dashed transparent;
  cursor: pointer;
  transition: border-color var(--sys-motion-duration) var(--sys-motion-easing),
    background-color var(--sys-motion-duration) var(--sys-motion-easing);
}

/* Hover/drag-over states (Tina, 2026-09-08) — no Figma export for
   either, so this borrows the same dashed-border dropzone convention
   used everywhere on the web, in the site's own neutral/primary
   tokens. .is-dragover is toggled by script.js on dragenter/dragleave/
   drop since CSS alone can't react to native drag events. */
.form-upload:hover {
  border-color: var(--color-neutral-400);
}

.form-upload.is-dragover {
  border-color: var(--color-primary-500);
  background: var(--color-primary-050);
}

/* The real input stays display:none (so the dropzone's own box is the
   clickable/droppable target); focus still needs to be visible
   somewhere when a keyboard user tabs to it, so the ring goes on the
   wrapping label via :has() instead of the invisible input itself. */
.form-upload:has(.form-upload__input:focus-visible) {
  outline: none;
  box-shadow: 0 0 0 3px var(--color-primary-050);
}

.form-upload__input {
  display: none;
}

.form-upload__icon {
  width: 28px;
  height: 28px;
}

.form-upload__hint {
  font-family: var(--font-zh);
  font-weight: 400;
  line-height: 1.5;
  color: var(--color-neutral-600);
  text-align: center;
}
