diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bf525f6..6491d36 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,17 @@ jobs: echo "ALGOLIA_INDEX_VALUE=" >> $GITHUB_ENV fi + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + + - name: Build Tailwind CSS + run: | + npm ci + npm run build:css + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 diff --git a/.gitignore b/.gitignore index 22d8dca..404e2a4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,14 @@ output/ tmp/ vendor/bundle crash.log +output.diff content/robots.txt /examples/ *.sketch + +# Tailwind build (Node.js deps + generated CSS — rebuilt in dev and in CI) +node_modules/ +content/assets/stylesheets/tailwind_built.css +content/assets/stylesheets/tailwind_built.css.map diff --git a/Dockerfile b/Dockerfile index 5c3bbfd..7b24a03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM ruby:3.3 ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 -RUN apt-get update -qq && apt-get install -qq -y build-essential nodejs libjq-dev +RUN apt-get update -qq && apt-get install -qq -y build-essential nodejs npm libjq-dev WORKDIR /app diff --git a/config.yaml b/config.yaml index 1826eb7..e891914 100644 --- a/config.yaml +++ b/config.yaml @@ -85,6 +85,11 @@ checks: - https://www.godaddy.com/ - https://www.npmjs.com/package/cloudmailin # cloudflare protection raises 403 - https://docs.cloudmailin.com/features/dmarc-verification/ # self-referential redirect + # rel="preconnect" hints for Google Fonts (layouts/application.haml) — + # bare origins with no path, so they 404 on a normal request even + # though the preconnect itself is legitimate. + - https://fonts.googleapis.com + - https://fonts.gstatic.com # Algolia search: pages from the main website fetched and indexed at # build time alongside the docs content (see lib/algolia_external.rb). diff --git a/content/assets/images/cloudmailin-lockup-nav.svg b/content/assets/images/cloudmailin-lockup-nav.svg new file mode 100644 index 0000000..8da1d9c --- /dev/null +++ b/content/assets/images/cloudmailin-lockup-nav.svg @@ -0,0 +1,41 @@ + diff --git a/content/assets/stylesheets/_bootstrap_choices.scss b/content/assets/stylesheets/_bootstrap_choices.scss deleted file mode 100644 index 89e3855..0000000 --- a/content/assets/stylesheets/_bootstrap_choices.scss +++ /dev/null @@ -1,56 +0,0 @@ -/*! - * Bootstrap v3.4.1 (https://getbootstrap.com/) - * Copyright 2011-2019 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -// Core variables and mixins -@import "bootstrap/variables"; -@import "bootstrap/mixins"; - -// Reset and dependencies -@import "bootstrap/normalize"; -@import "bootstrap/print"; -@import "bootstrap/glyphicons"; - -// Core CSS -@import "bootstrap/scaffolding"; -@import "bootstrap/type"; -@import "bootstrap/code"; -@import "bootstrap/grid"; -@import "bootstrap/tables"; -@import "bootstrap/forms"; -@import "bootstrap/buttons"; - -// Components -@import "bootstrap/component-animations"; -@import "bootstrap/dropdowns"; -@import "bootstrap/button-groups"; -@import "bootstrap/input-groups"; -@import "bootstrap/navs"; -@import "bootstrap/navbar"; -@import "bootstrap/breadcrumbs"; -@import "bootstrap/pagination"; -@import "bootstrap/pager"; -@import "bootstrap/labels"; -@import "bootstrap/badges"; -@import "bootstrap/jumbotron"; -@import "bootstrap/thumbnails"; -@import "bootstrap/alerts"; -@import "bootstrap/progress-bars"; -@import "bootstrap/media"; -@import "bootstrap/list-group"; -@import "bootstrap/panels"; -@import "bootstrap/responsive-embed"; -@import "bootstrap/wells"; -@import "bootstrap/close"; - -// Components w/ JavaScript -@import "bootstrap/modals"; -@import "bootstrap/tooltip"; -@import "bootstrap/popovers"; -@import "bootstrap/carousel"; - -// Utility classes -@import "bootstrap/utilities"; -@import "bootstrap/responsive-utilities"; diff --git a/content/assets/stylesheets/docs_new.scss b/content/assets/stylesheets/docs_new.scss index a9c1772..3434ffc 100644 --- a/content/assets/stylesheets/docs_new.scss +++ b/content/assets/stylesheets/docs_new.scss @@ -1,332 +1,98 @@ -$brand-primary: #4d7ABA !default; -// $brand-primary: #ff6600 !default; - -$navbar-height: 35px; -$navbar-default-bg: $brand-primary; - -$navbar-default-color: #fff; -$navbar-default-link-color: #fff; -$navbar-default-link-hover-color: #fff; -$navbar-default-link-active-color: #fff; -$navbar-default-brand-hover-color: #fff; - -$pre-bg: mix(lighten($brand-primary, 25), #fff, 5); -$table-head-bg: mix(darken($brand-primary, 25), #fff, 6); -$table-bg-accent: $pre-bg; - -$search-result-prevew-color: #000; - -@import "bootstrap-sprockets"; -@import "bootstrap_choices"; +// docs.cloudmailin.com — legacy stylesheet, now Bootstrap-free. +// +// Bootstrap 3 was removed as part of the Tailwind redesign. Tailwind v4's +// Preflight (shipped in tailwind_built.css) now provides the CSS reset, and +// all of the redesigned layout / navigation / content styling lives in +// tailwind/input.css. Only a handful of genuinely-still-needed, non-Tailwind +// pieces remain in this file: +// +// .container Bootstrap used to supply this; the redesigned layout +// still puts it on #docs-content and the footer rows +// (layouts/application.haml). Tailwind's own .container +// utility only sets width/max-width — it does NOT centre +// or pad — so we define a real centred container here. +// .info / .warning Development-only callouts (NANOC_ENV=development) +// emitted by layouts/application.haml. Bootstrap's +// .alert-info / .alert-warning used to style them. +// a.deprecated Sidebar "(deprecated)" markers (layouts/sidebar.haml). +// :root Algolia vars Customise the Algolia autocomplete-classic theme that +// is loaded from the CDN. +// +// This compiles to plain, UNLAYERED CSS. Per the CSS Cascade Layers spec an +// unlayered rule beats Tailwind's @layer utilities on a tie — which is why +// .container below wins over Tailwind's layered .container utility. Keep +// selectors here uniquely named so they don't clobber Tailwind utilities +// used directly in the HAML templates. :root { --aa-search-input-height: 30px; --aa-base-unit: 16; --aa-icon-size: 14px; - - --search-result-link-color: #666; - --search-result-prevew-color: #999; } -body { - background-color: $brand-primary; - // font-size: 1.2rem; - line-height: 1.8em; +// Centred page container (replaces Bootstrap 3's .container). Matches the +// ~1170px centred width the old Bootstrap container produced, with side +// gutters that widen on desktop. +.container { + width: 100%; + max-width: 72rem; + margin-inline: auto; + padding-inline: 1.25rem; } -p { - margin-top: 0.5em; - margin-bottom: 1em; -} - -a { - color: #0066cc; -} - -a.deprecated { - color: mix(#0066cc, #aaa, 20%); - - &::after { - content: ' (deprecated)'; - // color: #999; +@media (min-width: 64rem) { + .container { + padding-inline: 2rem; } } -h1 { - margin-bottom: 0.5em; +// Deprecated links in the sidebar. The link colour itself comes from +// .sidebar-link (tailwind/input.css); here we only append the label. +a.deprecated::after { + content: ' (deprecated)'; + opacity: 0.55; + font-size: 0.85em; } -h2 { - font-size: 2.5rem; -} - -h3 { - font-size: 2.25rem; - color: #333; -} - -h4 { - font-size: 2rem; - color: #666; -} - -nav { - @extend .navbar; - @extend .navbar-default; - box-shadow: 0px 1px 0px lighten($navbar-default-bg, 8); - - border: 0px; - border-radius: 0px; - padding-right: 10px; - margin-bottom: 0px; - - #nav-content { - @extend .container; - } - - header { - @extend .navbar-header; - width: inherit; - - #header_image { - @extend .navbar-brand; - padding-top: 4px; - padding-left: 30px; - } - - @media (max-width: $screen-sm-min) { - #header_image { - line-height: 40px; - } - } - } - - ul.home-nav { - @extend .nav; - @extend .navbar-nav; - padding-top: 2px; - } - - ul#search-nav { - @extend .nav; - @extend .navbar-nav; - @extend .navbar-right; - padding-left: 0px; - - #search-area { - margin-left: 10px; - } - - #searchbox { - padding: 0px 10px; - margin-top: 3px; - min-width: 380px; - max-width: 100%; - // width: 100%; - } - } -} - -.aa-Input { - padding-left: 3px; -} - -.aa-Item { - margin-bottom: 20px; -} - -a.search-result, a.search-result:hover { - text-decoration: none; - - h4 { - font-size: medium; - color: var(--search-result-link-color); - margin-top: 0px; - margin-bottom: 4px; - } +// Development-only callouts (NANOC_ENV=development). Replaces Bootstrap's +// .alert / .alert-info / .alert-warning. +.info, +.warning { + margin: 1rem 0; + padding: 0.75rem 1rem; + border: 1px solid transparent; + border-radius: 0.5rem; + font-size: 0.9rem; + line-height: 1.5; p { - font-size: small; - color: var(--search-result-prevew-color); - margin-bottom: 0px; - } - - .search-badge { - display: inline-block; - margin-left: 6px; - padding: 0px 6px; - border: 1px solid currentColor; - border-radius: 8px; - font-size: x-small; - font-weight: normal; - vertical-align: middle; - color: var(--search-result-prevew-color); - - &::after { - // north-east arrow, forced text presentation (no emoji rendering) - content: ' \2197\FE0E'; - } - } -} - -#middle { - background-color: #fff; - padding-top: 20px; - padding-bottom: 20px; -} - -#container { - @extend .container; - background-color: #fff; - // min-height: 80%; -} - -#sidebar { - @extend .col-md-4; - @extend .col-md-pull-8; - - @media (min-width: $screen-md-min) { - background: url('/assets/images/divider.png') no-repeat 80% 0%; - } - - ul { - // @extend .list-unstyled; - padding-left: 0; - list-style: none; - - li { - font-weight: bold; - padding-top: 10px; - padding-bottom: 5px; - - ul { - padding-left: 10px; - padding-top: 10px; - list-style-type: none; - - li { - padding-top: 0px; - font-weight: normal; - - ul { - padding-top: 5px; - } - } - } - } + margin: 0; } - // mobiles / smaller screen sidebar links - @media (max-width: $screen-md-min) { - ul li { - padding-top: 20px; - - ul li { - padding-top: 20px; - } - } + p + p { + margin-top: 0.5rem; } } -.documentation_content { - @extend .col-md-8; - @extend .col-md-push-4; - - img { - max-width: 80%; - border: 1px solid $pre-border-color; - margin: 20px 0px; - padding: 10px; - } - - .image-caption { - color: #666; - border: 1px solid $pre-border-color; - background-color: #f8f8f8; - margin-top: -20px; - margin-bottom: 0; - padding: 5px; - display: block; - } -} - -#footer { - margin-top: 20px; - padding-bottom: 20px; - - #details { - color: #fff; - @extend .container; - } -} - -table { - @extend .table; - @extend .table-bordered; - @extend .table-striped; - - thead { - background-color: $table-head-bg; - } -} - -.language_list { - background-color: $table-head-bg; - border: 1px solid $pre-border-color; - margin: 0px; - padding: 5px; - - .languages { - @extend .list-inline; - margin: 0px; - padding: 0px; - - a { - margin-right: 10px; - } - } -} - -pre { - border-radius: 0; - max-height: 300px; - overflow: auto; - // background-color: #f8f8f8; - background-color: #fff; - - code { - white-space: pre; - } -} - -blockquote { - font-size: inherit; - @extend .alert; - @extend .alert-info; - - p { - margin: 0px; - } -} - -#comments { - margin-top: 100px; -} - .info { - @extend .alert; - @extend .alert-info; + color: #1e40af; + background-color: #eff6ff; + border-color: #bfdbfe; } .warning { - @extend .alert; - @extend .alert-warning; -} - -#faqs { - h3 { - color: #000; - font-size: 16px; - font-weight: bold; - } + color: #92400e; + background-color: #fffbeb; + border-color: #fde68a; +} + +// The dev-mode attribute dump renders as
— it's
+// explicitly excluded from the terminal code-block styling in
+// tailwind/input.css (pre:not(.warning)), so keep it a plain, readable,
+// scrollable monospace block.
+pre.warning {
+ overflow-x: auto;
+ white-space: pre;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.8rem;
}
diff --git a/docker-compose.yml b/docker-compose.yml
index f8f4410..d2eb0ac 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,7 @@ services:
docs:
build: .
- command: bash -c "bundle exec guard & bundle exec nanoc live --port 80 --host 0.0.0.0"
+ command: bash -c "npm install && (bundle exec guard &) && (npx @tailwindcss/cli -i ./tailwind/input.css -o ./content/assets/stylesheets/tailwind_built.css --watch=always &) && bundle exec nanoc live --port 80 --host 0.0.0.0"
volumes:
- .:/app
env_file:
diff --git a/layouts/application.haml b/layouts/application.haml
index 4116d4b..b3e481d 100644
--- a/layouts/application.haml
+++ b/layouts/application.haml
@@ -6,8 +6,14 @@
%link{href: '/favicon.svg', rel: "icon", type: "image/svg+xml"}
%link{href: '/favicon_old.png', rel: "alternate icon", type: "image/png"}
%meta{"http-equiv": "Content-Type", content: "text/html; charset=utf-8"}
+
+ %link{rel: "preconnect", href: "https://fonts.googleapis.com"}
+ %link{rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous"}
+ %link{href: "https://fonts.googleapis.com/css2?family=Inter:wght@700&family=Open+Sans:ital,wght@0,400;0,600;0,700;1,400&family=DM+Mono:wght@400;500&display=swap", rel: "stylesheet"}
+
+
-#
@@ -39,19 +45,43 @@
%meta{name: 'twitter:creator', content: '@cloudmailin'}
%meta{name: 'twitter:card', content: 'summary_large_image'}
%body#documentation
- %nav#nav
- #nav-content
- %header#header
- #header_image
- = link_to image_tag('logo_trans_no_com.png', alt: 'cloudmailin logo, incoming email for your web app', style: 'height: 30px'), 'https://www.cloudmailin.com'
- %ul.home-nav
- %li= link_to 'Return to CloudMailin.com', 'https://www.cloudmailin.com'
- %ul#search-nav
- %li#search-area
- #searchbox
-
- #middle
- #container
+ .docs-topbar
+ %nav.docs-topbar-nav
+ .docs-topbar-row
+ .docs-topbar-brand
+ %a.docs-topbar-logo{href: "https://www.cloudmailin.com"}
+ = image_tag('cloudmailin-lockup-nav.svg', alt: 'CloudMailin logo, incoming email for your web app', style: 'height:36px;width:auto')
+ %span.docs-badge
+ Docs
+
+ .docs-topbar-tools
+ #searchbox
+
+ %a.docs-nav-external-link{href: "https://www.cloudmailin.com"}
+ cloudmailin.com
+ = icon(:external_link, 'docs-nav-external-icon')
+
+ -# Mobile "Contents" toggle — same sidebar partial as the desktop aside,
+ just presented as a collapsible drawer above the article on narrow
+ screens (no JS beyond the browser's native ).
+ %details.docs-mobile-toc
+ %summary.docs-mobile-toc-summary
+ %span.docs-mobile-toc-label
+ = icon(:bars_staggered, 'docs-mobile-toc-icon')
+ Contents
+ = icon(:chevron_down, 'docs-mobile-toc-chevron')
+ .docs-mobile-toc-body
+ = render('sidebar')
+
+ -# Docs frame: nav rail docked to the viewport's left edge (tinted panel,
+ full column height), article centered in the remaining width — the
+ established docs-site layout (ReadTheDocs, MDN, Stripe).
+ #docs-content
+ %aside.docs-desktop-aside
+ .docs-sidebar-sticky
+ = render('sidebar')
+
+ %main.docs-main
-if ENV['NANOC_ENV'] == 'development'
.info Development Mode
- if description.nil?
@@ -62,9 +92,8 @@
- else
.info
%p= description
- .documentation_content{id: identifier }
- %a.hidden-md.hidden-lg{href: '#sidebar'} Jump To Navigation
+ .docs-article{id: identifier}
- if redirect_to
.warning
%p Redirecting to #{link_to redirect_to, redirect_to}.
@@ -80,15 +109,54 @@
#comments
-# = render 'comments' unless disable_comments?
- = render('sidebar')
+ %footer.docs-footer
+ .container.docs-footer-body
+ .docs-footer-grid
+ .docs-footer-about
+ %a.docs-footer-brand{href: "https://www.cloudmailin.com"}
+ CloudMailin
+ %p.docs-footer-tagline
+ Developer-friendly email API for inbound and outbound email processing.
+ %div
+ %h3.docs-footer-heading Documentation
+ %ul.docs-footer-links
+ %li= link_to 'Getting Started', '/getting_started/', class: 'docs-footer-link'
+ %li= link_to 'HTTP POST Formats', '/http_post_formats/', class: 'docs-footer-link'
+ %li= link_to 'Receiving Email', '/receiving_email/http_status_codes/', class: 'docs-footer-link'
+ %li= link_to 'Outbound (Sending)', '/outbound/', class: 'docs-footer-link'
+ %div
+ %h3.docs-footer-heading CloudMailin
+ %ul.docs-footer-links
+ %li= link_to 'cloudmailin.com', 'https://www.cloudmailin.com', class: 'docs-footer-link'
+ %li= link_to 'Plans and Pricing', 'https://www.cloudmailin.com/plans-and-pricing', class: 'docs-footer-link'
+ %li= link_to 'Contact Support', 'https://www.cloudmailin.com/contact_us', class: 'docs-footer-link'
+ .docs-footer-divider
+ .container.docs-footer-legal
+ %p.docs-footer-copyright
+ © #{Time.now.year} CloudMailin.com
- %footer#footer
- #details
- © cloudmailin.com
+ -# Sidebar "Expand all / Collapse all" toggle. The sidebar partial renders
+ in both the desktop aside and the mobile drawer, so there can be two
+ toggles and two sets of — operate on all of them.
+ :plain
+
- -#
- / Thanks to https://github.com/tholman/github-corners
-
- unless ENV['NANOC_ENV'] == 'development'
= render 'clickcease'
= render 'analytics'
diff --git a/layouts/sidebar.haml b/layouts/sidebar.haml
index 4e0c92f..be45dcd 100644
--- a/layouts/sidebar.haml
+++ b/layouts/sidebar.haml
@@ -1,81 +1,96 @@
-#sidebar
- %h2.hidden-md.hidden-lg Navigation
- %ul
- %li= link_to 'Home', '/'
- %li
- Getting Started (Receiving)
- %ul
- %li= link_to 'Introduction', '/getting_started/'
- - for item in items_for_section('getting_started/receiving_email')
- %li= link_to title(item), item
- %li= link_to 'FAQs', '/getting_started/faqs/'
- %li= link_to 'Getting Started Sending Email', '/outbound/'
- %li
- HTTP POST Formats
- %ul
- %li= link_to 'Introduction to POST Formats', '/http_post_formats/'
- %li= link_to 'JSON (Normalized)', '/http_post_formats/json_normalized/'
- %li= link_to 'Multipart (Normalized)', '/http_post_formats/multipart_normalized/'
- %li= link_to 'Raw Message Format', '/http_post_formats/raw/'
- %li= link_to 'JSON Format', '/http_post_formats/json/', class: 'deprecated'
- %li= link_to 'Multipart Format', '/http_post_formats/multipart/', class: 'deprecated'
- %li= link_to 'Original Format', '/http_post_formats/original/', class: 'deprecated'
- -# %li= link_to 'Examples', '/receiving_email/examples/'
- %li
- Receiving Email
- %ul
- %li= link_to 'HTTP Status Codes', '/receiving_email/http_status_codes/'
- %li= link_to 'Using your own Domain', '/receiving_email/forwarding_and_custom_domains/'
- %li= link_to 'Store Attachments in Cloud Storage', '/receiving_email/store-email-attachments-in-s3-azure-google-storage/'
- %li= link_to 'Store Full Email in Cloud Storage', '/receiving_email/store-full-email-in-aws-cloud-storage/'
- %li= link_to 'Reply Parsing', '/features/extracting_replies_from_email/'
- %li
- = link_to 'Callbacks', '/features/callbacks/'
- %ul
- %li= link_to 'Authorization Callbacks', '/features/callbacks/#authorization-callbacks'
- %li= link_to 'Error Callbacks', '/features/callbacks/#error-callbacks'
- %li= link_to 'SPF (Sender Policy Framework)', '/features/spf/'
- %li= link_to 'DKIM Verification', '/features/dkim/'
- %li= link_to 'DMARC Verification', '/features/dmarc-verification/'
- %li= link_to 'Delivery Status API', '/features/delivery_status_api/'
- %li= link_to 'Securing HTTP Posts', '/receiving_email/securing_your_email_url_target/'
- %li= link_to 'IMAP Polling (Legacy Systems)', '/features/imap_collectors/'
- %li= link_to 'SPAM Filtering', '/features/spam_filtering/'
- %li= link_to 'Virus Scanning', '/features/virus_scanning/'
- %li= link_to 'IP Address List', '/receiving_email/ip_addresses/'
- %li
- = link_to 'Examples', '/receiving_email/examples/'
- - if in_section?('receiving_email/examples', include_self: true)
- %ul
- - items_for_section('receiving_email/examples').each do |item|
- %li= link_to title(item), item
- %li
- Local Development and Testing
- %ul
- %li= link_to 'Local Development / Debugger', '/receiving_email/localhost_debugger/'
- %li= link_to 'Test Driven Development', '/receiving_email/test_driven_development/'
+.sidebar-nav
+ .sidebar-top-row
+ = link_to 'Home', '/', class: sidebar_link_class('/')
+ %button.sidebar-expand-toggle{type: 'button', data: { sidebar_toggle: true }}= sidebar_expand_all? ? 'Collapse all' : 'Expand all'
- %li
- Outbound (Sending)
- %ul
- %li= link_to 'Introduction', '/outbound/'
- %li= link_to 'Getting Started', '/outbound/getting_started/'
- %li= link_to 'DKIM Signing', '/outbound/dkim/'
- %li= link_to 'DMARC Setup', '/outbound/dmarc/'
- %li= link_to 'Priorities and Tagging', '/outbound/priorities_and_tags/'
- %li= link_to 'Email Layouts', '/outbound/email-layouts/'
- %li= link_to 'Email Health', '/outbound/email-health/'
- %li= link_to 'Sending Email via API', '/outbound/sending_email_via_json_api/'
- %li
- = link_to 'Sending Email with SMTP', '/outbound/sending_email_with_smtp/'
- %ul
- - if in_section?('outbound/', include_self: true)
- - items_for_section('outbound/examples').each do |item|
- %li= link_to title(item), item
- - else
- %li= link_to 'Examples', '/outbound/sending_email_with_smtp/#examples'
+ %details.sidebar-group{open: sidebar_section_open?('getting_started')}
+ %summary.sidebar-group-summary
+ %span Getting Started (Receiving)
+ = icon(:chevron_right, 'sidebar-group-chevron')
+ %ul.sidebar-items
+ %li= link_to 'Introduction', '/getting_started/', class: sidebar_link_class('/getting_started/')
+ - for item in items_for_section('getting_started/receiving_email')
+ %li= link_to title(item), item, class: sidebar_link_class(item)
+ %li= link_to 'FAQs', '/getting_started/faqs/', class: sidebar_link_class('/getting_started/faqs/')
+ %li= link_to 'Getting Started Sending Email', '/outbound/', class: sidebar_link_class('/outbound/')
- %li
- Guides
- %ul
- %li= link_to 'Verify DKIM is Working', '/guides/verify-dkim/'
+ %details.sidebar-group{open: sidebar_section_open?('http_post_formats')}
+ %summary.sidebar-group-summary
+ %span HTTP POST Formats
+ = icon(:chevron_right, 'sidebar-group-chevron')
+ %ul.sidebar-items
+ %li= link_to 'Introduction to POST Formats', '/http_post_formats/', class: sidebar_link_class('/http_post_formats/')
+ %li= link_to 'JSON (Normalized)', '/http_post_formats/json_normalized/', class: sidebar_link_class('/http_post_formats/json_normalized/')
+ %li= link_to 'Multipart (Normalized)', '/http_post_formats/multipart_normalized/', class: sidebar_link_class('/http_post_formats/multipart_normalized/')
+ %li= link_to 'Raw Message Format', '/http_post_formats/raw/', class: sidebar_link_class('/http_post_formats/raw/')
+ %li= link_to 'JSON Format', '/http_post_formats/json/', class: sidebar_link_class('/http_post_formats/json/', 'deprecated')
+ %li= link_to 'Multipart Format', '/http_post_formats/multipart/', class: sidebar_link_class('/http_post_formats/multipart/', 'deprecated')
+ %li= link_to 'Original Format', '/http_post_formats/original/', class: sidebar_link_class('/http_post_formats/original/', 'deprecated')
+
+ %details.sidebar-group{open: sidebar_section_open?('receiving_email')}
+ %summary.sidebar-group-summary
+ %span Receiving Email
+ = icon(:chevron_right, 'sidebar-group-chevron')
+ %ul.sidebar-items
+ %li= link_to 'HTTP Status Codes', '/receiving_email/http_status_codes/', class: sidebar_link_class('/receiving_email/http_status_codes/')
+ %li= link_to 'Using your own Domain', '/receiving_email/forwarding_and_custom_domains/', class: sidebar_link_class('/receiving_email/forwarding_and_custom_domains/')
+ %li= link_to 'Store Attachments in Cloud Storage', '/receiving_email/store-email-attachments-in-s3-azure-google-storage/', class: sidebar_link_class('/receiving_email/store-email-attachments-in-s3-azure-google-storage/')
+ %li= link_to 'Store Full Email in Cloud Storage', '/receiving_email/store-full-email-in-aws-cloud-storage/', class: sidebar_link_class('/receiving_email/store-full-email-in-aws-cloud-storage/')
+ %li= link_to 'Reply Parsing', '/features/extracting_replies_from_email/', class: sidebar_link_class('/features/extracting_replies_from_email/')
+ %li
+ = link_to 'Callbacks', '/features/callbacks/', class: sidebar_link_class('/features/callbacks/')
+ %ul.sidebar-items.sidebar-items--nested
+ %li= link_to 'Authorization Callbacks', '/features/callbacks/#authorization-callbacks', class: sidebar_link_class('/features/callbacks/#authorization-callbacks')
+ %li= link_to 'Error Callbacks', '/features/callbacks/#error-callbacks', class: sidebar_link_class('/features/callbacks/#error-callbacks')
+ %li= link_to 'SPF (Sender Policy Framework)', '/features/spf/', class: sidebar_link_class('/features/spf/')
+ %li= link_to 'DKIM Verification', '/features/dkim/', class: sidebar_link_class('/features/dkim/')
+ %li= link_to 'DMARC Verification', '/features/dmarc-verification/', class: sidebar_link_class('/features/dmarc-verification/')
+ %li= link_to 'Delivery Status API', '/features/delivery_status_api/', class: sidebar_link_class('/features/delivery_status_api/')
+ %li= link_to 'Securing HTTP Posts', '/receiving_email/securing_your_email_url_target/', class: sidebar_link_class('/receiving_email/securing_your_email_url_target/')
+ %li= link_to 'IMAP Polling (Legacy Systems)', '/features/imap_collectors/', class: sidebar_link_class('/features/imap_collectors/')
+ %li= link_to 'SPAM Filtering', '/features/spam_filtering/', class: sidebar_link_class('/features/spam_filtering/')
+ %li= link_to 'Virus Scanning', '/features/virus_scanning/', class: sidebar_link_class('/features/virus_scanning/')
+ %li= link_to 'IP Address List', '/receiving_email/ip_addresses/', class: sidebar_link_class('/receiving_email/ip_addresses/')
+ %li
+ = link_to 'Examples', '/receiving_email/examples/', class: sidebar_link_class('/receiving_email/examples/')
+ - if in_section?('receiving_email/examples', include_self: true)
+ %ul.sidebar-items.sidebar-items--nested
+ - items_for_section('receiving_email/examples').each do |item|
+ %li= link_to title(item), item, class: sidebar_link_class(item)
+
+ %details.sidebar-group{open: sidebar_section_open?('local_dev')}
+ %summary.sidebar-group-summary
+ %span Local Development and Testing
+ = icon(:chevron_right, 'sidebar-group-chevron')
+ %ul.sidebar-items
+ %li= link_to 'Local Development / Debugger', '/receiving_email/localhost_debugger/', class: sidebar_link_class('/receiving_email/localhost_debugger/')
+ %li= link_to 'Test Driven Development', '/receiving_email/test_driven_development/', class: sidebar_link_class('/receiving_email/test_driven_development/')
+
+ %details.sidebar-group{open: sidebar_section_open?('outbound')}
+ %summary.sidebar-group-summary
+ %span Outbound (Sending)
+ = icon(:chevron_right, 'sidebar-group-chevron')
+ %ul.sidebar-items
+ %li= link_to 'Introduction', '/outbound/', class: sidebar_link_class('/outbound/')
+ %li= link_to 'Getting Started', '/outbound/getting_started/', class: sidebar_link_class('/outbound/getting_started/')
+ %li= link_to 'DKIM Signing', '/outbound/dkim/', class: sidebar_link_class('/outbound/dkim/')
+ %li= link_to 'DMARC Setup', '/outbound/dmarc/', class: sidebar_link_class('/outbound/dmarc/')
+ %li= link_to 'Priorities and Tagging', '/outbound/priorities_and_tags/', class: sidebar_link_class('/outbound/priorities_and_tags/')
+ %li= link_to 'Email Layouts', '/outbound/email-layouts/', class: sidebar_link_class('/outbound/email-layouts/')
+ %li= link_to 'Email Health', '/outbound/email-health/', class: sidebar_link_class('/outbound/email-health/')
+ %li= link_to 'Sending Email via API', '/outbound/sending_email_via_json_api/', class: sidebar_link_class('/outbound/sending_email_via_json_api/')
+ %li
+ = link_to 'Sending Email with SMTP', '/outbound/sending_email_with_smtp/', class: sidebar_link_class('/outbound/sending_email_with_smtp/')
+ %ul.sidebar-items.sidebar-items--nested
+ - if in_section?('outbound/', include_self: true)
+ - items_for_section('outbound/examples').each do |item|
+ %li= link_to title(item), item, class: sidebar_link_class(item)
+ - else
+ %li= link_to 'Examples', '/outbound/sending_email_with_smtp/#examples', class: sidebar_link_class('/outbound/sending_email_with_smtp/#examples')
+
+ %details.sidebar-group{open: sidebar_section_open?('guides')}
+ %summary.sidebar-group-summary
+ %span Guides
+ = icon(:chevron_right, 'sidebar-group-chevron')
+ %ul.sidebar-items
+ %li= link_to 'Verify DKIM is Working', '/guides/verify-dkim/', class: sidebar_link_class('/guides/verify-dkim/')
diff --git a/lib/common_marker_filter.rb b/lib/common_marker_filter.rb
index 8f20592..6dfb32c 100644
--- a/lib/common_marker_filter.rb
+++ b/lib/common_marker_filter.rb
@@ -6,8 +6,6 @@ class CommonMarkerFilter < Nanoc::Filter
requires 'commonmarker'
class CustomHtmlRenderer < ::CommonMarker::HtmlRenderer
- STYLE = 'pastie'.freeze
-
def header(node)
block do
id = node.first.string_content.downcase.gsub(/[^\w]/, '-')
@@ -35,7 +33,10 @@ def code_block(node)
# lang = CGI.escapeHTML(node.fence_info)
lang = node.fence_info.split(/\s+/)[0]
lexer = ::Rouge::Lexer.find_fancy(node.fence_info) || ::Rouge::Lexers::PlainText.new
- formatter = ::Rouge::Formatters::HTMLInline.new(STYLE)
+ # Class-based (not inline-styled) output so the docs Tailwind CSS
+ # can recolor tokens for the dark terminal-motif code blocks. See
+ # tailwind/input.css for the token color rules.
+ formatter = ::Rouge::Formatters::HTML.new
out("")
out(formatter.format(lexer.lex(source)))
diff --git a/lib/helpers/icon_helpers.rb b/lib/helpers/icon_helpers.rb
new file mode 100644
index 0000000..e878dc6
--- /dev/null
+++ b/lib/helpers/icon_helpers.rb
@@ -0,0 +1,17 @@
+# Inline SVG icons — the four small glyphs the layout needs, replacing a
+# render-blocking Font Awesome CDN stylesheet (the full library plus a
+# webfont, for four icons). 1em sizing + currentColor keep the existing
+# font-size/color CSS working unchanged.
+module IconHelpers
+ ICONS = {
+ chevron_right: ' ',
+ chevron_down: ' ',
+ external_link: ' ',
+ bars_staggered: ' '
+ }.freeze
+
+ def icon(name, css_class = nil)
+ classes = ['docs-icon', css_class].compact.join(' ')
+ %()
+ end
+end
diff --git a/lib/helpers/section_helpers.rb b/lib/helpers/section_helpers.rb
index 6c95a48..0629d33 100644
--- a/lib/helpers/section_helpers.rb
+++ b/lib/helpers/section_helpers.rb
@@ -1,4 +1,66 @@
module SectionHelpers
+ # Plain CSS classes (defined in tailwind/input.css), not Tailwind utility
+ # strings — sidebar links are bare tags that Bootstrap also styles
+ # (see the cascade-layers note at the top of tailwind/input.css), so a
+ # real class beats it on specificity where a utility class would lose.
+ SIDEBAR_LINK_CLASS = 'sidebar-link'.freeze
+ SIDEBAR_LINK_ACTIVE_CLASS = 'sidebar-link sidebar-link--active'.freeze
+
+ # True when `target` (a path string like '/foo/', or a compiled item)
+ # points at the page currently being rendered — used to pill-highlight
+ # the active link in layouts/sidebar.haml.
+ def current_target?(target)
+ target_path = target.respond_to?(:path) ? target.path.to_s : target.to_s
+ target_path.split('#').first.to_s.chomp('/') == @item.path.to_s.chomp('/')
+ end
+
+ # CSS class for a sidebar link, switching to the pill-highlighted
+ # "active" treatment when `target` is the current page. `extra` merges
+ # in any additional classes (e.g. 'deprecated').
+ def sidebar_link_class(target, extra = nil)
+ base = current_target?(target) ? SIDEBAR_LINK_ACTIVE_CLASS : SIDEBAR_LINK_CLASS
+ [base, extra].compact.join(' ')
+ end
+
+ # The two "Local Development and Testing" pages live under /receiving_email/
+ # but belong to their own sidebar section — list them explicitly so the
+ # resolver below can claim them before the broader /receiving_email/ prefix.
+ LOCAL_DEV_PAGES = %w[
+ /receiving_email/localhost_debugger/
+ /receiving_email/test_driven_development/
+ ].freeze
+
+ # Which single sidebar section the current page belongs to. The sidebar
+ # groups don't map 1:1 to URL prefixes — "Receiving Email" also lists
+ # /features/* pages, and "Local Development" is a subset of /receiving_email/*
+ # — so order matters here: the most specific rules must come first, and each
+ # page resolves to exactly ONE section. Used by layouts/sidebar.haml to open
+ # that section's by default while the others stay collapsed.
+ def active_sidebar_section
+ path = @item.path.to_s
+ return 'local_dev' if LOCAL_DEV_PAGES.any? { |p| path.start_with?(p) }
+ return 'outbound' if path.start_with?('/outbound/')
+ return 'guides' if path.start_with?('/guides/')
+ return 'http_post_formats' if path.start_with?('/http_post_formats/')
+ return 'getting_started' if path.start_with?('/getting_started/')
+ return 'receiving_email' if path.start_with?('/receiving_email/') || path.start_with?('/features/')
+
+ 'getting_started' # home page and anything else → open the first section
+ end
+
+ # The home page is the "what's in the docs?" page, so it opens every
+ # sidebar section (as the old flat sidebar did); everywhere else only
+ # the current section starts open.
+ def sidebar_expand_all?
+ @item.path.to_s.chomp('/').empty?
+ end
+
+ # True when the given sidebar section key should render its
+ # `open` — the active section, or all of them on the home page.
+ def sidebar_section_open?(key)
+ sidebar_expand_all? || active_sidebar_section == key
+ end
+
def in_section?(section, item: @item, include_self: false)
# paths = item.identifier.to_s.split('/')
# paths.any? { |path| path == section_name }
diff --git a/lib/helpers_.rb b/lib/helpers_.rb
index a108917..d38cca9 100644
--- a/lib/helpers_.rb
+++ b/lib/helpers_.rb
@@ -9,6 +9,7 @@
use_helper ItemHelpers
use_helper SectionHelpers
use_helper OpenapiHelpers
+use_helper IconHelpers
def image_tag(path, options)
full_path = image_path(path)
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..3f73ef6
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1064 @@
+{
+ "name": "docs-cloudmailin-tailwind",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "docs-cloudmailin-tailwind",
+ "version": "0.0.0",
+ "devDependencies": {
+ "@tailwindcss/cli": "^4.1.11",
+ "tailwindcss": "^4.1.11"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
+ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+ "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+ "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+ "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+ "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+ "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+ "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+ "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+ "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+ "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+ "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+ "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+ "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/cli": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.2.tgz",
+ "integrity": "sha512-Fzt+HrIZHDlkRYKdLMBeufaroaPvwCBG70sMLdmurdeadNMO/LxbmT8Sbb+P83ep0iAlAImettb7Y+rO+37rXw==",
+ "dev": true,
+ "dependencies": {
+ "@parcel/watcher": "2.5.1",
+ "@tailwindcss/node": "4.3.2",
+ "@tailwindcss/oxide": "4.3.2",
+ "enhanced-resolve": "5.21.6",
+ "mri": "^1.2.0",
+ "picocolors": "^1.1.1",
+ "tailwindcss": "4.3.2"
+ },
+ "bin": {
+ "tailwindcss": "dist/index.mjs"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
+ "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz",
+ "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==",
+ "dev": true,
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-x64": "4.3.2",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.2",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.2",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.2",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz",
+ "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz",
+ "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz",
+ "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz",
+ "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz",
+ "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz",
+ "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz",
+ "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz",
+ "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz",
+ "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz",
+ "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",
+ "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz",
+ "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "dev": true,
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss/node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
+ "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==",
+ "dev": true
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c09ab99
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "docs-cloudmailin-tailwind",
+ "private": true,
+ "version": "0.0.0",
+ "description": "Tailwind CSS build for docs.cloudmailin.com",
+ "scripts": {
+ "build:css": "tailwindcss -i ./tailwind/input.css -o ./content/assets/stylesheets/tailwind_built.css --minify",
+ "watch:css": "tailwindcss -i ./tailwind/input.css -o ./content/assets/stylesheets/tailwind_built.css --watch=always"
+ },
+ "devDependencies": {
+ "@tailwindcss/cli": "^4.1.11",
+ "tailwindcss": "^4.1.11"
+ }
+}
diff --git a/tailwind/input.css b/tailwind/input.css
new file mode 100644
index 0000000..d0705c8
--- /dev/null
+++ b/tailwind/input.css
@@ -0,0 +1,1052 @@
+/*
+ * Tailwind CSS build for docs.cloudmailin.com
+ *
+ * Compiled by the Tailwind CLI (Node, no Rails/Sprockets involved) into
+ * content/assets/stylesheets/tailwind_built.css, which nanoc then copies
+ * straight through to output/ like any other static content CSS file
+ * (see Rules — the `css` filetype case just runs `:erb` and routes it).
+ *
+ * This is the PRIMARY stylesheet for the site. Bootstrap 3 was removed in
+ * the Tailwind redesign; Tailwind v4's Preflight (below) is now the CSS
+ * reset. content/assets/stylesheets/docs_new.scss still loads before this
+ * one but is now Bootstrap-free — it holds only a few unlayered rules
+ * (.container, the dev-mode .info/.warning callouts, a.deprecated, and the
+ * Algolia autocomplete-theme :root vars). See that file's header for why.
+ *
+ * Why so much hand-written CSS instead of Tailwind utility classes: nearly
+ * all of this site is markdown compiled to HTML with ZERO classes on the
+ * generated tags (h1-h6, p, a, ul, table, pre…), so utilities can't be
+ * applied inline — the styling has to hang off element selectors scoped
+ * under `.docs-article`. This is the intended pattern, not a workaround.
+ * The HAML layouts (nav, footer, sidebar) follow the same approach with
+ * semantic classes (docs-* / sidebar-*) defined below rather than utility
+ * strings in the markup — the docs site favours neat HTML over inline
+ * utility clusters.
+ *
+ * One cascade note worth keeping in mind: `@import "tailwindcss"` wraps
+ * Tailwind's utilities in `@layer utilities`, and per the CSS Cascade
+ * Layers spec an UNLAYERED rule beats any layered rule for a contested
+ * property regardless of specificity. The handful of unlayered rules left
+ * in docs_new.scss (e.g. `.container`) therefore win over a Tailwind
+ * utility of the same name — which is deliberate there — so keep any new
+ * rules in docs_new.scss uniquely named to avoid clobbering utilities used
+ * in the templates. Rules in THIS file live in the normal (unlayered)
+ * cascade too, so element selectors here beat bare-tag Preflight defaults
+ * on specificity as expected.
+ */
+
+@import "tailwindcss";
+
+/* Explicit source scanning — this is a Ruby/HAML/Markdown project, not a
+ JS one, so make sure Tailwind's Oxide scanner walks the right trees for
+ utility classes used in templates and content. */
+@source "../layouts";
+@source "../content";
+@source "../lib";
+
+@theme {
+ /* Brand blue scale — matches app/assets/tailwind/application.css in the
+ main cloudmailin repo exactly (cm-blue-500 #4d7aba is primary). */
+ --color-cm-blue-50: #f0f4fa;
+ --color-cm-blue-100: #dce5f3;
+ --color-cm-blue-200: #b9cce7;
+ --color-cm-blue-300: #8eadd6;
+ --color-cm-blue-400: #638ac3;
+ --color-cm-blue-500: #4d7aba;
+ --color-cm-blue-600: #4470af;
+ --color-cm-blue-700: #3a5f9a;
+ --color-cm-blue-800: #2f4d7a;
+ --color-cm-blue-900: #243c5f;
+
+ --color-cm-warning: #f0ad4e;
+
+ /* Terminal code-block motif */
+ --color-cm-terminal-bg: #3d629a;
+ --color-cm-terminal-border: #8ab0d8;
+ --color-cm-terminal-dots: #345285;
+ --color-cm-code-bg: rgba(77, 122, 186, 0.02);
+
+ --shadow-nav: 0px 1px 0px #638ac3;
+
+ --font-family-display: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ --font-family-mono: "DM Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ --font-family-logo: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+/* ==========================================================================
+ Base typography — every markdown page renders through the same shared
+ layout with zero classes on its generated tags, so set the brand type
+ here rather than editing every content file.
+ ========================================================================== */
+
+html {
+ font-family: var(--font-family-display);
+ /* Bootstrap 3's scaffolding sets `html { font-size: 10px }` (its own
+ rem-basis convention). Nothing above contested the `font-size`
+ property specifically, so that 10px value won by default and every
+ rem-sized rule in this file — headings, spacing, the terminal code
+ blocks, tables — was silently rendering at 62.5% of its intended
+ size. Restore the standard 16px root so 1rem means 1rem again. */
+ font-size: 16px;
+}
+
+body#documentation {
+ font-family: var(--font-family-display);
+ background-color: #fff;
+}
+
+code, kbd, pre, samp {
+ font-family: var(--font-family-mono);
+}
+
+/* ==========================================================================
+ Nav bar & footer links/headings — real CSS classes rather than Tailwind
+ utilities, since these are / tags Bootstrap also styles (see the
+ cascade-layers note at the top of this file).
+ ========================================================================== */
+
+/* Bootstrap 3's responsive-utilities partial ALSO defines an unlayered
+ `.hidden { display: none }` class (same name as Tailwind's own `hidden`
+ utility) — it always wins over Tailwind's layered `hidden`/`lg:block`
+ combo regardless of viewport, so that pattern can't be used here for
+ "hidden on mobile, visible at a breakpoint". These custom, uniquely-
+ named, unlayered pairs do the same job without the name collision. */
+.docs-badge { display: none; }
+.docs-nav-external-link { display: none; }
+.docs-desktop-aside { display: none; }
+
+@media (width >= 40rem) {
+ .docs-badge { display: inline-flex; }
+}
+
+@media (width >= 64rem) {
+ .docs-nav-external-link { display: inline-flex; }
+ .docs-desktop-aside { display: block; }
+}
+
+/* Sticky top bar — brand + search + external link on the cm-blue surface. */
+.docs-topbar {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+}
+
+.docs-topbar-nav {
+ background-color: var(--color-cm-blue-500);
+ border-bottom: 1px solid var(--color-cm-blue-600);
+ box-shadow: var(--shadow-nav);
+}
+
+.docs-topbar-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ width: 100%;
+ /* 1.5rem inline padding matches the rail's padding-left so the logo and
+ the sidebar content share a left edge. */
+ padding: 0.5rem 1.5rem;
+}
+
+.docs-topbar-brand {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ flex-shrink: 0;
+}
+
+.docs-topbar-logo {
+ display: flex;
+ align-items: center;
+}
+
+.docs-topbar-logo img { display: block; }
+
+/* "Docs" pill next to the logo. display (hidden on mobile) is handled by
+ the responsive visibility pairs above. Deliberately var(--font-mono)
+ (Tailwind's ui-monospace default stack, what the old `font-mono` utility
+ resolved to) and not the DM Mono brand var — keeps the rendered output
+ identical to the utility version. */
+.docs-badge {
+ align-items: center;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 0.375rem;
+ padding: 0.125rem 0.5rem;
+ color: rgba(255, 255, 255, 0.8);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+/* Right-hand cluster: search box + cloudmailin.com link. */
+.docs-topbar-tools {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 0.75rem;
+ flex: 1;
+}
+
+/* The docs frame: nav rail docked to the viewport's left edge as a tinted,
+ bordered panel spanning the full column height, with the article centered
+ in the remaining width (the established docs layout — ReadTheDocs, MDN,
+ Stripe). The rail keeps the page frame even when content is short. */
+#docs-content {
+ min-height: calc(100vh - 3.5rem);
+}
+
+/* Side-by-side rail + article from the lg breakpoint up; stacked below. */
+@media (width >= 64rem) {
+ #docs-content {
+ display: flex;
+ align-items: stretch;
+ }
+}
+
+.docs-desktop-aside {
+ /* 18rem up to ~1500px viewports, then grows with the screen to a 24rem
+ cap (~2560px) so the rail doesn't look skinny on very wide displays. */
+ width: clamp(18rem, 12vw + 6rem, 24rem);
+ flex-shrink: 0;
+ /* White rail — the gray-200 hairline border alone defines the panel.
+ padding-top matches .docs-main's so both columns start level. */
+ background-color: #fff;
+ border-right: 1px solid var(--color-gray-200);
+ padding: 2.75rem 1.25rem 2.5rem 1.5rem;
+}
+
+.docs-main {
+ /* min-width: 0 lets the article column shrink below its content's
+ intrinsic width (wide code blocks) instead of overflowing the flex row. */
+ min-width: 0;
+ flex: 1;
+ padding: 1.5rem 1rem 4rem;
+}
+
+@media (width >= 64rem) {
+ .docs-main { padding: 2.75rem 3rem 5rem; }
+}
+
+.docs-main .docs-article,
+.docs-main .info,
+.docs-main .warning {
+ /* ~95 chars of body text; docs pages are code-fence-heavy so err wider
+ than pure-prose measure. */
+ max-width: 48rem;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* Two sidebar modes. Collapsed (one section open) is reading mode: the nav
+ fits the viewport, so pin it alongside the article. Expanded (two or more
+ sections open — the home page default, or after "Expand all") is browsing
+ mode: let the nav flow at full height with the page instead of adding a
+ second scrollbar. :has() picks the mode from the DOM state itself, so the
+ same rule covers server-rendered defaults and the client-side toggle. */
+.docs-sidebar-sticky {
+ position: sticky;
+ top: 5.5rem;
+ max-height: calc(100vh - 7rem);
+ overflow-y: auto;
+}
+
+.docs-sidebar-sticky:has(.sidebar-group[open] ~ .sidebar-group[open]) {
+ position: static;
+ max-height: none;
+ overflow-y: visible;
+}
+
+.docs-nav-external-link {
+ align-items: center;
+ gap: 0.375rem;
+ flex-shrink: 0;
+ padding-left: 0.75rem;
+ border-left: 1px solid rgba(255, 255, 255, 0.2);
+ color: rgba(255, 255, 255, 0.7);
+ text-decoration: none;
+ font-size: 0.875rem;
+ line-height: calc(1.25 / 0.875);
+}
+
+.docs-nav-external-link:hover { color: rgba(255, 255, 255, 0.9); }
+
+.docs-nav-external-icon { font-size: 10px; }
+
+/* Footer frame — the .container width/centering rule itself lives in
+ docs_new.scss; these classes ride alongside it in the markup. */
+.docs-footer {
+ background-color: var(--color-cm-blue-500);
+}
+
+.docs-footer-body {
+ padding-top: 3rem;
+ padding-bottom: 3rem;
+}
+
+.docs-footer-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 2rem;
+}
+
+@media (width >= 40rem) {
+ .docs-footer-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+}
+
+/* Brand + tagline column: full row on mobile, one column from sm up. */
+.docs-footer-about {
+ grid-column: span 2 / span 2;
+}
+
+@media (width >= 40rem) {
+ .docs-footer-about { grid-column: span 1 / span 1; }
+}
+
+.docs-footer-brand {
+ display: inline-block;
+ margin-bottom: 0.75rem;
+ color: #fff;
+ text-decoration: none;
+ font-family: var(--font-family-logo);
+ font-size: 1.25rem;
+ line-height: calc(1.75 / 1.25);
+ font-weight: 700;
+ letter-spacing: -0.025em;
+}
+
+.docs-footer-tagline {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 0.875rem;
+ line-height: 1.625;
+}
+
+.docs-footer-heading {
+ font-family: var(--font-family-mono);
+ font-size: 0.75rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: rgba(255, 255, 255, 0.5);
+ margin: 0 0 0.75rem;
+}
+
+.docs-footer-link {
+ color: rgba(255, 255, 255, 0.75);
+ text-decoration: none;
+ font-size: 0.875rem;
+}
+
+.docs-footer-link:hover { color: #fff; }
+
+.docs-footer-links {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.docs-footer-links > li + li {
+ margin-top: 0.5rem;
+}
+
+/* Copyright strip under the link columns. */
+.docs-footer-divider {
+ border-top: 1px solid rgba(255, 255, 255, 0.15);
+}
+
+.docs-footer-legal {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+
+.docs-footer-copyright {
+ margin: 0;
+ color: rgba(255, 255, 255, 0.4);
+ font-size: 0.75rem;
+ line-height: calc(1 / 0.75);
+ text-align: center;
+}
+
+/* ==========================================================================
+ Sidebar links — see layout/sidebar.haml + lib/helpers/section_helpers.rb.
+ Plain classes (not Tailwind utility strings) for the same cascade-layers
+ reason as above.
+ ========================================================================== */
+
+/* Root of the sidebar partial (rendered in both the desktop aside and the
+ mobile drawer): base type size + 1rem rhythm between the section groups. */
+.sidebar-nav {
+ font-size: 0.9rem;
+}
+
+.sidebar-nav > * + * {
+ margin-top: 1rem;
+}
+
+/* Home link + "Expand all" toggle on one line. */
+.sidebar-top-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+/* Link lists inside each group; --nested is the indented
+ sub-list under a parent link (Callbacks, Examples, SMTP). */
+.sidebar-items {
+ list-style: none;
+ margin: 0.5rem 0 0;
+ padding: 0;
+}
+
+.sidebar-items > li + li {
+ margin-top: 0.125rem;
+}
+
+.sidebar-items--nested {
+ margin-top: 0.125rem;
+ padding-left: 0.75rem;
+}
+
+.sidebar-link {
+ display: block;
+ border-radius: 0.375rem;
+ padding: 0.375rem 0.75rem;
+ color: #4b5563;
+ text-decoration: none;
+ font-size: inherit;
+}
+
+.sidebar-link:hover {
+ background-color: #f9fafb;
+ color: #111827;
+}
+
+.sidebar-link--active {
+ background-color: var(--color-cm-blue-50);
+ color: var(--color-cm-blue-700);
+ font-weight: 500;
+}
+
+.sidebar-link--active:hover {
+ background-color: var(--color-cm-blue-50);
+ color: var(--color-cm-blue-700);
+}
+
+/* --------------------------------------------------------------------------
+ Collapsible sidebar sections (native /). Only the active
+ section is `open` by default (see active_sidebar_section in section_helpers)
+ so the nav is short enough to fit without an internal scroll; the "Expand
+ all" toggle (data-sidebar-toggle, handled by the inline script in
+ application.haml) opens every group for customers who want to scan them all.
+ -------------------------------------------------------------------------- */
+
+/* The IS the section header — remove the native disclosure triangle
+ and restyle it as the old mono/uppercase label, now clickable. */
+.sidebar-group-summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+ cursor: pointer;
+ list-style: none;
+ user-select: none;
+ padding: 0.2rem 0;
+ color: #9ca3af;
+ font-family: var(--font-family-mono);
+ font-size: 0.75rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+}
+
+.sidebar-group-summary::-webkit-details-marker { display: none; }
+.sidebar-group-summary::marker { content: ""; }
+
+.sidebar-group-summary:hover { color: #6b7280; }
+
+/* Inline SVG icons (lib/helpers/icon_helpers.rb): 1em box sized by the
+ surrounding font-size, aligned like an icon-font glyph. */
+.docs-icon {
+ display: inline-block;
+ vertical-align: -0.125em;
+}
+
+/* Chevron points right when collapsed, rotates down when the group is open. */
+.sidebar-group-chevron {
+ font-size: 0.625rem;
+ color: #cbd5e1;
+ transition: transform 0.15s ease;
+}
+
+.sidebar-group[open] > .sidebar-group-summary .sidebar-group-chevron {
+ transform: rotate(90deg);
+}
+
+/* "Expand all" / "Collapse all" toggle — a quiet mono text button. */
+.sidebar-expand-toggle {
+ background: none;
+ border: none;
+ padding: 0.15rem 0.25rem;
+ cursor: pointer;
+ color: #9ca3af;
+ font-family: var(--font-family-mono);
+ font-size: 0.6875rem;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+}
+
+.sidebar-expand-toggle:hover { color: var(--color-cm-blue-600); }
+
+/* ==========================================================================
+ Search box (#searchbox mount point + Algolia Autocomplete's own markup)
+ search.js / lib/algolia.rb are untouched — only visuals below.
+ ========================================================================== */
+
+/* The mount point itself: grows with the top bar's flex row up to a
+ viewport-dependent cap. */
+#searchbox {
+ position: relative;
+ width: 100%;
+ max-width: 10rem;
+}
+
+@media (width >= 40rem) {
+ #searchbox { max-width: 14rem; }
+}
+
+@media (width >= 64rem) {
+ #searchbox { max-width: 20rem; }
+}
+
+#searchbox .aa-Autocomplete {
+ width: 100%;
+}
+
+#searchbox .aa-Form {
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ background-color: rgba(255, 255, 255, 0.12);
+ border-radius: 0.5rem;
+ box-shadow: none;
+ transition: background-color 0.15s ease, border-color 0.15s ease;
+}
+
+#searchbox .aa-Form:focus-within {
+ background-color: rgba(255, 255, 255, 0.18);
+ border-color: rgba(255, 255, 255, 0.5);
+ box-shadow: none;
+}
+
+#searchbox .aa-InputWrapperPrefix .aa-SubmitButton {
+ width: 2.25rem;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+#searchbox .aa-Input {
+ padding-left: 0.25rem;
+ color: #fff;
+ font-family: var(--font-family-display);
+ font-size: 0.875rem;
+}
+
+#searchbox .aa-Input::placeholder {
+ color: rgba(255, 255, 255, 0.55);
+}
+
+#searchbox .aa-InputWrapperSuffix .aa-ClearButton,
+#searchbox .aa-InputWrapperSuffix .aa-LoadingIndicator {
+ color: rgba(255, 255, 255, 0.6);
+}
+
+#searchbox .aa-Panel {
+ margin-top: 0.5rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.75rem;
+ box-shadow: 0 20px 40px -12px rgba(15, 23, 42, 0.35);
+ overflow: hidden;
+}
+
+#searchbox .aa-PanelLayout {
+ padding: 0.5rem 0;
+}
+
+#searchbox .aa-Source {
+ padding: 0;
+}
+
+.aa-Item {
+ margin-bottom: 0;
+ border-radius: 0.5rem;
+}
+
+.aa-Item[aria-selected="true"] {
+ background-color: var(--color-cm-blue-50);
+}
+
+a.search-result,
+a.search-result:hover {
+ display: block;
+ padding: 0.5rem 1rem;
+ text-decoration: none;
+ color: inherit;
+}
+
+a.search-result h4 {
+ margin: 0 0 0.2rem;
+ font-family: var(--font-family-display);
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: #111827;
+}
+
+a.search-result p {
+ margin: 0;
+ font-size: 0.8rem;
+ line-height: 1.5;
+ color: #6b7280;
+}
+
+a.search-result mark {
+ background-color: var(--color-cm-blue-100);
+ color: var(--color-cm-blue-700);
+ border-radius: 0.15em;
+ padding: 0 0.1em;
+}
+
+.search-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25em;
+ margin-left: 0.4rem;
+ padding: 0.05rem 0.5rem;
+ border-radius: 9999px;
+ background-color: var(--color-cm-blue-50);
+ color: var(--color-cm-blue-600);
+ font-family: var(--font-family-mono);
+ font-size: 0.625rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ vertical-align: middle;
+}
+
+.search-badge::after {
+ /* north-east arrow, forced text presentation (no emoji rendering) */
+ content: ' \2197\FE0E';
+}
+
+/* ==========================================================================
+ Sidebar mobile chevron
+ ========================================================================== */
+
+/* Bootstrap's HTML5-elements normalize shim sets `details { display: block }`
+ as an unlayered rule (same collision as .hidden above) — Tailwind's
+ layered `lg:hidden` utility can't beat it, so hide it here instead. */
+@media (width >= 64rem) {
+ .docs-mobile-toc { display: none; }
+}
+
+.docs-mobile-toc summary { list-style: none; }
+.docs-mobile-toc summary::-webkit-details-marker { display: none; }
+
+.docs-mobile-toc {
+ border-bottom: 1px solid var(--color-gray-100);
+ background-color: var(--color-gray-50);
+}
+
+/* The clickable "Contents" row. */
+.docs-mobile-toc-summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 1rem;
+ cursor: pointer;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+.docs-mobile-toc-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: var(--color-gray-700);
+ font-size: 0.875rem;
+ line-height: calc(1.25 / 0.875);
+ font-weight: 500;
+}
+
+.docs-mobile-toc-icon {
+ color: var(--color-gray-400);
+}
+
+.docs-mobile-toc-chevron {
+ font-size: 0.75rem;
+ color: var(--color-gray-400);
+ transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.docs-mobile-toc[open] .docs-mobile-toc-chevron {
+ transform: rotate(180deg);
+}
+
+/* Scrollable drawer body holding the sidebar partial. */
+.docs-mobile-toc-body {
+ padding: 0 1rem 1rem;
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+/* ==========================================================================
+ Article content — every compiled markdown page lands here with bare,
+ class-free tags (h1-h4, p, ul/ol, table, blockquote, code/pre, img).
+ ========================================================================== */
+
+.docs-article {
+ font-family: var(--font-family-display);
+ font-size: 0.95rem;
+ line-height: 1.65;
+ color: #374151;
+}
+
+.docs-article > *:first-child { margin-top: 0; }
+
+.docs-article h1 {
+ font-family: var(--font-family-display);
+ font-size: 2rem;
+ font-weight: 700;
+ letter-spacing: -0.025em;
+ color: #111827;
+ line-height: 1.2;
+ margin: 0 0 0.75rem;
+}
+
+.docs-article h2 {
+ font-family: var(--font-family-display);
+ font-size: 1.5rem;
+ font-weight: 700;
+ letter-spacing: -0.025em;
+ color: #111827;
+ line-height: 1.3;
+ margin: 2.25rem 0 0.85rem;
+ padding-top: 1.5rem;
+ border-top: 1px solid #eef0f3;
+}
+
+.docs-article > h2:first-of-type,
+.docs-article h1 + h2 {
+ border-top: none;
+ padding-top: 0;
+ margin-top: 1.25rem;
+}
+
+.docs-article h3 {
+ font-family: var(--font-family-display);
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #374151;
+ margin: 1.75rem 0 0.65rem;
+}
+
+.docs-article h4 {
+ font-family: var(--font-family-mono);
+ font-size: 0.875rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+ color: #374151;
+ margin: 1.4rem 0 0.5rem;
+}
+
+.docs-article p { margin: 0 0 1rem; }
+
+.docs-article a {
+ color: var(--color-cm-blue-600);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.docs-article a:hover { color: var(--color-cm-blue-700); }
+
+.docs-article ul, .docs-article ol {
+ margin: 0 0 1rem;
+ padding-left: 1.4rem;
+}
+
+/* Restore list markers. Tailwind v4's Preflight resets `ol, ul, menu` to
+ `list-style: none` (it lives in @layer base, so it beats the browser's
+ default). Bootstrap's unlayered UA-style used to leave markers alone, but
+ with Bootstrap removed nothing puts them back — so set them explicitly
+ here or every bulleted/numbered list in the docs renders flat. */
+.docs-article ul { list-style: disc; }
+.docs-article ol { list-style: decimal; }
+.docs-article ul ul { list-style: circle; }
+.docs-article ul ul ul { list-style: square; }
+.docs-article ol ol { list-style: lower-alpha; }
+.docs-article ol ol ol { list-style: lower-roman; }
+
+/* GitHub-style task lists (commonmarker `tasklist` extension) carry their
+ own checkbox , so suppress the redundant bullet on those items. */
+.docs-article li.task-list-item { list-style: none; }
+
+.docs-article li { margin-bottom: 0.4rem; }
+.docs-article li > ul, .docs-article li > ol { margin-top: 0.4rem; margin-bottom: 0; }
+
+.docs-article strong { color: #1f2937; font-weight: 600; }
+
+.docs-article hr {
+ border: none;
+ border-top: 1px solid #e5e7eb;
+ margin: 2rem 0;
+}
+
+/* Inline code (not inside a ) */
+.docs-article code {
+ background-color: var(--color-cm-blue-50);
+ color: var(--color-cm-blue-700);
+ border-radius: 0.3rem;
+ padding: 0.15em 0.4em;
+ font-size: 0.85em;
+}
+
+.docs-article pre code {
+ background: none;
+ color: inherit;
+ padding: 0;
+ border-radius: 0;
+ font-size: inherit;
+}
+
+/* Blockquote callouts */
+.docs-article blockquote {
+ margin: 1.5rem 0;
+ padding: 0.9rem 1.25rem;
+ border-left: 4px solid var(--color-cm-blue-500);
+ border-radius: 0.5rem;
+ background-color: rgba(77, 122, 186, 0.05);
+ color: #374151;
+}
+
+.docs-article blockquote p { margin: 0; }
+.docs-article blockquote p + p { margin-top: 0.6rem; }
+
+/* Tables */
+.docs-article table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 1.25rem 0 1.75rem;
+ font-size: 0.9rem;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.75rem;
+ overflow: hidden;
+}
+
+.docs-article thead { background-color: #f9fafb; }
+
+.docs-article th {
+ text-align: left;
+ font-family: var(--font-family-mono);
+ font-size: 0.72rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: #9ca3af;
+ padding: 0.6rem 1rem;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.docs-article td {
+ padding: 0.6rem 1rem;
+ border-top: 1px solid #f1f2f4;
+ color: #374151;
+ vertical-align: top;
+}
+
+.docs-article tbody tr:hover { background-color: #fafbfc; }
+
+/* Images */
+.docs-article img {
+ max-width: 100%;
+ height: auto;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ margin: 1.25rem 0;
+ padding: 0.5rem;
+ background: #fff;
+}
+
+.docs-article .image-caption {
+ display: block;
+ margin-top: -1rem;
+ margin-bottom: 1.25rem;
+ padding: 0.4rem 0.6rem;
+ border: 1px solid #e5e7eb;
+ border-top: none;
+ border-radius: 0 0 0.5rem 0.5rem;
+ background-color: #f9fafb;
+ color: #6b7280;
+ font-size: 0.8rem;
+}
+
+/* ==========================================================================
+ Code blocks + multi-language tab bar.
+
+ content/assets/javascripts/code.js groups adjacent blocks and
+ inserts a `.language_list` tab bar directly before the first of
+ each group, tagging every in the group with a `data-language`
+ attribute and showing one at a time via `display: block`. That JS is
+ untouched — we do NOT wrap the s in a container (it would break the
+ adjacency grouping). Instead the bar is connected to the code purely with
+ CSS: the bar rounds its TOP corners and drops its bottom margin/radius,
+ and every grouped (`[data-language]`) rounds only its BOTTOM corners
+ with no top margin — so whichever language is visible sits flush beneath
+ the bar as a single card. Standalone blocks keep full rounding.
+
+ lib/common_marker_filter.rb emits Rouge's class-based token spans (not
+ inline styles) so the palette below can colour them for the dark surface.
+ ========================================================================== */
+
+/* The code surface — a neutral near-black, no faux terminal-window chrome.
+ #1f2937 / #111827 are Tailwind gray-800 / gray-900, the same neutral scale
+ the rest of the site uses (h1 is gray-900), so code blocks sit in the same
+ colour family rather than introducing a separate blue. */
+.docs-article pre:not(.warning) {
+ margin: 1.5rem 0;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ background-color: #1f2937;
+ padding: 0;
+}
+
+.docs-article pre:not(.warning) code {
+ display: block;
+ padding: 1rem 1.25rem;
+ overflow-x: auto;
+ font-family: var(--font-family-mono);
+ font-size: 0.83rem;
+ line-height: 1.65;
+ white-space: pre;
+ /* Base colour for plain identifiers (.n/.nx), whitespace and anything not
+ recoloured below — a soft near-white. */
+ color: #e5e7eb;
+}
+
+/* Grouped blocks (data-language set by code.js): square the top corners and
+ drop the top margin so they butt flush against the tab bar above; keep the
+ rounded bottom. Applies to every in the group, so it holds whichever
+ language is currently shown. */
+.docs-article pre:not(.warning)[data-language] {
+ margin: 0 0 1.5rem;
+ border-radius: 0 0 0.5rem 0.5rem;
+}
+
+/* ------------------------------------------------------------------------
+ Syntax highlighting palette (Rouge class-based tokens)
+
+ The code surface is now a neutral near-black (#1f2937, relative luminance
+ ~0.02), so these hues read at much higher contrast than they did on the
+ old mid-blue background — most accent tokens now clear WCAG AA, and the
+ differentiation between categories is clearer. Every token stays fairly
+ LIGHT for legibility, with the HUES spread across the wheel (gold, green,
+ coral, cyan, orchid, lavender) so categories are told apart by hue.
+ Buckets follow Rouge's own token abbreviations; grouped by role:
+
+ keyword gold def/class/if/return/const/var
+ type/class orchid class + type + constant names, exceptions
+ string green every string flavour + regex + interpolation
+ number/const coral ints/floats/hex + true/false/null/nil + symbol
+ function/key cyan method & fn names, builtins, JSON/XML keys+tags
+ variable lavender $var / @ivar / @@cvar / globals
+ comment slate muted + italic, meant to recede
+ punct/operator quiet brackets, commas, operators — structural
+ ------------------------------------------------------------------------ */
+.docs-article pre:not(.warning) code {
+ /* Comments — muted slate, italic; low emphasis by design */
+ .c, .c1, .cm, .cs, .cd, .ch, .cpf { color: #9fb2ce; font-style: italic; }
+ /* Preprocessor / meta lines ( below with
+ no bottom margin/radius, so bar + code read as one connected surface. A
+ slightly darker strip (gray-900) sits above the gray-800 code area. */
+.docs-article .language_list {
+ display: flex;
+ align-items: center;
+ margin: 1.5rem 0 0;
+ padding: 0.3rem 0.5rem;
+ border-radius: 0.5rem 0.5rem 0 0;
+ background-color: #111827;
+}
+
+.docs-article .languages {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.15rem;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.docs-article .languages a {
+ display: inline-block;
+ padding: 0.3rem 0.7rem;
+ border-radius: 0.3rem;
+ color: #9ca3af;
+ font-family: var(--font-family-mono);
+ font-size: 0.72rem;
+ letter-spacing: 0.02em;
+ text-decoration: none;
+ transition: color 0.12s ease, background-color 0.12s ease;
+}
+
+.docs-article .languages a:hover {
+ color: #f9fafb;
+ background-color: rgba(255, 255, 255, 0.06);
+}
+
+/* The clicked/selected tab keeps focus, so this doubles as the "active"
+ indicator (code.js sets no active class of its own). */
+.docs-article .languages a:focus {
+ color: #f9fafb;
+ background-color: rgba(255, 255, 255, 0.1);
+ outline: none;
+}
+
+/* Plain (non-fenced) blocks — the dev-mode attribute dump uses
+ class="warning" (Bootstrap alert styling), explicitly excluded above. */