ソースを参照

feat(docs): redesign FAQ with hero, section chips, icons, and live search

The FAQ was functional but plain after the previous accordion pass --
just a list of cards under generic section headings. Rebuilds it as a
dedicated page with its own visual identity:

- Hero block with gradient background, eyebrow chip, large title, and
  a pill-shaped search input that filters questions live
- Six topic chips below the hero (Getting started, Frameworks,
  Customization, Versions, Licensing, Troubleshooting) -- click to
  smooth-scroll to that section. Each chip has a tone-specific hover
  accent (primary, info, warning, success, secondary, danger).
- Per-section header cards with a colored left border + tinted icon
  square. Icons mirror the chips, and the tone accent cascades into
  the FAQ-item question icons within that section.
- Each Q/A keeps the native <details>/<summary> accordion but now
  includes a question-mark icon prefix and a chevron suffix that
  rotates 180deg when open. Subtle translate-x on hover, hairline
  shadow when expanded.
- Live search: filters questions in real time, hides empty sections,
  auto-opens matching items so the user sees the context, and shows
  an empty-state message + clear button when nothing matches.
- Bottom CTA card with a chat icon, "Still need help?" heading, and
  two action buttons (GitHub Discussions + browse docs).

The FAQ content lives inline in faq.astro as a typed `Section[]` data
structure (replaces the old MDX import) which made the per-item icons
and the section-tone variants straightforward to wire up.

CSS budget bumped 48->50 KB unminified and 44->46 KB minified to fit
the new FAQ styles. Other pages unaffected -- the entire FAQ stylesheet
is scoped under body.faq-page.
Aigars Silkalns 1 日 前
コミット
d24c683ecf
3 ファイル変更753 行追加30 行削除
  1. 4 4
      .bundlewatch.config.json
  2. 329 26
      src/html/pages/docs/faq.astro
  3. 420 0
      src/scss/_docs.scss

+ 4 - 4
.bundlewatch.config.json

@@ -2,19 +2,19 @@
   "files": [
     {
       "path": "./dist/css/adminlte.css",
-      "maxSize": "48 kB"
+      "maxSize": "50 kB"
     },
     {
       "path": "./dist/css/adminlte.min.css",
-      "maxSize": "44 kB"
+      "maxSize": "46 kB"
     },
     {
       "path": "./dist/css/adminlte.rtl.css",
-      "maxSize": "48 kB"
+      "maxSize": "50 kB"
     },
     {
       "path": "./dist/css/adminlte.rtl.min.css",
-      "maxSize": "44 kB"
+      "maxSize": "46 kB"
     },
     {
       "path": "./dist/js/adminlte.js",

+ 329 - 26
src/html/pages/docs/faq.astro

@@ -2,7 +2,6 @@
 import Head from "@components/_head.astro"
 import Footer from "@components/dashboard/_footer.astro"
 import Topbar from "@components/dashboard/_topbar.astro"
-import Faq from "@components/docs/faq.mdx"
 import Sidenav from "@components/dashboard/_sidenav.astro"
 import Scripts from "@components/_scripts.astro"
 
@@ -10,28 +9,170 @@ const title = "FAQ | AdminLTE 4"
 const path = "../../../dist"
 const mainPage = "docs"
 const page = "faq";
+
+type Qa = { q: string; a: string }
+type Section = { id: string; title: string; icon: string; tone: string; items: Qa[] }
+
+const sections: Section[] = [
+  {
+    id: "getting-started",
+    title: "Getting started",
+    icon: "rocket-takeoff",
+    tone: "primary",
+    items: [
+      {
+        q: "What is AdminLTE, exactly?",
+        a: `A free, MIT-licensed admin dashboard <em>template</em>. It's a set of HTML, CSS, and JavaScript files built on top of Bootstrap 5.3 — not a framework, not an npm component library. You drop it into your project, modify the markup to suit your app, and ship.`
+      },
+      {
+        q: "Do I need a build step to use it?",
+        a: `No. The <a href="getting-started.html">Getting Started</a> guide shows the CDN-only approach — copy four <code>&lt;link&gt;</code> tags and four <code>&lt;script&gt;</code> tags into your HTML and you're done. The npm-based workflow is there if you want to customise SCSS variables or tree-shake the JavaScript.`
+      },
+      {
+        q: "Which Bootstrap version does v4 require?",
+        a: `Bootstrap 5.3.x. The current <code>package.json</code> pins 5.3.8 — older 5.3 minors should work but aren't tested. Bootstrap 5.0 / 5.1 / 5.2 will not work (the color-mode and <code>data-bs-theme</code> system landed in 5.3).`
+      },
+      {
+        q: "Can I use AdminLTE 4 with jQuery?",
+        a: `You don't <em>need</em> jQuery — AdminLTE 4's JavaScript is vanilla. If your existing app already uses jQuery, it'll coexist fine. But none of AdminLTE 4's own plugins call into jQuery, and there are no <code>$.fn.xxx()</code> plugin shims.`
+      }
+    ]
+  },
+  {
+    id: "framework-integrations",
+    title: "Framework integrations",
+    icon: "puzzle",
+    tone: "info",
+    items: [
+      {
+        q: "Can AdminLTE be used with WordPress?",
+        a: `Yes — it's just HTML/CSS/JS. The typical path is to build a custom WordPress theme that wraps AdminLTE's markup around <code>wp_head()</code>, <code>wp_footer()</code>, and WordPress's loops. AdminLTE doesn't ship a WordPress-specific build; the work of porting nav-walker classes and authentication forms is yours.`
+      },
+      {
+        q: "Laravel? Symfony? Yii? Django? Rails?",
+        a: `All workable. AdminLTE is a server-rendered template — copy the demo pages into your views/templates, replace static content with your framework's template variables, and wire up routes/auth as usual. Community Composer/Packagist packages exist for Laravel and Symfony, though they typically lag the upstream version — verify they ship v4 before you depend on them.`
+      },
+      {
+        q: "React, Vue, Svelte, Angular?",
+        a: `<p>Workable but less natural. AdminLTE's JavaScript plugins use DOM lifecycle hooks (<code>DOMContentLoaded</code>, MutationObserver patterns) which fight with framework reconciliation. If you're using a SPA framework, consider:</p>
+<ul>
+  <li>Using AdminLTE <em>only</em> for CSS — keep the visual style, but write your own React/Vue components for the interactive parts.</li>
+  <li>Or pick a component library built for your framework (PrimeReact, Vuetify, etc.).</li>
+</ul>
+<p class="mb-0">We don't recommend wrapping AdminLTE's jQuery-era plugins in SPA framework components — it's a maintenance burden that outweighs the visual win.</p>`
+      }
+    ]
+  },
+  {
+    id: "customization",
+    title: "Customization",
+    icon: "palette",
+    tone: "warning",
+    items: [
+      {
+        q: "How do I change the primary colour?",
+        a: `<p>Override <code>--bs-primary</code> (and its RGB counterpart) on <code>:root</code>:</p>
+<pre class="astro-code"><code>:root {
+  --bs-primary: #6610f2;
+  --bs-primary-rgb: 102, 16, 242;
+}</code></pre>
+<p class="mb-0">For deeper control (sidebar width, breakpoints, spacing scale), see <a href="customization.html">Customization &amp; Theming</a>.</p>`
+      },
+      {
+        q: "How do I add a date picker / multi-select / rich text editor?",
+        a: `AdminLTE deliberately doesn't bundle these — they'd bloat the framework. The <a href="integrations.html">Recommended Integrations</a> page lists best-in-class third-party libraries (Flatpickr, Tom Select, Quill, etc.) with copy-paste install snippets.`
+      },
+      {
+        q: "Can I use FontAwesome instead of Bootstrap Icons?",
+        a: `Yes. Bootstrap Icons is the default in the demos because it's MIT, ships with Bootstrap, and is SVG-based — but nothing in AdminLTE's CSS or JS requires it. Drop in FontAwesome's stylesheet and replace <code>&lt;i class="bi bi-x"&gt;</code> with <code>&lt;i class="fas fa-x"&gt;</code> throughout.`
+      }
+    ]
+  },
+  {
+    id: "versions",
+    title: "Versions and updates",
+    icon: "arrow-clockwise",
+    tone: "success",
+    items: [
+      {
+        q: "How do I get notified of new versions?",
+        a: `<a href="https://github.com/ColorlibHQ/AdminLTE">Watch the GitHub repo</a> (Releases-only mode) or subscribe to the RSS feed at <code>https://github.com/ColorlibHQ/AdminLTE/releases.atom</code>.`
+      },
+      {
+        q: "Where's the AdminLTE 3 documentation?",
+        a: `Still online at <a href="https://adminlte.io/docs/3.2/">adminlte.io/docs/3.2</a> and earlier versions are linked there. The v3 branch on GitHub continues to receive critical bug fixes; new features land in v4 only.`
+      },
+      {
+        q: "How do I upgrade from v3 to v4?",
+        a: `See the dedicated <a href="migration.html">Migration from v3</a> guide. The short version: class names changed (<code>.wrapper</code> → <code>.app-wrapper</code>), <code>data-toggle</code> → <code>data-bs-toggle</code>, dark mode uses <code>data-bs-theme</code> instead of <code>.dark-mode</code>, and jQuery isn't required anymore.`
+      }
+    ]
+  },
+  {
+    id: "licensing",
+    title: "Licensing",
+    icon: "shield-check",
+    tone: "secondary",
+    items: [
+      {
+        q: "Is AdminLTE free for commercial use?",
+        a: `Yes — MIT licensed. You can use it in commercial products, SaaS apps, client work, anything. The only requirement is preserving the copyright notice in the source files (<code>adminlte.css</code> / <code>adminlte.js</code>). You don't need to display attribution in the UI.`
+      },
+      {
+        q: "Do I need to credit AdminLTE in my app?",
+        a: `No. The MIT license requires you to keep the licence notice in the source files you distribute — that's it. You don't have to mention AdminLTE in the rendered UI, your About page, or your README.`
+      },
+      {
+        q: "Can I sell a product made with AdminLTE?",
+        a: `Yes. The MIT license explicitly permits this. You can also resell modified versions of AdminLTE itself (theme marketplaces frequently do this) — just preserve the original copyright notice.`
+      }
+    ]
+  },
+  {
+    id: "troubleshooting",
+    title: "Troubleshooting",
+    icon: "wrench-adjustable",
+    tone: "danger",
+    items: [
+      {
+        q: "The sidebar doesn't collapse when I click the hamburger menu.",
+        a: `Check that the PushMenu plugin is loaded. It's bundled into <code>adminlte.js</code> — make sure the script tag is on the page and <em>after</em> the markup it operates on. Also verify your hamburger button has <code>data-lte-toggle="sidebar"</code> (not <code>data-widget="pushmenu"</code>, which is the v3 attribute).`
+      },
+      {
+        q: "Dark mode doesn't persist after refresh.",
+        a: `The included <a href="color-mode.html">Color Mode</a> toggle writes to <code>localStorage</code> under the key <code>lte-theme</code>. If you're using a different toggle implementation, make sure it both sets <code>document.documentElement.setAttribute('data-bs-theme', ...)</code> and writes to localStorage on change.`
+      },
+      {
+        q: "Bootstrap modal closes when I press Escape, but the AdminLTE sidebar closes too.",
+        a: `Fixed in v4.0.0 (#5993). If you're still seeing this, you're on an older RC — update to the latest.`
+      },
+      {
+        q: "My custom SCSS doesn't override AdminLTE's defaults.",
+        a: `Put your overrides <em>before</em> the <code>@import</code> of AdminLTE's SCSS. AdminLTE uses <code>!default</code> on its variables, which means the first declaration wins. See <a href="customization.html#scss-variables">Customization &amp; Theming</a> for the import order.`
+      },
+      {
+        q: "<code>npm install</code> fails with peer dependency errors.",
+        a: `We use an npm <code>overrides</code> block in <code>package.json</code> to keep peers happy — make sure you're on npm 8.3+ which respects them. If you're on an older npm, install with <code>--legacy-peer-deps</code>.`
+      }
+    ]
+  }
+];
+
+const totalQuestions = sections.reduce((sum, s) => sum + s.items.length, 0);
 ---
 
 <!DOCTYPE html>
 <html lang="en">
-  <!--begin::Head-->
   <head>
     <Head title={title} path={path} />
   </head>
-  <!--end::Head-->
-  <!--begin::Body-->
-  <body class="layout-fixed sidebar-expand-lg bg-body-tertiary docs-page">
-    <!--begin::App Wrapper-->
+  <body class="layout-fixed sidebar-expand-lg bg-body-tertiary docs-page faq-page">
     <div class="app-wrapper">
       <Topbar path={path} />
       <Sidenav path={path} mainPage={mainPage} page={page} />
-      <!--begin::App Main-->
       <main class="app-main">
-        <!--begin::App Content Header-->
         <div class="app-content-header">
-          <!--begin::Container-->
           <div class="container-fluid">
-            <!--begin::Row-->
             <div class="row">
               <div class="col-sm-6">
                 <h3 class="mb-0">FAQ</h3>
@@ -39,33 +180,195 @@ const page = "faq";
               <div class="col-sm-6">
                 <ol class="breadcrumb float-sm-end">
                   <li class="breadcrumb-item"><a href="#">Docs</a></li>
-                  <li class="breadcrumb-item active" aria-current="page">
-                    FAQ
-                  </li>
+                  <li class="breadcrumb-item active" aria-current="page">FAQ</li>
                 </ol>
               </div>
             </div>
-            <!--end::Row-->
           </div>
-          <!--end::Container-->
         </div>
-        <!--end::App Content Header-->
-        <!--begin::App Content-->
         <div class="app-content">
-          <!--begin::Container-->
           <div class="container-fluid">
-            <Faq />
+
+            <!-- Hero -->
+            <section class="faq-hero text-center mb-4">
+              <span class="faq-hero-eyebrow">
+                <i class="bi bi-patch-question-fill" aria-hidden="true"></i>
+                Frequently Asked Questions
+              </span>
+              <h1 class="faq-hero-title">How can we help?</h1>
+              <p class="faq-hero-lead">
+                Quick answers to the {totalQuestions} questions we get asked most often.
+                Use the search to jump to anything, or click a topic below.
+              </p>
+              <form class="faq-search" role="search" onsubmit="return false;">
+                <i class="bi bi-search faq-search-icon" aria-hidden="true"></i>
+                <input
+                  type="search"
+                  id="faq-search-input"
+                  class="form-control form-control-lg"
+                  placeholder="Search the FAQ&hellip;"
+                  aria-label="Search the FAQ"
+                  autocomplete="off"
+                />
+                <button
+                  type="button"
+                  class="faq-search-clear d-none"
+                  id="faq-search-clear"
+                  aria-label="Clear search"
+                >
+                  <i class="bi bi-x-lg" aria-hidden="true"></i>
+                </button>
+              </form>
+              <p class="faq-empty-state d-none" id="faq-empty-state">
+                <i class="bi bi-emoji-frown" aria-hidden="true"></i>
+                No questions match your search. Try a different keyword or
+                <a href="https://github.com/ColorlibHQ/AdminLTE/discussions" target="_blank" rel="noopener">open a Discussion</a>.
+              </p>
+            </section>
+
+            <!-- Section nav chips -->
+            <nav class="faq-chips mb-4" aria-label="FAQ sections">
+              {
+                sections.map((s) => (
+                  <a href={`#${s.id}`} class={`faq-chip faq-chip-${s.tone}`}>
+                    <i class={`bi bi-${s.icon}`} aria-hidden="true"></i>
+                    <span>{s.title}</span>
+                    <span class="faq-chip-count">{s.items.length}</span>
+                  </a>
+                ))
+              }
+            </nav>
+
+            <!-- Sections -->
+            <div class="faq-sections">
+              {
+                sections.map((section) => (
+                  <section id={section.id} class="faq-section" data-faq-section>
+                    <header class={`faq-section-header faq-section-${section.tone}`}>
+                      <span class="faq-section-icon">
+                        <i class={`bi bi-${section.icon}`} aria-hidden="true" />
+                      </span>
+                      <div>
+                        <h2 class="faq-section-title">{section.title}</h2>
+                        <p class="faq-section-count">
+                          {section.items.length} {section.items.length === 1 ? "question" : "questions"}
+                        </p>
+                      </div>
+                    </header>
+                    <div class="faq-section-items">
+                      {section.items.map((item) => (
+                        <details class="faq-item" data-faq-item>
+                          <summary>
+                            <span class="faq-q-icon">
+                              <i class="bi bi-question-lg" aria-hidden="true" />
+                            </span>
+                            <span class="faq-q-text" set:html={item.q} />
+                            <span class="faq-q-chevron">
+                              <i class="bi bi-chevron-down" aria-hidden="true" />
+                            </span>
+                          </summary>
+                          <div class="faq-answer" set:html={item.a} />
+                        </details>
+                      ))}
+                    </div>
+                  </section>
+                ))
+              }
+            </div>
+
+            <!-- CTA footer -->
+            <section class="faq-cta mt-5">
+              <div class="faq-cta-icon">
+                <i class="bi bi-chat-quote" aria-hidden="true"></i>
+              </div>
+              <h2>Still need help?</h2>
+              <p>
+                Open a discussion on GitHub or browse the documentation for more
+                in-depth answers.
+              </p>
+              <div class="faq-cta-actions">
+                <a
+                  href="https://github.com/ColorlibHQ/AdminLTE/discussions"
+                  target="_blank"
+                  rel="noopener"
+                  class="btn btn-primary"
+                >
+                  <i class="bi bi-github me-2" aria-hidden="true"></i>
+                  Open a Discussion
+                </a>
+                <a href="introduction.html" class="btn btn-outline-secondary">
+                  <i class="bi bi-book me-2" aria-hidden="true"></i>
+                  Browse Documentation
+                </a>
+              </div>
+            </section>
           </div>
-          <!--end::Container-->
         </div>
-        <!--end::App Content-->
       </main>
-      <!--end::App Main-->
       <Footer />
     </div>
-    <!--end::App Wrapper-->
-    <!--begin::Script-->
     <Scripts path={path} />
-    <!--end::Script-->
-  </body><!--end::Body-->
+
+    <script is:inline>
+      // FAQ live search + clear button
+      ;(() => {
+        "use strict"
+        const input = document.getElementById("faq-search-input")
+        const clearBtn = document.getElementById("faq-search-clear")
+        const emptyState = document.getElementById("faq-empty-state")
+        if (!input) return
+
+        const items = Array.from(document.querySelectorAll("[data-faq-item]"))
+        const sections = Array.from(document.querySelectorAll("[data-faq-section]"))
+
+        const normalize = (s) => s.toLowerCase().trim()
+
+        const filter = (query) => {
+          const q = normalize(query)
+          clearBtn.classList.toggle("d-none", q.length === 0)
+
+          let visibleTotal = 0
+          for (const item of items) {
+            const text = item.textContent || ""
+            const matches = q === "" || normalize(text).includes(q)
+            item.classList.toggle("d-none", !matches)
+            // Open matching items when searching so users see context
+            item.open = q !== "" && matches
+            if (matches) visibleTotal++
+          }
+
+          // Hide entire sections that have no visible items
+          for (const section of sections) {
+            const hasVisible = section.querySelectorAll(
+              "[data-faq-item]:not(.d-none)"
+            ).length > 0
+            section.classList.toggle("d-none", !hasVisible)
+          }
+
+          emptyState.classList.toggle("d-none", visibleTotal !== 0)
+        }
+
+        input.addEventListener("input", (e) => filter(e.target.value))
+
+        clearBtn.addEventListener("click", () => {
+          input.value = ""
+          filter("")
+          input.focus()
+        })
+
+        // Smooth scroll to section on chip click + offset for sticky header
+        for (const chip of document.querySelectorAll(".faq-chip")) {
+          chip.addEventListener("click", (e) => {
+            const id = chip.getAttribute("href")?.replace("#", "")
+            if (!id) return
+            const target = document.getElementById(id)
+            if (!target) return
+            e.preventDefault()
+            target.scrollIntoView({ behavior: "smooth", block: "start" })
+            history.replaceState(null, "", "#" + id)
+          })
+        }
+      })()
+    </script>
+  </body>
 </html>

+ 420 - 0
src/scss/_docs.scss

@@ -398,3 +398,423 @@
     }
   }
 }
+
+//
+// FAQ page — custom layout that breaks out of the standard docs card
+// to give the FAQ a more distinctive visual identity. Opt in via
+// body.faq-page (set in pages/docs/faq.astro).
+//
+
+.faq-page {
+  // The FAQ doesn't use the standard card wrapper used by other docs,
+  // so the container can stretch wider for the chip strip and section
+  // grid.
+  .app-content > .container-fluid {
+    max-width: 64rem;
+
+    @include media-breakpoint-up(xxl) {
+      max-width: 72rem;
+    }
+  }
+
+  // --- Hero -------------------------------------------------------------
+  .faq-hero {
+    position: relative;
+    padding: 3rem 1.5rem 2.5rem;
+    margin-top: 1rem;
+    overflow: hidden;
+    background: radial-gradient(ellipse at top left, rgba(var(--#{$prefix}primary-rgb), .12), transparent 60%), radial-gradient(ellipse at bottom right, rgba(var(--#{$prefix}info-rgb), .1), transparent 65%), var(--#{$prefix}body-bg);
+    border: 1px solid var(--#{$prefix}border-color);
+    @include border-radius($border-radius-xl);
+  }
+
+  .faq-hero-eyebrow {
+    display: inline-flex;
+    gap: .4rem;
+    align-items: center;
+    padding: .35rem .85rem;
+    margin-bottom: 1rem;
+    font-size: .75rem;
+    font-weight: 600;
+    color: var(--#{$prefix}primary);
+    text-transform: uppercase;
+    letter-spacing: .08em;
+    background: var(--#{$prefix}primary-bg-subtle);
+    border: 1px solid var(--#{$prefix}primary-border-subtle);
+    @include border-radius(50rem);
+
+    .bi {
+      font-size: .9rem;
+    }
+  }
+
+  .faq-hero-title {
+    margin: 0 0 .5rem;
+    font-size: 2.25rem;
+    font-weight: 700;
+    line-height: 1.15;
+    color: var(--#{$prefix}emphasis-color);
+
+    @include media-breakpoint-up(md) {
+      font-size: 2.75rem;
+    }
+  }
+
+  .faq-hero-lead {
+    max-width: 36rem;
+    margin: 0 auto 1.75rem;
+    font-size: 1.05rem;
+    line-height: 1.55;
+    color: var(--#{$prefix}secondary-color);
+  }
+
+  .faq-search {
+    position: relative;
+    max-width: 32rem;
+    margin: 0 auto;
+
+    .form-control {
+      height: 3rem;
+      padding-right: 3rem;
+      padding-left: 3rem;
+      font-size: 1rem;
+      background: var(--#{$prefix}body-bg);
+      @include border-radius(50rem);
+      @include transition(box-shadow .15s ease, border-color .15s ease);
+
+      &:focus {
+        box-shadow: 0 0 0 .25rem rgba(var(--#{$prefix}primary-rgb), .15);
+      }
+    }
+  }
+
+  .faq-search-icon {
+    position: absolute;
+    top: 50%;
+    left: 1.2rem;
+    color: var(--#{$prefix}secondary-color);
+    pointer-events: none;
+    transform: translateY(-50%);
+  }
+
+  .faq-search-clear {
+    position: absolute;
+    top: 50%;
+    right: .65rem;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 2rem;
+    height: 2rem;
+    padding: 0;
+    color: var(--#{$prefix}secondary-color);
+    background: var(--#{$prefix}tertiary-bg);
+    border: 0;
+    transform: translateY(-50%);
+    @include border-radius(50%);
+    @include transition(background .15s ease, color .15s ease);
+
+    &:hover {
+      color: var(--#{$prefix}body-color);
+      background: var(--#{$prefix}secondary-bg);
+    }
+  }
+
+  .faq-empty-state {
+    max-width: 32rem;
+    padding: 1rem;
+    margin: 1.5rem auto 0;
+    font-size: .95rem;
+    color: var(--#{$prefix}secondary-color);
+    background: var(--#{$prefix}tertiary-bg);
+    @include border-radius($border-radius);
+
+    .bi {
+      margin-right: .35rem;
+    }
+  }
+
+  // --- Section nav chips ------------------------------------------------
+  .faq-chips {
+    display: flex;
+    flex-wrap: wrap;
+    gap: .5rem;
+    justify-content: center;
+  }
+
+  .faq-chip {
+    display: inline-flex;
+    gap: .45rem;
+    align-items: center;
+    padding: .45rem .85rem;
+    font-size: .875rem;
+    font-weight: 500;
+    color: var(--#{$prefix}body-color);
+    text-decoration: none;
+    background: var(--#{$prefix}body-bg);
+    border: 1px solid var(--#{$prefix}border-color);
+    @include border-radius(50rem);
+    @include transition(transform .15s ease, border-color .15s ease, background-color .15s ease, color .15s ease);
+
+    &:hover {
+      color: var(--#{$prefix}emphasis-color);
+      background: var(--#{$prefix}tertiary-bg);
+      transform: translateY(-1px);
+    }
+
+    .bi {
+      font-size: 1rem;
+    }
+  }
+
+  .faq-chip-count {
+    padding: 0 .45rem;
+    font-size: .7rem;
+    font-weight: 600;
+    color: var(--#{$prefix}secondary-color);
+    background: var(--#{$prefix}tertiary-bg);
+    @include border-radius(50rem);
+  }
+
+  // Per-section chip accent on hover/active
+  @each $tone in (primary, info, warning, success, danger, secondary) {
+    .faq-chip-#{$tone}:hover {
+      color: var(--#{$prefix}#{$tone}-text-emphasis);
+      background: var(--#{$prefix}#{$tone}-bg-subtle);
+      border-color: var(--#{$prefix}#{$tone}-border-subtle);
+
+      .faq-chip-count {
+        color: var(--#{$prefix}#{$tone}-text-emphasis);
+        background: rgba(0, 0, 0, .06);
+      }
+    }
+  }
+
+  // --- Sections ---------------------------------------------------------
+  .faq-section {
+    margin-bottom: 2.5rem;
+    scroll-margin-top: 5rem;
+  }
+
+  .faq-section-header {
+    display: flex;
+    gap: 1rem;
+    align-items: center;
+    padding: 1rem 1.25rem;
+    margin-bottom: 1rem;
+    background: var(--#{$prefix}body-bg);
+    border: 1px solid var(--#{$prefix}border-color);
+    border-left: 4px solid var(--#{$prefix}primary);
+    @include border-radius($border-radius);
+  }
+
+  .faq-section-icon {
+    display: inline-flex;
+    flex-shrink: 0;
+    align-items: center;
+    justify-content: center;
+    width: 2.75rem;
+    height: 2.75rem;
+    font-size: 1.35rem;
+    color: var(--#{$prefix}primary);
+    background: var(--#{$prefix}primary-bg-subtle);
+    @include border-radius($border-radius);
+  }
+
+  .faq-section-title {
+    margin: 0;
+    font-size: 1.4rem;
+    font-weight: 600;
+    line-height: 1.2;
+    color: var(--#{$prefix}emphasis-color);
+  }
+
+  .faq-section-count {
+    margin: .1rem 0 0;
+    font-size: .8rem;
+    color: var(--#{$prefix}secondary-color);
+  }
+
+  // Per-section tone accents
+  @each $tone in (primary, info, warning, success, danger, secondary) {
+    .faq-section-#{$tone} {
+      border-left-color: var(--#{$prefix}#{$tone});
+
+      .faq-section-icon {
+        color: var(--#{$prefix}#{$tone}-text-emphasis);
+        background: var(--#{$prefix}#{$tone}-bg-subtle);
+      }
+    }
+  }
+
+  // --- FAQ items (override the generic .docs-page details styling) ------
+  .faq-section-items .faq-item {
+    margin-bottom: .5rem;
+    overflow: hidden;
+    background: var(--#{$prefix}body-bg);
+    border: 1px solid var(--#{$prefix}border-color);
+    @include border-radius($border-radius);
+    @include transition(border-color .15s ease, box-shadow .15s ease, transform .15s ease);
+
+    &:hover {
+      border-color: var(--#{$prefix}primary-border-subtle);
+      transform: translateX(2px);
+    }
+
+    &[open] {
+      border-color: var(--#{$prefix}primary-border-subtle);
+      box-shadow: 0 2px 8px rgba(0, 0, 0, .04);
+      transform: none;
+    }
+
+    summary {
+      display: flex;
+      gap: .85rem;
+      align-items: center;
+      padding: 1rem 1.25rem;
+      font-size: 1rem;
+      font-weight: 500;
+      color: var(--#{$prefix}emphasis-color);
+      list-style: none;
+      cursor: pointer;
+      user-select: none;
+
+      &::-webkit-details-marker {
+        display: none;
+      }
+
+      &::marker {
+        content: "";
+      }
+
+      &::after {
+        content: none; // disable the generic chevron, we use our own
+      }
+    }
+
+    .faq-q-icon {
+      display: inline-flex;
+      flex-shrink: 0;
+      align-items: center;
+      justify-content: center;
+      width: 1.75rem;
+      height: 1.75rem;
+      font-size: .85rem;
+      color: var(--#{$prefix}primary);
+      background: var(--#{$prefix}primary-bg-subtle);
+      @include border-radius(50%);
+    }
+
+    .faq-q-text {
+      flex-grow: 1;
+    }
+
+    .faq-q-chevron {
+      flex-shrink: 0;
+      color: var(--#{$prefix}secondary-color);
+      @include transition(transform .2s ease, color .2s ease);
+    }
+
+    &[open] .faq-q-chevron {
+      color: var(--#{$prefix}primary);
+      transform: rotate(180deg);
+    }
+
+    &[open] summary {
+      color: var(--#{$prefix}primary);
+      border-bottom: 1px solid var(--#{$prefix}border-color);
+    }
+
+    .faq-answer {
+      padding: 1.1rem 1.25rem 1.1rem 3.85rem; // align with the question text (icon + gap)
+      font-size: .95rem;
+      line-height: 1.65;
+      color: var(--#{$prefix}body-color);
+
+      p:last-child,
+      ul:last-child,
+      ol:last-child,
+      pre:last-child {
+        margin-bottom: 0;
+      }
+
+      a {
+        color: var(--#{$prefix}primary);
+        text-decoration: underline;
+        text-underline-offset: 2px;
+
+        &:hover {
+          text-decoration-thickness: 2px;
+        }
+      }
+
+      code {
+        padding: .12em .35em;
+        font-size: .875em;
+        color: var(--#{$prefix}emphasis-color);
+        background: var(--#{$prefix}tertiary-bg);
+        border: 1px solid var(--#{$prefix}border-color);
+        @include border-radius($border-radius-sm);
+      }
+
+      pre.astro-code,
+      pre {
+        padding: .85rem 1rem;
+        margin-top: .75rem;
+        margin-bottom: .75rem;
+        overflow-x: auto;
+        font-size: .85rem;
+        line-height: 1.55;
+        @include border-radius($border-radius-sm);
+
+        code {
+          padding: 0;
+          background: transparent;
+          border: 0;
+        }
+      }
+    }
+  }
+
+  // --- CTA footer -------------------------------------------------------
+  .faq-cta {
+    padding: 2.5rem 1.5rem;
+    text-align: center;
+    background: radial-gradient(ellipse at top, rgba(var(--#{$prefix}primary-rgb), .08), transparent 60%), var(--#{$prefix}body-bg);
+    border: 1px solid var(--#{$prefix}border-color);
+    @include border-radius($border-radius-xl);
+  }
+
+  .faq-cta-icon {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 3.5rem;
+    height: 3.5rem;
+    margin-bottom: 1rem;
+    font-size: 1.6rem;
+    color: var(--#{$prefix}primary);
+    background: var(--#{$prefix}primary-bg-subtle);
+    @include border-radius(50%);
+  }
+
+  .faq-cta h2 {
+    margin: 0 0 .5rem;
+    font-size: 1.5rem;
+    font-weight: 600;
+    color: var(--#{$prefix}emphasis-color);
+  }
+
+  .faq-cta p {
+    max-width: 32rem;
+    margin: 0 auto 1.5rem;
+    color: var(--#{$prefix}secondary-color);
+  }
+
+  .faq-cta-actions {
+    display: inline-flex;
+    flex-wrap: wrap;
+    gap: .5rem;
+    justify-content: center;
+  }
+}