From b2859923e5bbeffbf66bdb55d6ed9ea4588a7cd2 Mon Sep 17 00:00:00 2001 From: Steve Smith Date: Sun, 12 Jul 2026 18:51:33 +0100 Subject: [PATCH 1/3] Redesign the docs with Tailwind CSS, removing Bootstrap Replaces Bootstrap 3 with a Tailwind v4 build (npm @tailwindcss/cli, brand tokens in tailwind/input.css) and redesigns the docs to match the refreshed main-site brand: docked left nav rail with the article centered in the remaining width, collapsible sidebar sections (native details/summary, current section open, expand/collapse all, everything open on the home page), redesigned code fences with a connected language tab bar, Rouge class-based highlighting, and typography aligned with the dashboard's type scale. Markdown content is styled through semantic classes in the same Tailwind entry file since generated tags carry no classes. --- .github/workflows/main.yml | 11 + .gitignore | 6 + Dockerfile | 2 +- config.yaml | 5 + .../assets/images/cloudmailin-lockup-nav.svg | 41 + .../stylesheets/_bootstrap_choices.scss | 56 - content/assets/stylesheets/docs_new.scss | 378 ++---- docker-compose.yml | 2 +- layouts/application.haml | 115 +- layouts/sidebar.haml | 173 +-- lib/common_marker_filter.rb | 7 +- lib/helpers/section_helpers.rb | 62 + package-lock.json | 1064 +++++++++++++++++ package.json | 14 + tailwind/input.css | 797 ++++++++++++ 15 files changed, 2265 insertions(+), 468 deletions(-) create mode 100644 content/assets/images/cloudmailin-lockup-nav.svg delete mode 100644 content/assets/stylesheets/_bootstrap_choices.scss create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tailwind/input.css 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..57c31df 100644
--- a/layouts/application.haml
+++ b/layouts/application.haml
@@ -6,8 +6,15 @@
     %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"}
+    %link{rel: "stylesheet", href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"}
+
     
     
+    
     -# 
     
     
@@ -39,19 +46,45 @@
     %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
+    .sticky.top-0{class: "z-50"}
+      %nav{class: "bg-cm-blue-500 border-b border-cm-blue-600 shadow-nav"}
+        -# px-6 (1.5rem) matches the rail's padding-left so the logo and the
+           sidebar content share a left edge.
+        .w-full.px-6{class: "flex items-center justify-between gap-3 py-2"}
+          .flex.items-center.shrink-0{class: "gap-3"}
+            %a.flex.items-center{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', class: 'block')
+            %span.docs-badge.items-center.rounded-md.border.text-white{class: "border-white/30 px-2 py-0.5 text-[11px] font-mono uppercase tracking-wider text-white/80"}
+              Docs
+
+          .flex.items-center.justify-end{class: "gap-3 flex-1"}
+            #searchbox.relative{class: "w-full max-w-[10rem] sm:max-w-[14rem] lg:max-w-xs"}
+
+            %a.docs-nav-external-link.items-center{class: "gap-1.5 pl-3 border-l border-white/20 text-sm shrink-0", href: "https://www.cloudmailin.com"}
+              cloudmailin.com
+              %i.fas.fa-arrow-up-right-from-square{class: "text-[10px]"}
+
+    -# 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{class: "border-b border-gray-100 bg-gray-50"} + %summary.list-none{class: "flex items-center justify-between px-4 py-3 cursor-pointer select-none"} + %span.flex.items-center.text-gray-700{class: "gap-2 text-sm font-medium"} + %i.fas.fa-bars-staggered.text-gray-400 + Contents + %i.fas.fa-chevron-down.docs-mobile-toc-chevron{class: "text-xs text-gray-400 transition-transform"} + .px-4{class: "pb-4 max-h-[60vh] overflow-y-auto"} + = 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{class: "lg:flex lg:items-stretch"} + %aside.docs-desktop-aside + .docs-sidebar-sticky + = render('sidebar') + + %main.docs-main{class: "min-w-0 flex-1"} -if ENV['NANOC_ENV'] == 'development' .info Development Mode - if description.nil? @@ -62,9 +95,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 +112,54 @@ #comments -# = render 'comments' unless disable_comments? - = render('sidebar') + %footer{class: "bg-cm-blue-500"} + .container{class: "py-12"} + .grid{class: "grid-cols-2 sm:grid-cols-4 gap-8"} + .col-span-2{class: "sm:col-span-1"} + %a.docs-footer-brand.inline-block{class: "mb-3 text-xl", href: "https://www.cloudmailin.com"} + CloudMailin + %p.text-white{class: "text-sm leading-relaxed text-white/60"} + Developer-friendly email API for inbound and outbound email processing. + %div + %h3.docs-footer-heading Documentation + %ul.list-none.p-0.m-0{class: "space-y-2"} + %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.list-none.p-0.m-0{class: "space-y-2"} + %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' + .border-t.border-white{class: "border-white/15"} + .container{class: "py-4"} + %p.text-center.m-0.text-white{class: "text-xs text-white/40"} + © #{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..bc157c5 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/' +.space-y-4{class: "text-[0.9rem]"} + .flex.items-center.justify-between + = 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) + %i.fas.fa-chevron-right.sidebar-group-chevron + %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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 + %i.fas.fa-chevron-right.sidebar-group-chevron + %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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 + %i.fas.fa-chevron-right.sidebar-group-chevron + %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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.list-none.p-0{class: "space-y-0.5 pl-3 mt-0.5"} + %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.list-none.p-0{class: "space-y-0.5 pl-3 mt-0.5"} + - 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 + %i.fas.fa-chevron-right.sidebar-group-chevron + %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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) + %i.fas.fa-chevron-right.sidebar-group-chevron + %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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.list-none.p-0{class: "space-y-0.5 pl-3 mt-0.5"} + - 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 + %i.fas.fa-chevron-right.sidebar-group-chevron + %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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/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/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..f36e452 --- /dev/null +++ b/tailwind/input.css @@ -0,0 +1,797 @@ +/* + * 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. + * Tailwind utility classes are used directly in the HAML layout files + * (nav, footer, sidebar) where we author the markup ourselves. + * + * 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; } +} + +/* 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); +} + +.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 { + 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 { + color: rgba(255, 255, 255, 0.7); + text-decoration: none; +} + +.docs-nav-external-link:hover { color: rgba(255, 255, 255, 0.9); } + +.docs-footer-brand { + color: #fff; + text-decoration: none; + font-family: var(--font-family-logo); + font-weight: 700; + letter-spacing: -0.025em; +} + +.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; } + +/* ========================================================================== + 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. + ========================================================================== */ + +.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; } + +/* 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. + ========================================================================== */ + +#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[open] .docs-mobile-toc-chevron { + transform: rotate(180deg); +} + +/* ========================================================================== + 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. */

From 9e481b6dadaf9692f4401667dc98b8cddb484998 Mon Sep 17 00:00:00 2001
From: Steve Smith 
Date: Sun, 12 Jul 2026 19:01:22 +0100
Subject: [PATCH 2/3] Replace the Font Awesome CDN stylesheet with four inline
 SVG icons

The redesign loaded the entire Font Awesome library (render-blocking
CDN CSS plus a webfont) for four glyphs: sidebar chevrons, the nav
external-link arrow and the mobile drawer icons. Inline SVGs via an
icon helper drop the external request; 1em sizing and currentColor
keep the existing CSS working unchanged.
---
 layouts/application.haml    |  7 +++----
 layouts/sidebar.haml        | 12 ++++++------
 lib/helpers/icon_helpers.rb | 17 +++++++++++++++++
 lib/helpers_.rb             |  1 +
 tailwind/input.css          |  7 +++++++
 5 files changed, 34 insertions(+), 10 deletions(-)
 create mode 100644 lib/helpers/icon_helpers.rb

diff --git a/layouts/application.haml b/layouts/application.haml
index 57c31df..9a745fc 100644
--- a/layouts/application.haml
+++ b/layouts/application.haml
@@ -10,7 +10,6 @@
     %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"}
-    %link{rel: "stylesheet", href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"}
 
     
     
@@ -62,7 +61,7 @@
 
             %a.docs-nav-external-link.items-center{class: "gap-1.5 pl-3 border-l border-white/20 text-sm shrink-0", href: "https://www.cloudmailin.com"}
               cloudmailin.com
-              %i.fas.fa-arrow-up-right-from-square{class: "text-[10px]"}
+              = icon(:external_link, 'text-[10px]')
 
     -# Mobile "Contents" toggle — same sidebar partial as the desktop aside,
        just presented as a collapsible drawer above the article on narrow
@@ -70,9 +69,9 @@
     %details.docs-mobile-toc{class: "border-b border-gray-100 bg-gray-50"}
       %summary.list-none{class: "flex items-center justify-between px-4 py-3 cursor-pointer select-none"}
         %span.flex.items-center.text-gray-700{class: "gap-2 text-sm font-medium"}
-          %i.fas.fa-bars-staggered.text-gray-400
+          = icon(:bars_staggered, 'text-gray-400')
           Contents
-        %i.fas.fa-chevron-down.docs-mobile-toc-chevron{class: "text-xs text-gray-400 transition-transform"}
+        = icon(:chevron_down, 'docs-mobile-toc-chevron text-xs text-gray-400 transition-transform')
       .px-4{class: "pb-4 max-h-[60vh] overflow-y-auto"}
         = render('sidebar')
 
diff --git a/layouts/sidebar.haml b/layouts/sidebar.haml
index bc157c5..da99d04 100644
--- a/layouts/sidebar.haml
+++ b/layouts/sidebar.haml
@@ -6,7 +6,7 @@
   %details.sidebar-group{open: sidebar_section_open?('getting_started')}
     %summary.sidebar-group-summary
       %span Getting Started (Receiving)
-      %i.fas.fa-chevron-right.sidebar-group-chevron
+      = icon(:chevron_right, 'sidebar-group-chevron')
     %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"}
       %li= link_to 'Introduction', '/getting_started/', class: sidebar_link_class('/getting_started/')
       - for item in items_for_section('getting_started/receiving_email')
@@ -17,7 +17,7 @@
   %details.sidebar-group{open: sidebar_section_open?('http_post_formats')}
     %summary.sidebar-group-summary
       %span HTTP POST Formats
-      %i.fas.fa-chevron-right.sidebar-group-chevron
+      = icon(:chevron_right, 'sidebar-group-chevron')
     %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"}
       %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/')
@@ -30,7 +30,7 @@
   %details.sidebar-group{open: sidebar_section_open?('receiving_email')}
     %summary.sidebar-group-summary
       %span Receiving Email
-      %i.fas.fa-chevron-right.sidebar-group-chevron
+      = icon(:chevron_right, 'sidebar-group-chevron')
     %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"}
       %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/')
@@ -61,7 +61,7 @@
   %details.sidebar-group{open: sidebar_section_open?('local_dev')}
     %summary.sidebar-group-summary
       %span Local Development and Testing
-      %i.fas.fa-chevron-right.sidebar-group-chevron
+      = icon(:chevron_right, 'sidebar-group-chevron')
     %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"}
       %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/')
@@ -69,7 +69,7 @@
   %details.sidebar-group{open: sidebar_section_open?('outbound')}
     %summary.sidebar-group-summary
       %span Outbound (Sending)
-      %i.fas.fa-chevron-right.sidebar-group-chevron
+      = icon(:chevron_right, 'sidebar-group-chevron')
     %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"}
       %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/')
@@ -91,6 +91,6 @@
   %details.sidebar-group{open: sidebar_section_open?('guides')}
     %summary.sidebar-group-summary
       %span Guides
-      %i.fas.fa-chevron-right.sidebar-group-chevron
+      = icon(:chevron_right, 'sidebar-group-chevron')
     %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"}
       %li= link_to 'Verify DKIM is Working', '/guides/verify-dkim/', class: sidebar_link_class('/guides/verify-dkim/')
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_.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/tailwind/input.css b/tailwind/input.css
index f36e452..b2ebc51 100644
--- a/tailwind/input.css
+++ b/tailwind/input.css
@@ -275,6 +275,13 @@ code, kbd, pre, samp {
 
 .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;

From 7b3302a9b355af04f41e27f031afe71511f51403 Mon Sep 17 00:00:00 2001
From: Steve Smith 
Date: Sun, 12 Jul 2026 19:15:25 +0100
Subject: [PATCH 3/3] Move layout styling from utility classes to semantic CSS
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The docs layouts now use named classes (docs-topbar, sidebar-items,
docs-footer-grid, ...) styled in tailwind/input.css instead of utility
strings in the HAML — this site is mostly markdown content styled
through element selectors already, so the templates read better as
semantic markup. Tailwind still provides the build, Preflight, theme
tokens and palette variables the semantic rules draw on. Rendered
output is unchanged (verified against computed-style baselines).
---
 layouts/application.haml |  62 +++++-----
 layouts/sidebar.haml     |  22 ++--
 tailwind/input.css       | 252 ++++++++++++++++++++++++++++++++++++++-
 3 files changed, 291 insertions(+), 45 deletions(-)

diff --git a/layouts/application.haml b/layouts/application.haml
index 9a745fc..b3e481d 100644
--- a/layouts/application.haml
+++ b/layouts/application.haml
@@ -45,45 +45,43 @@
     %meta{name: 'twitter:creator', content: '@cloudmailin'}
     %meta{name: 'twitter:card', content: 'summary_large_image'}
   %body#documentation
-    .sticky.top-0{class: "z-50"}
-      %nav{class: "bg-cm-blue-500 border-b border-cm-blue-600 shadow-nav"}
-        -# px-6 (1.5rem) matches the rail's padding-left so the logo and the
-           sidebar content share a left edge.
-        .w-full.px-6{class: "flex items-center justify-between gap-3 py-2"}
-          .flex.items-center.shrink-0{class: "gap-3"}
-            %a.flex.items-center{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', class: 'block')
-            %span.docs-badge.items-center.rounded-md.border.text-white{class: "border-white/30 px-2 py-0.5 text-[11px] font-mono uppercase tracking-wider text-white/80"}
+    .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
 
-          .flex.items-center.justify-end{class: "gap-3 flex-1"}
-            #searchbox.relative{class: "w-full max-w-[10rem] sm:max-w-[14rem] lg:max-w-xs"}
+          .docs-topbar-tools
+            #searchbox
 
-            %a.docs-nav-external-link.items-center{class: "gap-1.5 pl-3 border-l border-white/20 text-sm shrink-0", href: "https://www.cloudmailin.com"}
+            %a.docs-nav-external-link{href: "https://www.cloudmailin.com"}
               cloudmailin.com
-              = icon(:external_link, 'text-[10px]')
+              = 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{class: "border-b border-gray-100 bg-gray-50"} - %summary.list-none{class: "flex items-center justify-between px-4 py-3 cursor-pointer select-none"} - %span.flex.items-center.text-gray-700{class: "gap-2 text-sm font-medium"} - = icon(:bars_staggered, 'text-gray-400') + %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 text-xs text-gray-400 transition-transform') - .px-4{class: "pb-4 max-h-[60vh] overflow-y-auto"} + = 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{class: "lg:flex lg:items-stretch"} + #docs-content %aside.docs-desktop-aside .docs-sidebar-sticky = render('sidebar') - %main.docs-main{class: "min-w-0 flex-1"} + %main.docs-main -if ENV['NANOC_ENV'] == 'development' .info Development Mode - if description.nil? @@ -111,30 +109,30 @@ #comments -# = render 'comments' unless disable_comments? - %footer{class: "bg-cm-blue-500"} - .container{class: "py-12"} - .grid{class: "grid-cols-2 sm:grid-cols-4 gap-8"} - .col-span-2{class: "sm:col-span-1"} - %a.docs-footer-brand.inline-block{class: "mb-3 text-xl", href: "https://www.cloudmailin.com"} + %footer.docs-footer + .container.docs-footer-body + .docs-footer-grid + .docs-footer-about + %a.docs-footer-brand{href: "https://www.cloudmailin.com"} CloudMailin - %p.text-white{class: "text-sm leading-relaxed text-white/60"} + %p.docs-footer-tagline Developer-friendly email API for inbound and outbound email processing. %div %h3.docs-footer-heading Documentation - %ul.list-none.p-0.m-0{class: "space-y-2"} + %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.list-none.p-0.m-0{class: "space-y-2"} + %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' - .border-t.border-white{class: "border-white/15"} - .container{class: "py-4"} - %p.text-center.m-0.text-white{class: "text-xs text-white/40"} + .docs-footer-divider + .container.docs-footer-legal + %p.docs-footer-copyright © #{Time.now.year} CloudMailin.com -# Sidebar "Expand all / Collapse all" toggle. The sidebar partial renders diff --git a/layouts/sidebar.haml b/layouts/sidebar.haml index da99d04..be45dcd 100644 --- a/layouts/sidebar.haml +++ b/layouts/sidebar.haml @@ -1,5 +1,5 @@ -.space-y-4{class: "text-[0.9rem]"} - .flex.items-center.justify-between +.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' @@ -7,7 +7,7 @@ %summary.sidebar-group-summary %span Getting Started (Receiving) = icon(:chevron_right, 'sidebar-group-chevron') - %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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) @@ -18,7 +18,7 @@ %summary.sidebar-group-summary %span HTTP POST Formats = icon(:chevron_right, 'sidebar-group-chevron') - %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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/') @@ -31,7 +31,7 @@ %summary.sidebar-group-summary %span Receiving Email = icon(:chevron_right, 'sidebar-group-chevron') - %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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/') @@ -39,7 +39,7 @@ %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.list-none.p-0{class: "space-y-0.5 pl-3 mt-0.5"} + %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/') @@ -54,7 +54,7 @@ %li = link_to 'Examples', '/receiving_email/examples/', class: sidebar_link_class('/receiving_email/examples/') - if in_section?('receiving_email/examples', include_self: true) - %ul.list-none.p-0{class: "space-y-0.5 pl-3 mt-0.5"} + %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) @@ -62,7 +62,7 @@ %summary.sidebar-group-summary %span Local Development and Testing = icon(:chevron_right, 'sidebar-group-chevron') - %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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/') @@ -70,7 +70,7 @@ %summary.sidebar-group-summary %span Outbound (Sending) = icon(:chevron_right, 'sidebar-group-chevron') - %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %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/') @@ -81,7 +81,7 @@ %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.list-none.p-0{class: "space-y-0.5 pl-3 mt-0.5"} + %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) @@ -92,5 +92,5 @@ %summary.sidebar-group-summary %span Guides = icon(:chevron_right, 'sidebar-group-chevron') - %ul.list-none.p-0.m-0{class: "space-y-0.5 mt-2"} + %ul.sidebar-items %li= link_to 'Verify DKIM is Working', '/guides/verify-dkim/', class: sidebar_link_class('/guides/verify-dkim/') diff --git a/tailwind/input.css b/tailwind/input.css index b2ebc51..d0705c8 100644 --- a/tailwind/input.css +++ b/tailwind/input.css @@ -18,8 +18,10 @@ * 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. - * Tailwind utility classes are used directly in the HAML layout files - * (nav, footer, sidebar) where we author the markup ourselves. + * 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 @@ -122,6 +124,70 @@ code, kbd, pre, samp { .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, @@ -130,6 +196,14 @@ code, kbd, pre, samp { 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. */ @@ -143,6 +217,10 @@ code, kbd, pre, samp { } .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; } @@ -180,20 +258,69 @@ code, kbd, pre, samp { } .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; @@ -212,12 +339,74 @@ code, kbd, pre, samp { .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; @@ -313,6 +502,22 @@ code, kbd, pre, samp { 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%; } @@ -443,10 +648,53 @@ a.search-result mark { .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).