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 @@ + + favicon + + + + + + + + + + + + + + + + + + + + + + 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. */