/*! * Spry CSS — a light, no-JavaScript CSS framework of utilities and components. * * Optional companion to the Spry web application framework. * Core + default theme in one file. Themes live in themes/*.css and are * activated with (or the #spry-theme-* radio hooks * used by the demo page's CSS-only switcher). * * Color mode: (omit for auto). */ /* ========================================================================== 1. Default theme — light palette -------------------------------------------------------------------------- Palette blocks use :where() so their specificity is (0,0,0): any active theme scope (attribute or radio hook) overrides them, including the dark blocks below. Themes can therefore be "fully light" and ignore dark mode. ========================================================================== */ :where(:root) { color-scheme: light; /* Palette */ --spry-bg: #f5f7f9; --spry-bg-alt: #edf1f5; --spry-bg-inset: #eef2f6; --spry-surface: #ffffff; --spry-surface-hover: #f6f9fc; --spry-fg: #1c2733; --spry-fg-muted: #5b6b7c; --spry-border: #d9e1e9; --spry-border-strong: #b9c6d2; --spry-accent: #346894; --spry-accent-hover: #2a5578; --spry-accent-fg: #ffffff; --spry-danger: #b3261e; --spry-success: #2e7d43; --spry-warning: #946200; --spry-info: #0b68a3; --spry-link: var(--spry-accent); --spry-link-hover: var(--spry-accent-hover); --spry-backdrop: rgb(15 23 36 / 0.45); /* Soft tints are derived — themes only set the base colors above */ --spry-accent-soft: color-mix(in srgb, var(--spry-accent) 12%, transparent); --spry-danger-soft: color-mix(in srgb, var(--spry-danger) 10%, transparent); --spry-success-soft: color-mix(in srgb, var(--spry-success) 10%, transparent); --spry-warning-soft: color-mix(in srgb, var(--spry-warning) 14%, transparent); --spry-info-soft: color-mix(in srgb, var(--spry-info) 10%, transparent); --spry-focus-ring: 0 0 0 3px color-mix(in srgb, var(--spry-accent) 25%, transparent); /* Typography */ --spry-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --spry-font-heading: var(--spry-font); --spry-font-mono: ui-monospace, "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; --spry-font-size: 1rem; --spry-line-height: 1.6; --spry-heading-weight: 650; --spry-heading-color: var(--spry-fg); /* Shape, depth, motion */ --spry-radius: 0.5rem; --spry-radius-sm: 0.3rem; --spry-radius-lg: 0.75rem; --spry-radius-pill: 999px; --spry-border-width: 1px; --spry-shadow: 0 1px 2px rgb(16 24 40 / 0.05), 0 1px 3px rgb(16 24 40 / 0.08); --spry-shadow-lg: 0 16px 40px -12px rgb(16 24 40 / 0.25); --spry-glass: none; --spry-transition: 140ms ease; --spry-dialog-duration: 180ms; --spry-dialog-ease: cubic-bezier(0.2, 0.9, 0.3, 1); --spry-dialog-close-duration: 140ms; --spry-dialog-close-ease: ease; --spry-dialog-in: translateY(0.6rem) scale(0.98); /* Spacing scale (0..5) */ --spry-space-0: 0; --spry-space-1: 0.25rem; --spry-space-2: 0.5rem; --spry-space-3: 1rem; --spry-space-4: 1.5rem; --spry-space-5: 2.5rem; /* Controls (inputs, selects) */ --spry-control-bg: var(--spry-surface); --spry-control-fg: var(--spry-fg); --spry-control-border: var(--spry-border); --spry-control-radius: var(--spry-radius-sm); --spry-control-padding-x: 0.65rem; --spry-control-padding-y: 0.45rem; /* Buttons */ --spry-btn-bg: var(--spry-surface); --spry-btn-fg: var(--spry-fg); --spry-btn-border: var(--spry-border); --spry-btn-bg-hover: var(--spry-bg-alt); --spry-btn-bg-active: var(--spry-bg-inset); /* Cards */ --spry-card-bg: var(--spry-surface); --spry-card-border: var(--spry-border); --spry-card-padding: 1.25rem; --spry-card-shadow: var(--spry-shadow); /* Tables */ --spry-table-header-bg: var(--spry-bg-alt); --spry-table-hover: var(--spry-bg-alt); /* Progress */ --spry-progress-bg: var(--spry-bg-inset); --spry-progress-fg: var(--spry-accent); --spry-pulse-cap: 0.4em; /* half the default bar height: stadium ends */ /* Popovers & dialogs */ --spry-popover-bg: var(--spry-surface); --spry-popover-border: var(--spry-border); --spry-popover-shadow: var(--spry-shadow-lg); --spry-popover-padding: 1.1rem 1.25rem; accent-color: var(--spry-accent); } /* -------------------------------------------------------------------------- Default theme — dark palette. Duplicated on purpose: once for forced dark (data-mode="dark" or the #spry-mode-dark switcher hook), once for auto dark (system preference, unless disabled with data-mode="light" or #spry-mode-light). -------------------------------------------------------------------------- */ :where(:root[data-mode="dark"], html:has(#spry-mode-dark:checked)) { color-scheme: dark; --spry-bg: #0f141b; --spry-bg-alt: #151d27; --spry-bg-inset: #1a2330; --spry-surface: #161f2b; --spry-surface-hover: #1c2734; --spry-fg: #dee7f1; --spry-fg-muted: #8fa1b5; --spry-border: #2a3646; --spry-border-strong: #3d4c61; --spry-accent: #6ba3d8; --spry-accent-hover: #8cbcea; --spry-accent-fg: #0b141d; --spry-danger: #e5736c; --spry-success: #72c08c; --spry-warning: #d8a850; --spry-info: #6cb6e8; --spry-backdrop: rgb(2 6 12 / 0.6); --spry-shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 1px 3px rgb(0 0 0 / 0.4); --spry-shadow-lg: 0 16px 40px -12px rgb(0 0 0 / 0.6); } @media (prefers-color-scheme: dark) { :where(:root:not([data-mode="light"]):not(:has(#spry-mode-light:checked)), html:has(#spry-mode-auto:checked):not([data-mode="light"])) { color-scheme: dark; --spry-bg: #0f141b; --spry-bg-alt: #151d27; --spry-bg-inset: #1a2330; --spry-surface: #161f2b; --spry-surface-hover: #1c2734; --spry-fg: #dee7f1; --spry-fg-muted: #8fa1b5; --spry-border: #2a3646; --spry-border-strong: #3d4c61; --spry-accent: #6ba3d8; --spry-accent-hover: #8cbcea; --spry-accent-fg: #0b141d; --spry-danger: #e5736c; --spry-success: #72c08c; --spry-warning: #d8a850; --spry-info: #6cb6e8; --spry-backdrop: rgb(2 6 12 / 0.6); --spry-shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 1px 3px rgb(0 0 0 / 0.4); --spry-shadow-lg: 0 16px 40px -12px rgb(0 0 0 / 0.6); } } /* ========================================================================== 2. Reset & sane defaults ========================================================================== */ *, *::before, *::after { box-sizing: border-box; } * { margin: 0; } html { scroll-behavior: smooth; scroll-padding-top: 5rem; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } body { font-family: var(--spry-font); font-size: var(--spry-font-size); line-height: var(--spry-line-height); color: var(--spry-fg); background: var(--spry-bg); -webkit-font-smoothing: antialiased; } img, video, svg { display: block; max-width: 100%; height: auto; } svg:not([width]) { width: 1em; } input, button, select, textarea { font: inherit; color: inherit; } button { cursor: pointer; } :where(a) { color: var(--spry-link); text-underline-offset: 0.2em; } :where(a:hover) { color: var(--spry-link-hover); } :focus-visible { outline: 2px solid var(--spry-accent); outline-offset: 2px; } ::selection { background: color-mix(in srgb, var(--spry-accent) 30%, transparent); } ::placeholder { color: var(--spry-fg-muted); opacity: 0.8; } [hidden] { display: none !important; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } /* ========================================================================== 3. Base typography ========================================================================== */ h1, h2, h3, h4, h5, h6 { font-family: var(--spry-font-heading); font-weight: var(--spry-heading-weight); color: var(--spry-heading-color); line-height: 1.2; text-wrap: balance; margin-block: 1.6em 0.6em; } h1, .h1 { font-size: clamp(1.9rem, 1.4rem + 1.8vw, 2.6rem); } h2, .h2 { font-size: clamp(1.5rem, 1.25rem + 1vw, 2rem); } h3, .h3 { font-size: 1.35rem; } h4, .h4 { font-size: 1.15rem; } h5, .h5 { font-size: 1rem; } h6, .h6 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--spry-fg-muted); } h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child, h6:first-child { margin-block-start: 0; } p, ul, ol, dl, blockquote, figure, pre { margin-block: 0 1em; } :where(li) > ul, :where(li) > ol { margin-block: 0.25em; } :where(li + li) { margin-block-start: 0.25em; } :where(dt) { font-weight: 600; } :where(dd) { margin-inline-start: 1.5em; } small { font-size: 0.85em; } b, strong { font-weight: 700; } hr { border: 0; border-top: var(--spry-border-width) solid var(--spry-border); margin-block: 2rem; } blockquote { border-inline-start: 3px solid var(--spry-accent); padding: 0.25rem 1rem; color: var(--spry-fg-muted); } blockquote > p:last-child { margin-bottom: 0; } :where(code, kbd, samp) { font-family: var(--spry-font-mono); font-size: 0.9em; } :where(:not(pre) > code) { background: var(--spry-bg-inset); border: var(--spry-border-width) solid var(--spry-border); border-radius: var(--spry-radius-sm); padding: 0.1em 0.35em; } pre { background: var(--spry-bg-inset); border: var(--spry-border-width) solid var(--spry-border); border-radius: var(--spry-radius); padding: 1rem 1.25rem; overflow-x: auto; line-height: 1.5; } pre code { background: none; border: 0; padding: 0; font-size: 0.875rem; } kbd { background: var(--spry-bg-alt); border: var(--spry-border-width) solid var(--spry-border-strong); border-bottom-width: 2px; border-radius: var(--spry-radius-sm); padding: 0.1em 0.4em; font-size: 0.8em; } mark { background: var(--spry-warning-soft); color: inherit; border-radius: 0.2em; padding: 0 0.15em; } abbr[title] { text-decoration: underline dotted; cursor: help; } :where(figure) figcaption { color: var(--spry-fg-muted); font-size: 0.875em; margin-top: 0.5rem; } /* ========================================================================== 4. Utilities — layout ========================================================================== */ .container { max-width: 72rem; margin-inline: auto; padding-inline: clamp(1rem, 3vw, 2rem); } .container-sm { max-width: 52rem; } .container-lg { max-width: 96rem; } .stack { display: flex; flex-direction: column; gap: var(--spry-space-3); } .cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--spry-space-2); } .center { display: flex; align-items: center; justify-content: center; } .with-sidebar { display: grid; grid-template-columns: minmax(11rem, 16rem) minmax(0, 1fr); gap: var(--spry-space-5); } @media (max-width: 55rem) { .with-sidebar { grid-template-columns: minmax(0, 1fr); } } .block { display: block; } .inline-block { display: inline-block; } .inline { display: inline; } .hidden { display: none; } .visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } .relative { position: relative; } .absolute { position: absolute; } .fixed { position: fixed; } .sticky { position: sticky; } .top-0 { top: 0; } .right-0 { right: 0; } .bottom-0 { bottom: 0; } .left-0 { left: 0; } .inset-0 { inset: 0; } .overflow-auto { overflow: auto; } .overflow-hidden { overflow: hidden; } .w-25 { width: 25%; } .w-50 { width: 50%; } .w-75 { width: 75%; } .w-100 { width: 100%; } .w-auto { width: auto; } .h-100 { height: 100%; } .mw-100 { max-width: 100%; } .min-w-0 { min-width: 0; } /* ========================================================================== 5. Utilities — spacing ========================================================================== */ .m-0 { margin: var(--spry-space-0); } .m-1 { margin: var(--spry-space-1); } .m-2 { margin: var(--spry-space-2); } .m-3 { margin: var(--spry-space-3); } .m-4 { margin: var(--spry-space-4); } .m-5 { margin: var(--spry-space-5); } .mt-0 { margin-top: var(--spry-space-0); } .mt-1 { margin-top: var(--spry-space-1); } .mt-2 { margin-top: var(--spry-space-2); } .mt-3 { margin-top: var(--spry-space-3); } .mt-4 { margin-top: var(--spry-space-4); } .mt-5 { margin-top: var(--spry-space-5); } .mr-0 { margin-right: var(--spry-space-0); } .mr-1 { margin-right: var(--spry-space-1); } .mr-2 { margin-right: var(--spry-space-2); } .mr-3 { margin-right: var(--spry-space-3); } .mr-4 { margin-right: var(--spry-space-4); } .mr-5 { margin-right: var(--spry-space-5); } .mb-0 { margin-bottom: var(--spry-space-0); } .mb-1 { margin-bottom: var(--spry-space-1); } .mb-2 { margin-bottom: var(--spry-space-2); } .mb-3 { margin-bottom: var(--spry-space-3); } .mb-4 { margin-bottom: var(--spry-space-4); } .mb-5 { margin-bottom: var(--spry-space-5); } .ml-0 { margin-left: var(--spry-space-0); } .ml-1 { margin-left: var(--spry-space-1); } .ml-2 { margin-left: var(--spry-space-2); } .ml-3 { margin-left: var(--spry-space-3); } .ml-4 { margin-left: var(--spry-space-4); } .ml-5 { margin-left: var(--spry-space-5); } .mx-0 { margin-inline: var(--spry-space-0); } .mx-1 { margin-inline: var(--spry-space-1); } .mx-2 { margin-inline: var(--spry-space-2); } .mx-3 { margin-inline: var(--spry-space-3); } .mx-4 { margin-inline: var(--spry-space-4); } .mx-5 { margin-inline: var(--spry-space-5); } .my-0 { margin-block: var(--spry-space-0); } .my-1 { margin-block: var(--spry-space-1); } .my-2 { margin-block: var(--spry-space-2); } .my-3 { margin-block: var(--spry-space-3); } .my-4 { margin-block: var(--spry-space-4); } .my-5 { margin-block: var(--spry-space-5); } .mx-auto { margin-inline: auto; } .ml-auto { margin-left: auto; } .mt-auto { margin-top: auto; } .p-0 { padding: var(--spry-space-0); } .p-1 { padding: var(--spry-space-1); } .p-2 { padding: var(--spry-space-2); } .p-3 { padding: var(--spry-space-3); } .p-4 { padding: var(--spry-space-4); } .p-5 { padding: var(--spry-space-5); } .pt-0 { padding-top: var(--spry-space-0); } .pt-1 { padding-top: var(--spry-space-1); } .pt-2 { padding-top: var(--spry-space-2); } .pt-3 { padding-top: var(--spry-space-3); } .pt-4 { padding-top: var(--spry-space-4); } .pt-5 { padding-top: var(--spry-space-5); } .pr-0 { padding-right: var(--spry-space-0); } .pr-1 { padding-right: var(--spry-space-1); } .pr-2 { padding-right: var(--spry-space-2); } .pr-3 { padding-right: var(--spry-space-3); } .pr-4 { padding-right: var(--spry-space-4); } .pr-5 { padding-right: var(--spry-space-5); } .pb-0 { padding-bottom: var(--spry-space-0); } .pb-1 { padding-bottom: var(--spry-space-1); } .pb-2 { padding-bottom: var(--spry-space-2); } .pb-3 { padding-bottom: var(--spry-space-3); } .pb-4 { padding-bottom: var(--spry-space-4); } .pb-5 { padding-bottom: var(--spry-space-5); } .pl-0 { padding-left: var(--spry-space-0); } .pl-1 { padding-left: var(--spry-space-1); } .pl-2 { padding-left: var(--spry-space-2); } .pl-3 { padding-left: var(--spry-space-3); } .pl-4 { padding-left: var(--spry-space-4); } .pl-5 { padding-left: var(--spry-space-5); } .px-0 { padding-inline: var(--spry-space-0); } .px-1 { padding-inline: var(--spry-space-1); } .px-2 { padding-inline: var(--spry-space-2); } .px-3 { padding-inline: var(--spry-space-3); } .px-4 { padding-inline: var(--spry-space-4); } .px-5 { padding-inline: var(--spry-space-5); } .py-0 { padding-block: var(--spry-space-0); } .py-1 { padding-block: var(--spry-space-1); } .py-2 { padding-block: var(--spry-space-2); } .py-3 { padding-block: var(--spry-space-3); } .py-4 { padding-block: var(--spry-space-4); } .py-5 { padding-block: var(--spry-space-5); } /* ========================================================================== 6. Utilities — flex ========================================================================== */ .flex { display: flex; } .inline-flex { display: inline-flex; } .flex-row { flex-direction: row; } .flex-col { flex-direction: column; } .flex-col-reverse { flex-direction: column-reverse; } .flex-wrap { flex-wrap: wrap; } .flex-nowrap { flex-wrap: nowrap; } .flex-1 { flex: 1 1 0%; } .flex-auto { flex: 1 1 auto; } .flex-none { flex: none; } .items-start { align-items: flex-start; } .items-center { align-items: center; } .items-end { align-items: flex-end; } .items-stretch { align-items: stretch; } .items-baseline { align-items: baseline; } .justify-start { justify-content: flex-start; } .justify-center { justify-content: center; } .justify-end { justify-content: flex-end; } .justify-between { justify-content: space-between; } .justify-around { justify-content: space-around; } .justify-evenly { justify-content: space-evenly; } .self-start { align-self: flex-start; } .self-center { align-self: center; } .self-end { align-self: flex-end; } .order-first { order: -1; } .order-last { order: 999; } /* ========================================================================== 7. Utilities — grid ========================================================================== */ .grid { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--spry-grid-min, 14rem)), 1fr)); } .grid-sm { --spry-grid-min: 9rem; } .grid-lg { --spry-grid-min: 20rem; } .grid-cols-1 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(1, minmax(0, 1fr)); } .grid-cols-2 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(2, minmax(0, 1fr)); } .grid-cols-3 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(3, minmax(0, 1fr)); } .grid-cols-4 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(4, minmax(0, 1fr)); } .grid-cols-5 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(5, minmax(0, 1fr)); } .grid-cols-6 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(6, minmax(0, 1fr)); } .grid-cols-7 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(7, minmax(0, 1fr)); } .grid-cols-8 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(8, minmax(0, 1fr)); } .grid-cols-9 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(9, minmax(0, 1fr)); } .grid-cols-10 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(10, minmax(0, 1fr)); } .grid-cols-11 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(11, minmax(0, 1fr)); } .grid-cols-12 { display: grid; gap: var(--spry-space-3); grid-template-columns: repeat(12, minmax(0, 1fr)); } @media (max-width: 45rem) { .grid-cols-3, .grid-cols-4, .grid-cols-5, .grid-cols-6, .grid-cols-7, .grid-cols-8, .grid-cols-9, .grid-cols-10, .grid-cols-11, .grid-cols-12 { grid-template-columns: repeat(2, minmax(0, 1fr)); } } .col-span-1 { grid-column: span 1; } .col-span-2 { grid-column: span 2; } .col-span-3 { grid-column: span 3; } .col-span-4 { grid-column: span 4; } .col-span-5 { grid-column: span 5; } .col-span-6 { grid-column: span 6; } .col-span-7 { grid-column: span 7; } .col-span-8 { grid-column: span 8; } .col-span-9 { grid-column: span 9; } .col-span-10 { grid-column: span 10; } .col-span-11 { grid-column: span 11; } .col-span-12 { grid-column: span 12; } .col-span-full { grid-column: 1 / -1; } .gap-0 { gap: var(--spry-space-0); } .gap-1 { gap: var(--spry-space-1); } .gap-2 { gap: var(--spry-space-2); } .gap-3 { gap: var(--spry-space-3); } .gap-4 { gap: var(--spry-space-4); } .gap-5 { gap: var(--spry-space-5); } .gap-x-0 { column-gap: var(--spry-space-0); } .gap-x-1 { column-gap: var(--spry-space-1); } .gap-x-2 { column-gap: var(--spry-space-2); } .gap-x-3 { column-gap: var(--spry-space-3); } .gap-x-4 { column-gap: var(--spry-space-4); } .gap-x-5 { column-gap: var(--spry-space-5); } .gap-y-0 { row-gap: var(--spry-space-0); } .gap-y-1 { row-gap: var(--spry-space-1); } .gap-y-2 { row-gap: var(--spry-space-2); } .gap-y-3 { row-gap: var(--spry-space-3); } .gap-y-4 { row-gap: var(--spry-space-4); } .gap-y-5 { row-gap: var(--spry-space-5); } /* ========================================================================== 8. Utilities — text & color ========================================================================== */ .text-xs { font-size: 0.75rem; } .text-sm { font-size: 0.875rem; } .text-base { font-size: 1rem; } .text-lg { font-size: 1.125rem; } .text-xl { font-size: 1.25rem; } .text-2xl { font-size: 1.5rem; } .text-left { text-align: left; } .text-center { text-align: center; } .text-right { text-align: right; } .text-nowrap { white-space: nowrap; } .text-muted { color: var(--spry-fg-muted); } .text-accent { color: var(--spry-accent); } .text-danger { color: var(--spry-danger); } .text-success { color: var(--spry-success); } .text-warning { color: var(--spry-warning); } .text-info { color: var(--spry-info); } .fw-light { font-weight: 300; } .fw-normal { font-weight: 400; } .fw-medium { font-weight: 500; } .fw-semibold { font-weight: 600; } .fw-bold { font-weight: 700; } .fs-italic { font-style: italic; } .fs-normal { font-style: normal; } .uppercase { text-transform: uppercase; } .lowercase { text-transform: lowercase; } .capitalize { text-transform: capitalize; } .lead { font-size: 1.15em; line-height: 1.55; color: var(--spry-fg-muted); } .small { font-size: 0.85em; } .mono { font-family: var(--spry-font-mono); font-size: 0.92em; } .num { font-variant-numeric: tabular-nums; text-align: right; } .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .clamp-2, .clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; } .clamp-2 { -webkit-line-clamp: 2; line-clamp: 2; } .clamp-3 { -webkit-line-clamp: 3; line-clamp: 3; } .measure { max-width: 65ch; } .measure-sm { max-width: 45ch; } .bg-surface { background: var(--spry-surface); } .bg-alt { background: var(--spry-bg-alt); } .bg-inset { background: var(--spry-bg-inset); } .bg-accent { background: var(--spry-accent); color: var(--spry-accent-fg); } .border { border: var(--spry-border-width) solid var(--spry-border); } .border-0 { border: 0; } .rounded { border-radius: var(--spry-radius); } .rounded-sm { border-radius: var(--spry-radius-sm); } .rounded-lg { border-radius: var(--spry-radius-lg); } .rounded-pill { border-radius: var(--spry-radius-pill); } .shadow { box-shadow: var(--spry-shadow); } .shadow-lg { box-shadow: var(--spry-shadow-lg); } .shadow-none { box-shadow: none; } /* Long-form content (markdown, docs, help text) inside cards and dialogs. The page-scale headings are oversized in those containers, so .prose caps the measure and steps headings down to a document scale; spacing rides --spry-space-* so themes and dark mode compose unchanged. */ .prose { max-width: 65ch; } .prose h1 { font-size: 1.5rem; margin-block: var(--spry-space-4) var(--spry-space-2); } .prose h2 { font-size: 1.25rem; margin-block: var(--spry-space-4) var(--spry-space-2); } .prose h3 { font-size: 1.1rem; margin-block: var(--spry-space-3) var(--spry-space-2); } .prose h4 { font-size: 1rem; margin-block: var(--spry-space-3) var(--spry-space-2); } .prose p, .prose ul, .prose ol, .prose dl, .prose blockquote { margin-block-end: var(--spry-space-3); } /* Flush at both ends — trailing/leading flow margins would sit on top of the card or dialog padding and make one side look heavier than the other. */ .prose > :first-child { margin-block-start: 0; } .prose > :last-child { margin-block-end: 0; } /* ========================================================================== 9. Components — topbar & navigation ========================================================================== */ .topbar { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--spry-space-2); padding: 0.65rem clamp(1rem, 3vw, 2rem); background: var(--spry-surface); border-bottom: var(--spry-border-width) solid var(--spry-border); backdrop-filter: var(--spry-glass); } .topbar-brand { font-weight: 700; font-size: 1.05rem; color: var(--spry-fg); text-decoration: none; letter-spacing: 0.01em; } .topbar-brand:hover { color: var(--spry-accent); } .nav { display: flex; flex-wrap: wrap; align-items: center; gap: var(--spry-space-1); list-style: none; padding: 0; margin: 0; } .nav a { display: inline-block; padding: 0.35rem 0.75rem; border-radius: var(--spry-radius-sm); color: var(--spry-fg); text-decoration: none; font-weight: 500; transition: background-color var(--spry-transition), color var(--spry-transition); } .nav a:hover { background: var(--spry-bg-alt); color: var(--spry-accent); } .nav a.active { background: var(--spry-accent-soft); color: var(--spry-accent); font-weight: 600; } /* Menus with CSS-only submenus */ .menu, .menu ul { list-style: none; margin: 0; padding: 0; } .menu { display: flex; flex-direction: column; gap: 2px; } .menu a { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.4rem 0.7rem; border-radius: var(--spry-radius-sm); color: var(--spry-fg); text-decoration: none; } .menu a:hover, .menu a.active { background: var(--spry-bg-alt); color: var(--spry-accent); } .has-submenu { position: relative; } .has-submenu > a::after { content: "▾"; font-size: 0.75em; color: var(--spry-fg-muted); } .submenu { position: absolute; top: 100%; left: 0; z-index: 30; min-width: 11rem; padding: 0.35rem; background: var(--spry-popover-bg); border: var(--spry-border-width) solid var(--spry-popover-border); border-radius: var(--spry-radius); box-shadow: var(--spry-popover-shadow); backdrop-filter: var(--spry-glass); opacity: 0; visibility: hidden; transform: translateY(0.3rem); transition: opacity var(--spry-transition), transform var(--spry-transition), visibility var(--spry-transition); } .submenu a, .menu a { width: 100%; } .submenu a { display: block; padding: 0.4rem 0.75rem; color: var(--spry-fg); text-decoration: none; border-radius: var(--spry-radius-sm); } .submenu a:hover { background: var(--spry-accent-soft); color: var(--spry-accent); } .has-submenu:hover > .submenu, .has-submenu:focus-within > .submenu { opacity: 1; visibility: visible; transform: none; } .menu .has-submenu .submenu { position: static; opacity: 1; visibility: visible; transform: none; border: 0; box-shadow: none; padding: 0 0 0 1rem; margin: 2px 0; } .menu .has-submenu > a::after { content: "▸"; } .menu .has-submenu .submenu a { font-size: 0.95em; } .breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; list-style: none; padding: 0; margin: 0; font-size: 0.9rem; } .breadcrumb li + li::before { content: "/"; color: var(--spry-fg-muted); margin-right: 0.4rem; } .breadcrumb a { color: var(--spry-link); text-decoration: none; } .breadcrumb a:hover { text-decoration: underline; } .breadcrumb [aria-current] { color: var(--spry-fg-muted); } /* ========================================================================== 10. Components — tabs (CSS-only, radio based) -------------------------------------------------------------------------- Markup: all radio inputs first (direct children), then .tab-list with one label per input (same order), then .tab-panels with one panel per input. ========================================================================== */ .tabs > input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; } .tab-list { display: flex; flex-wrap: wrap; gap: 0.25rem; border-bottom: var(--spry-border-width) solid var(--spry-border); } .tab-list label { padding: 0.5rem 0.9rem; border-radius: var(--spry-radius-sm) var(--spry-radius-sm) 0 0; cursor: pointer; font-weight: 500; color: var(--spry-fg-muted); transition: background-color var(--spry-transition), color var(--spry-transition), box-shadow var(--spry-transition); } .tab-list label:hover { color: var(--spry-accent); background: var(--spry-bg-alt); } .tab-panels > * { display: none; padding-top: var(--spry-space-3); } .tab-panels > :first-child { display: block; } .tabs > input:nth-of-type(1):checked ~ .tab-list label:nth-of-type(1), .tabs > input:nth-of-type(2):checked ~ .tab-list label:nth-of-type(2), .tabs > input:nth-of-type(3):checked ~ .tab-list label:nth-of-type(3), .tabs > input:nth-of-type(4):checked ~ .tab-list label:nth-of-type(4), .tabs > input:nth-of-type(5):checked ~ .tab-list label:nth-of-type(5), .tabs > input:nth-of-type(6):checked ~ .tab-list label:nth-of-type(6) { color: var(--spry-accent); font-weight: 600; box-shadow: inset 0 -3px 0 var(--spry-accent); } .tabs > input:nth-of-type(1):checked ~ .tab-panels > :nth-child(1), .tabs > input:nth-of-type(2):checked ~ .tab-panels > :nth-child(2), .tabs > input:nth-of-type(3):checked ~ .tab-panels > :nth-child(3), .tabs > input:nth-of-type(4):checked ~ .tab-panels > :nth-child(4), .tabs > input:nth-of-type(5):checked ~ .tab-panels > :nth-child(5), .tabs > input:nth-of-type(6):checked ~ .tab-panels > :nth-child(6) { display: block; } .tabs > input:focus-visible ~ .tab-list label { outline: 2px solid var(--spry-accent); outline-offset: -2px; } /* ========================================================================== 11. Components — buttons ========================================================================== */ /* Bare buttons and submit inputs get the same base look at zero specificity (:where), so .btn-* variant classes always win. */ .btn, :where(button, input[type="submit"], input[type="reset"], input[type="button"]) { display: inline-flex; align-items: center; justify-content: center; gap: 0.45em; padding: var(--spry-control-padding-y) calc(var(--spry-control-padding-x) + 0.15rem); border: var(--spry-border-width) solid var(--spry-btn-border); border-radius: var(--spry-control-radius); background: var(--spry-btn-bg); color: var(--spry-btn-fg); font-weight: 550; font-size: inherit; line-height: 1.2; text-decoration: none; white-space: nowrap; cursor: pointer; transition: background-color var(--spry-transition), border-color var(--spry-transition), color var(--spry-transition), box-shadow var(--spry-transition); } .btn:hover, :where(button, input[type="submit"], input[type="reset"], input[type="button"]):hover { background: var(--spry-btn-bg-hover); border-color: var(--spry-border-strong); } .btn:active, :where(button, input[type="submit"], input[type="reset"], input[type="button"]):active { background: var(--spry-btn-bg-active); } .btn:focus-visible, :where(button, input[type="submit"], input[type="reset"], input[type="button"]):focus-visible { outline: 2px solid var(--spry-accent); outline-offset: 2px; } .btn:disabled, .btn[aria-disabled="true"], :where(button, input[type="submit"], input[type="reset"], input[type="button"]):disabled { opacity: 0.55; cursor: not-allowed; pointer-events: none; } .btn-primary { background: var(--spry-accent); border-color: var(--spry-accent); color: var(--spry-accent-fg); } .btn-primary:hover { background: var(--spry-accent-hover); border-color: var(--spry-accent-hover); } .btn-danger { background: var(--spry-danger); border-color: var(--spry-danger); color: #fff; } .btn-danger:hover { background: color-mix(in srgb, var(--spry-danger) 85%, black); border-color: color-mix(in srgb, var(--spry-danger) 85%, black); } .btn-outline { background: transparent; border-color: var(--spry-accent); color: var(--spry-accent); } .btn-outline:hover { background: var(--spry-accent-soft); border-color: var(--spry-accent-hover); color: var(--spry-accent-hover); } .btn-ghost { background: transparent; border-color: transparent; color: var(--spry-fg); } .btn-ghost:hover { background: var(--spry-bg-alt); border-color: transparent; } .btn-link { background: none; border: 0; color: var(--spry-link); text-decoration: underline; text-underline-offset: 0.2em; padding-inline: 0.25rem; } .btn-link:hover { color: var(--spry-link-hover); } /* Size modifiers use :is() for (0,2,0) specificity so theme base restyles (scoped, (0,1,0)-ish) cannot clobber the small/large padding. */ :is(.btn, button, input[type="submit"], input[type="reset"], input[type="button"]).btn-sm { padding: 0.25rem 0.6rem; font-size: 0.85em; border-radius: var(--spry-radius-sm); } :is(.btn, button, input[type="submit"], input[type="reset"], input[type="button"]).btn-lg { padding: 0.6rem 1.2rem; font-size: 1.1em; border-radius: var(--spry-radius); } .btn-block { display: flex; width: 100%; } .btn-group { display: inline-flex; } .btn-group > .btn, .btn-group > button { border-radius: 0; margin-left: calc(-1 * var(--spry-border-width)); } .btn-group > .btn:first-child, .btn-group > button:first-child { margin-left: 0; border-start-start-radius: var(--spry-control-radius); border-end-start-radius: var(--spry-control-radius); } .btn-group > .btn:last-child, .btn-group > button:last-child { border-start-end-radius: var(--spry-control-radius); border-end-end-radius: var(--spry-control-radius); } /* ========================================================================== 12. Components — forms ========================================================================== */ input[type="text"], input[type="email"], input[type="password"], input[type="number"], input[type="search"], input[type="tel"], input[type="url"], input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"], input[type="color"], select, textarea, .input { width: 100%; padding: var(--spry-control-padding-y) var(--spry-control-padding-x); background: var(--spry-control-bg); color: var(--spry-control-fg); border: var(--spry-border-width) solid var(--spry-control-border); border-radius: var(--spry-control-radius); box-shadow: var(--spry-glass); transition: border-color var(--spry-transition), box-shadow var(--spry-transition); } textarea { min-height: 6lh; resize: vertical; } select:not([size]):not([multiple]) { width: auto; } select[multiple] { height: auto; } input[type="color"] { width: 3.5rem; padding: 0.2rem; height: 2.4rem; cursor: pointer; } input[type="search"] { border-radius: var(--spry-radius-pill); } input:hover:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]), select:hover, textarea:hover { border-color: var(--spry-border-strong); } input:focus:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]), select:focus, textarea:focus, .input:focus { outline: none; border-color: var(--spry-accent); box-shadow: var(--spry-focus-ring); } input:disabled, select:disabled, textarea:disabled { opacity: 0.55; cursor: not-allowed; background: var(--spry-bg-inset); } input[readonly], textarea[readonly] { background: var(--spry-bg-inset); } .is-invalid { border-color: var(--spry-danger) !important; } .is-invalid:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--spry-danger) 25%, transparent) !important; } .is-valid { border-color: var(--spry-success) !important; } .field { display: flex; flex-direction: column; gap: 0.35rem; } .field .label, .label { font-weight: 600; font-size: 0.92em; } .hint { font-size: 0.85em; color: var(--spry-fg-muted); } .field .is-invalid ~ .hint { color: var(--spry-danger); } fieldset { border: var(--spry-border-width) solid var(--spry-border); border-radius: var(--spry-radius); padding: 1rem 1.25rem; margin: 0 0 1rem; min-width: 0; } legend { font-weight: 600; padding-inline: 0.4rem; } /* Checkboxes & radios */ input[type="checkbox"], input[type="radio"] { width: 1.05em; height: 1.05em; flex: none; accent-color: var(--spry-accent); cursor: pointer; } .choice { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; } .choices { display: flex; flex-direction: column; align-items: flex-start; gap: 0.4rem; } .choices-inline { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 1.2rem; } /* Switch (toggle) */ .switch { display: inline-flex; align-items: center; gap: 0.6rem; cursor: pointer; } .switch input { position: absolute; opacity: 0; width: 1px; height: 1px; } .switch .track { position: relative; flex: none; width: 2.4rem; height: 1.35rem; border-radius: var(--spry-radius-pill); background: var(--spry-border-strong); transition: background-color var(--spry-transition); } .switch .track::after { content: ""; position: absolute; top: 0.175rem; left: 0.175rem; width: 1rem; height: 1rem; border-radius: 50%; background: var(--spry-surface); box-shadow: var(--spry-shadow); transition: translate var(--spry-transition); } .switch input:checked + .track { background: var(--spry-accent); } .switch input:checked + .track::after { translate: 1.05rem 0; } .switch input:focus-visible + .track { outline: 2px solid var(--spry-accent); outline-offset: 2px; } .switch input:disabled ~ * { opacity: 0.55; cursor: not-allowed; } /* ========================================================================== 13. Components — cards ========================================================================== */ .card { background: var(--spry-card-bg); border: var(--spry-border-width) solid var(--spry-card-border); border-radius: var(--spry-radius); box-shadow: var(--spry-card-shadow); backdrop-filter: var(--spry-glass); } /* Padding comes with the card, so a bare
already looks right; structural sections carry their own padding flush to the frame, so a card built from them drops the default. Both rules sit at zero specificity (:where), the same trick the reset uses — padding utilities (.p-0, .px-2, …) override them without a specificity fight. */ :where(.card) { padding: var(--spry-card-padding); } :where(.card:has(> .card-header, > .card-body, > .card-footer)) { padding: 0; } .card-flat { box-shadow: none; } .card-header, .card-body, .card-footer { padding: var(--spry-card-padding); } .card-header { border-bottom: var(--spry-border-width) solid var(--spry-card-border); background: var(--spry-bg-alt); border-radius: var(--spry-radius) var(--spry-radius) 0 0; } .card-footer { border-top: var(--spry-border-width) solid var(--spry-card-border); background: var(--spry-bg-alt); border-radius: 0 0 var(--spry-radius) var(--spry-radius); } .card-header + .card-body, .card-body + .card-footer, .card-header:first-child:last-child { margin: 0; } .card-header + .card-body { border-radius: 0; } .card-body + .card-footer { border-radius: 0 0 var(--spry-radius) var(--spry-radius); } .card-title { font-size: 1.1rem; font-weight: 650; margin: 0; font-family: var(--spry-font-heading); } .card-subtitle { font-size: 0.875em; color: var(--spry-fg-muted); margin: 0.15rem 0 0; } /* ========================================================================== 14. Components — alerts & badges ========================================================================== */ .alert { display: flex; align-items: baseline; gap: 0.6rem; padding: 0.8rem 1rem; border: var(--spry-border-width) solid var(--spry-border); border-radius: var(--spry-radius); background: var(--spry-surface); } .alert p { margin: 0; } .alert-danger { background: var(--spry-danger-soft); border-color: color-mix(in srgb, var(--spry-danger) 35%, transparent); color: var(--spry-danger); } .alert-success { background: var(--spry-success-soft); border-color: color-mix(in srgb, var(--spry-success) 35%, transparent); color: var(--spry-success); } .alert-warning { background: var(--spry-warning-soft); border-color: color-mix(in srgb, var(--spry-warning) 35%, transparent); color: var(--spry-warning); } .alert-info { background: var(--spry-info-soft); border-color: color-mix(in srgb, var(--spry-info) 35%, transparent); color: var(--spry-info); } .badge { display: inline-flex; align-items: center; gap: 0.3em; padding: 0.1em 0.65em; border-radius: var(--spry-radius-pill); font-size: 0.78em; font-weight: 600; line-height: 1.6; background: var(--spry-accent-soft); color: var(--spry-accent); white-space: nowrap; } .badge-danger { background: var(--spry-danger-soft); color: var(--spry-danger); } .badge-success { background: var(--spry-success-soft); color: var(--spry-success); } .badge-warning { background: var(--spry-warning-soft); color: var(--spry-warning); } .badge-info { background: var(--spry-info-soft); color: var(--spry-info); } .badge-muted { background: var(--spry-bg-alt); color: var(--spry-fg-muted); border: var(--spry-border-width) solid var(--spry-border); } /* ========================================================================== 15. Components — tables ========================================================================== */ .table-wrap { overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: 0.95em; background: var(--spry-surface); border: var(--spry-border-width) solid var(--spry-border); border-radius: var(--spry-radius); } caption { caption-side: bottom; text-align: left; color: var(--spry-fg-muted); font-size: 0.85em; padding-top: 0.5rem; } th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: var(--spry-border-width) solid var(--spry-border); vertical-align: top; } thead th { background: var(--spry-table-header-bg); font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.06em; color: var(--spry-fg-muted); border-bottom-width: 2px; } tbody tr:last-child td, tbody tr:last-child th { border-bottom: 0; } .table-striped tbody tr:nth-child(even) { background: color-mix(in srgb, var(--spry-bg-alt) 55%, transparent); } .table-hover tbody tr:hover { background: var(--spry-table-hover); } .table-compact th, .table-compact td { padding: 0.3rem 0.55rem; } .table-borderless, .table-borderless th, .table-borderless td { border: 0; background: transparent; } /* ========================================================================== 16. Components — progress, meter, spinners ========================================================================== */ progress { appearance: none; -webkit-appearance: none; border: 0; width: 100%; height: 0.8rem; border-radius: var(--spry-radius-pill); overflow: hidden; background: var(--spry-progress-bg); } progress::-webkit-progress-bar { background: var(--spry-progress-bg); border-radius: var(--spry-radius-pill); } progress::-webkit-progress-value { background: var(--spry-progress-fg); border-radius: var(--spry-radius-pill); transition: width 200ms ease; } progress::-moz-progress-bar { background: var(--spry-progress-fg); border-radius: var(--spry-radius-pill); } /* Indeterminate: a GTK-style "pulse" block bouncing back and forth. Chromium ignores author styles on the indeterminate value pseudo entirely, so the slug lives on the ELEMENT's background (both engines animate element backgrounds reliably) and the track pseudo is made transparent so it doesn't paint over it. */ /* html-prefixed so themed plain `progress { background }` rules (equal or lower specificity, loaded later) can never override the indeterminate state. The slug is a true stadium — rectangle + two half-disc caps in three equally-sized layers (so percentage positions move them in lockstep); a single radial would be a lens, which reads pointy. */ html progress:indeterminate { background: radial-gradient(var(--spry-pulse-cap) var(--spry-pulse-cap) at var(--spry-pulse-cap) 50%, var(--spry-progress-fg) 97%, transparent 100%) 0% 50% / 25% 100% no-repeat, radial-gradient(var(--spry-pulse-cap) var(--spry-pulse-cap) at calc(100% - var(--spry-pulse-cap)) 50%, var(--spry-progress-fg) 97%, transparent 100%) 0% 50% / 25% 100% no-repeat, linear-gradient(90deg, transparent 0 var(--spry-pulse-cap), var(--spry-progress-fg) var(--spry-pulse-cap) calc(100% - var(--spry-pulse-cap)), transparent calc(100% - var(--spry-pulse-cap))) 0% 50% / 25% 100% no-repeat, var(--spry-progress-bg); animation: spry-pulse 1.4s linear infinite alternate; } html progress:indeterminate::-webkit-progress-bar, progress:indeterminate::-webkit-progress-bar { background: transparent !important; } html progress:indeterminate::-moz-progress-bar, progress:indeterminate::-moz-progress-bar { background: transparent !important; } .progress-thin { height: 0.4rem; } meter { width: 100%; height: 1.1rem; } .spinner { display: inline-block; width: 1.5rem; height: 1.5rem; border-radius: 50%; border: 0.18em solid var(--spry-border); border-top-color: var(--spry-accent); animation: spry-spin 0.9s linear infinite; vertical-align: -0.25em; } .spinner-sm { width: 1rem; height: 1rem; border-width: 0.14em; } .spinner-lg { width: 2.5rem; height: 2.5rem; border-width: 0.12em; } @keyframes spry-spin { to { transform: rotate(1turn); } } @keyframes spry-stripes { to { background-position: 1.7rem 0; } } @keyframes spry-pulse { /* three slug layers + the track — identical percentage positions keep the stadium parts glued together */ from { background-position: 0% 50%, 0% 50%, 0% 50%, 0 0; } to { background-position: 100% 50%, 100% 50%, 100% 50%, 0 0; } } /* ========================================================================== 17. Components — dialogs -------------------------------------------------------------------------- Native (open non-modally with the open attribute, or modally with dialog.showModal()). Dialog-like windows without any JavaScript: use
with a popovertarget button (see the popover section). ========================================================================== */ dialog { width: min(92vw, 36rem); max-height: min(85vh, 100%); margin: auto; /* re-assert UA centering — the reset above zeroes margins */ padding: 0; border: var(--spry-border-width) solid var(--spry-popover-border); border-radius: var(--spry-radius-lg); background: var(--spry-popover-bg); color: var(--spry-fg); box-shadow: var(--spry-popover-shadow); backdrop-filter: var(--spry-glass); } dialog.sm, .dialog-sm { width: min(92vw, 24rem); } dialog.lg, .dialog-lg { width: min(92vw, 56rem); } /* A dialog without structural parts (titlebar/body/footer/card sections) would render its content flush against the frame, so pad it like a popover. Structural dialogs keep padding: 0 above. */ dialog:not(:has(.dialog-titlebar, .dialog-body, .dialog-footer, .card-header, .card-body, .card-footer)) { padding: var(--spry-popover-padding); } dialog::backdrop { background: var(--spry-backdrop); } .dialog-titlebar { display: flex; align-items: center; gap: 0.5rem; padding: 0.55rem 0.65rem 0.55rem 1rem; background: var(--spry-bg-alt); border-bottom: var(--spry-border-width) solid var(--spry-border); border-radius: var(--spry-radius-lg) var(--spry-radius-lg) 0 0; user-select: none; } .dialog-title { font-weight: 650; font-size: 0.95rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .dialog-close { display: inline-flex; align-items: center; justify-content: center; width: 1.7rem; height: 1.7rem; flex: none; padding: 0; border: 0; border-radius: var(--spry-radius-sm); background: transparent; color: var(--spry-fg-muted); font-size: 1.1rem; line-height: 1; cursor: pointer; transition: background-color var(--spry-transition), color var(--spry-transition); } .dialog-close:hover { background: var(--spry-danger-soft); color: var(--spry-danger); } .dialog-close:focus-visible { outline: 2px solid var(--spry-accent); outline-offset: 1px; } .dialog-body { padding: 1.25rem; } .dialog-footer { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 0.5rem; padding: 0.8rem 1.25rem; border-top: var(--spry-border-width) solid var(--spry-border); background: var(--spry-bg-alt); border-radius: 0 0 var(--spry-radius-lg) var(--spry-radius-lg); } /* Open/close animation. Two-state pattern: the closed state carries the "hidden" values and the EXIT timing; the open state carries the ENTRY timing — each direction reads the transition of the state it lands in. @starting-style makes even a dialog with a static open attribute fade in. Browsers without support simply don't animate. */ dialog, [popover] { opacity: 0; transform: var(--spry-dialog-in); transition: opacity var(--spry-dialog-close-duration) var(--spry-dialog-close-ease), transform var(--spry-dialog-close-duration) var(--spry-dialog-close-ease), display var(--spry-dialog-close-duration) allow-discrete, overlay var(--spry-dialog-close-duration) allow-discrete; } dialog[open], :popover-open { opacity: 1; transform: none; transition: opacity var(--spry-dialog-duration) var(--spry-dialog-ease), transform var(--spry-dialog-duration) var(--spry-dialog-ease), display var(--spry-dialog-duration) allow-discrete, overlay var(--spry-dialog-duration) allow-discrete; @starting-style { opacity: 0; transform: var(--spry-dialog-in); } } dialog::backdrop, [popover]::backdrop { opacity: 0; transition: opacity var(--spry-dialog-close-duration) var(--spry-dialog-close-ease), display var(--spry-dialog-close-duration) allow-discrete, overlay var(--spry-dialog-close-duration) allow-discrete; } dialog[open]::backdrop, :popover-open::backdrop { opacity: 1; transition: opacity var(--spry-dialog-duration) var(--spry-dialog-ease), display var(--spry-dialog-duration) allow-discrete, overlay var(--spry-dialog-duration) allow-discrete; @starting-style { opacity: 0; } } /* ========================================================================== 18. Components — popovers & menus (no JavaScript) -------------------------------------------------------------------------- Any element with the popover attribute opens/closes via a button with popovertarget="id" (or the light-dismiss "auto" behavior by default). ========================================================================== */ [popover] { /* re-assert the UA popover centering model (inset 0 + margin auto); the reset above zeroes margins, which parks popovers in the top-left */ margin: auto; width: min(92vw, 26rem); padding: var(--spry-popover-padding); border: var(--spry-border-width) solid var(--spry-popover-border); border-radius: var(--spry-radius-lg); background: var(--spry-popover-bg); color: var(--spry-fg); box-shadow: var(--spry-popover-shadow); backdrop-filter: var(--spry-glass); } [popover]::backdrop { background: var(--spry-backdrop); } /* A popover built from dialog layout parts is a "window": drop the generic popover padding so the titlebar/body/footer run flush to the frame. */ [popover]:has(> .dialog-titlebar, > .dialog-body, > .dialog-footer) { padding: 0; } /* Adwaita-style anchored popovers: give the trigger `.anchor` and the popover `.popover-anchor`, and set the same --anchor-name custom property on both. The popover drops below the trigger and flips at the viewport edges; its backdrop is transparent (menus light-dismiss without a modal scrim). Without anchor positioning support, popovers stay centered. */ .anchor { anchor-name: var(--anchor-name, --spry-anchor); } @supports (position-area: bottom) { .popover-anchor { position-anchor: var(--anchor-name, --spry-anchor); position-area: bottom; position-try: flip-block, flip-inline; margin: 0.4rem 0 0; width: auto; max-width: min(92vw, 26rem); } .popover-anchor::backdrop { background: transparent; } } .popover-title { font-weight: 650; margin-bottom: 0.4rem; } .popover-menu { padding: 0.4rem; min-width: 12rem; } /* Only set display in the open state: an author `display` on a closed [popover] overrides the UA's display:none and the menu never hides. */ .popover-menu:popover-open { display: flex; flex-direction: column; gap: 1px; } .popover-menu a, .popover-menu button { display: flex; align-items: center; gap: 0.6rem; width: 100%; padding: 0.45rem 0.7rem; border: 0; border-radius: var(--spry-radius-sm); background: none; color: var(--spry-fg); text-align: left; text-decoration: none; font-weight: 450; } .popover-menu a:hover, .popover-menu button:hover { background: var(--spry-accent-soft); color: var(--spry-accent); } .popover-footer { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 0.9rem; } /* ========================================================================== 19. Components — expanders (details/summary) ========================================================================== */ details { border: var(--spry-border-width) solid var(--spry-border); border-radius: var(--spry-radius); background: var(--spry-surface); } details + details { margin-top: 0.6rem; } summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.7rem 1rem; border-radius: var(--spry-radius); font-weight: 600; cursor: pointer; list-style: none; user-select: none; transition: background-color var(--spry-transition); } summary:hover { background: var(--spry-bg-alt); } summary::-webkit-details-marker { display: none; } summary::before { content: ""; width: 0.45rem; height: 0.45rem; flex: none; margin-right: 0.15rem; border-right: 2px solid var(--spry-fg-muted); border-bottom: 2px solid var(--spry-fg-muted); rotate: -45deg; transition: rotate var(--spry-transition); } details[open] > summary::before { rotate: 45deg; } details[open] > summary { border-bottom: var(--spry-border-width) solid var(--spry-border); border-radius: var(--spry-radius) var(--spry-radius) 0 0; } details > :not(summary):not(:last-child) { padding: 1rem 1rem 0; } details > :not(summary):last-child { padding: 1rem; } details > *:not(summary) { margin: 0; } .expander-group > details + details { margin-top: calc(-1 * var(--spry-border-width)); border-radius: 0; } .expander-group > details:first-child { border-radius: var(--spry-radius) var(--spry-radius) 0 0; } .expander-group > details:last-child { border-radius: 0 0 var(--spry-radius) var(--spry-radius); } .expander-group > details:only-child { border-radius: var(--spry-radius); } .expander-group > details[open]:not(:last-child) > summary { border-bottom-color: transparent; } /* ========================================================================== 20. Components — hero, avatars, OTP, drawers, pagination ========================================================================== */ .hero { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--spry-space-3); padding: calc(var(--spry-space-5) * 1.4) var(--spry-space-3); background: radial-gradient(60rem 24rem at 50% -8rem, var(--spry-accent-soft), transparent 70%), var(--spry-bg-alt); border-block: var(--spry-border-width) solid var(--spry-border); } .hero-compact { padding-block: var(--spry-space-4); } .hero-eyebrow { font-size: 0.78rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--spry-accent); } .hero-title { font-size: clamp(2rem, 1.2rem + 3vw, 3.25rem); max-width: 24ch; margin: 0; } .hero-sub { max-width: 52ch; color: var(--spry-fg-muted); margin: 0; } .hero-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--spry-space-2); } .avatar { display: inline-flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; flex: none; border-radius: 50%; background: var(--spry-accent-soft); color: var(--spry-accent); font-weight: 650; font-size: 0.9rem; letter-spacing: 0.02em; overflow: hidden; user-select: none; } .avatar img { width: 100%; height: 100%; object-fit: cover; } .avatar-sm { width: 1.75rem; height: 1.75rem; font-size: 0.68rem; } .avatar-lg { width: 3.5rem; height: 3.5rem; font-size: 1.2rem; } .avatar-group { display: inline-flex; align-items: center; } .avatar-group .avatar { border: 2px solid var(--spry-bg); margin-left: -0.65rem; box-shadow: none; } .avatar-group .avatar:first-child { margin-left: 0; } /* OTP / verification-code input. One character per field; moving focus automatically takes one line of JS in apps that want it. */ .otp { display: inline-flex; align-items: center; gap: var(--spry-space-2); } .otp input { width: 2.9rem; height: 3.4rem; padding: 0; text-align: center; font-family: var(--spry-font-mono); font-size: 1.35rem; line-height: 1; } .otp-separator { color: var(--spry-fg-muted); font-weight: 600; } /* Drawers: popover-based side panels, no JavaScript. The slide direction comes from the same --spry-dialog-in variable the dialog animation reads. */ .drawer-left, .drawer-right { width: min(22rem, 90vw); height: 100dvh; padding: 0; margin: 0; border-radius: 0; border-block: 0; box-shadow: var(--spry-shadow-lg); } .drawer-left { inset: 0 auto 0 0; border-right-width: var(--spry-border-width); --spry-dialog-in: translateX(-100%); } .drawer-right { inset: 0 0 0 auto; border-left-width: var(--spry-border-width); --spry-dialog-in: translateX(100%); } .drawer-left .dialog-titlebar, .drawer-right .dialog-titlebar { border-radius: 0; } .drawer-left .dialog-footer, .drawer-right .dialog-footer { border-radius: 0; } /* Pagination */ .pagination { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; list-style: none; padding: 0; margin: 0; } .pagination a, .pagination span { display: inline-flex; align-items: center; justify-content: center; min-width: 2.2rem; height: 2.2rem; padding: 0 0.55rem; border: var(--spry-border-width) solid transparent; border-radius: var(--spry-radius-sm); color: var(--spry-fg); text-decoration: none; font-weight: 500; transition: background-color var(--spry-transition), color var(--spry-transition); } .pagination a:hover { background: var(--spry-bg-alt); color: var(--spry-accent); } .pagination [aria-current="page"] { background: var(--spry-accent); color: var(--spry-accent-fg); font-weight: 650; } .pagination [aria-disabled="true"] { opacity: 0.45; pointer-events: none; }