diff --git a/.githooks/check-freeze.sh b/.githooks/check-freeze.sh index a5ddd16231..6c39d76895 100755 --- a/.githooks/check-freeze.sh +++ b/.githooks/check-freeze.sh @@ -39,6 +39,19 @@ while IFS= read -r qmd; do [ -n "$qmd" ] || continue freeze_rel="_freeze/${qmd%.qmd}/execute-results/html.json" + # Skip when the freeze file is intentionally removed in this change: the + # page no longer executes code (e.g. converted to the markdown engine), so + # its freeze entry is vestigial and correctly deleted, not stale. + if [ "$MODE" = "commit" ]; then + if git diff --cached --diff-filter=D --name-only 2>/dev/null | grep -Fxq "$freeze_rel"; then + continue + fi + else + if git diff "${REMOTE}..HEAD" --diff-filter=D --name-only 2>/dev/null | grep -Fxq "$freeze_rel"; then + continue + fi + fi + # Read freeze JSON from git objects, not working tree, to avoid false passes # when the freeze file is updated on disk but not yet staged. if [ "$MODE" = "commit" ]; then diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 1e9ab01d9f..c2eca77709 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -227,9 +227,14 @@ jobs: 'docs/extensions/listings/custom-formats.yml': 'docs/extensions/index.html', 'docs/extensions/listings/revealjs-formats.yml': 'docs/extensions/index.html', 'docs/extensions/listings/revealjs.yml': 'docs/extensions/index.html', - // Gallery data files → gallery index - 'docs/gallery/carousel.yml': 'docs/gallery/index.html', - 'docs/gallery/gallery.yml': 'docs/gallery/index.html', + // Gallery data files → gallery pages + 'docs/gallery/data/featured.yml': 'docs/gallery/index.html', + 'docs/gallery/data/reports-analytics.yml': 'docs/gallery/reports-analytics.html', + 'docs/gallery/data/dashboards-monitoring.yml': 'docs/gallery/dashboards-monitoring.html', + 'docs/gallery/data/research-scholarship.yml': 'docs/gallery/research-scholarship.html', + 'docs/gallery/data/teaching-learning.yml': 'docs/gallery/teaching-learning.html', + 'docs/gallery/data/software-documentation.yml': 'docs/gallery/software-documentation.html', + 'docs/gallery/data/personal-community.yml': 'docs/gallery/personal-community.html', // Guide listing → guide index 'docs/guide/guide.yml': 'docs/guide/index.html', // Download data → download index diff --git a/_axe-baseline.json b/_axe-baseline.json index d35e9a9ec7..22b845c833 100644 --- a/_axe-baseline.json +++ b/_axe-baseline.json @@ -692,7 +692,7 @@ "note": "third-party: leaflet, no upstream issue. leaflet.js gives each marker icon alt=\"\" and also tabindex=\"0\", so it is presentational and focusable at the same time. axe adds the random htmlwidget ID because this page has three maps; update the ID when the page is refrozen." }, { - "signature": "color-contrast :: #f8f8f8 on #dddddd", + "signature": "color-contrast :: #e8eef4 on #dddddd", "pages": ["docs/interactive/index.html", "docs/interactive/widgets/jupyter.html"], "impact": "serious", "note": "third-party: leaflet, no upstream issue. The text color of the leaflet attribution control does not change in the Quarto dark theme, because leaflet.css has no dark theme." @@ -710,7 +710,7 @@ "note": "third-party: leaflet, no upstream issue. The ESDIS link in the leaflet tile attribution is #0078a8 on the #343a40 page text, 2.32:1. Scoped to these leaflet pages because the signature also matches other low-contrast links." }, { - "signature": "color-contrast :: #000000 on #151515", + "signature": "color-contrast :: #000000 on #161f28", "pages": ["docs/interactive/widgets/htmlwidgets.html"], "impact": "serious", "note": "third-party: dygraphs, rstudio/dygraphs#241. dygraph.js 1.1.1 sets the axis-label color in an inline style, so CSS cannot change it. Clears when the R dygraphs package bundles dygraph.js 2.0 or later. Scoped to this page because the same signature on penguins-preview.html is a Quarto defect." @@ -758,7 +758,7 @@ "note": "third-party: knitr, yihui/knitr#1747. kable() leaves the corner cell above the row names as an empty on purpose. yihui/knitr#2500 added scope to the other header cells but kept this cell empty." }, { - "signature": "color-contrast :: #f8f8f8 on #ffffff", + "signature": "color-contrast :: #e8eef4 on #ffffff", "pages": ["docs/interactive/ojs/examples/arquero.html", "docs/interactive/ojs/libraries.html"], "impact": "serious", "note": "third-party: Arquero, no upstream issue. The Arquero table viewer sets background: #fff on its sticky header in an inline style, so in dark mode the header text gets the light theme text color. Scoped to the two pages with the viewer." diff --git a/_extensions/quartoorg/_extension.yml b/_extensions/quartoorg/_extension.yml index f631515c85..43bc17f5be 100644 --- a/_extensions/quartoorg/_extension.yml +++ b/_extensions/quartoorg/_extension.yml @@ -7,8 +7,8 @@ contributes: html: toc: true theme: - light: [cosmo, scss/quartoorg.scss] - dark: [cosmo, scss/quartoorg-dark.scss] + light: [cosmo, scss/quartoorg.scss, scss/quartoorg-palette.scss] + dark: [cosmo, scss/quartoorg-dark.scss, scss/quartoorg-palette.scss] code-copy: true code-overflow: wrap include-in-header: diff --git a/_extensions/quartoorg/scss/partials/_announcement.scss b/_extensions/quartoorg/scss/partials/_announcement.scss index 853126dc98..3e0beb4fde 100644 --- a/_extensions/quartoorg/scss/partials/_announcement.scss +++ b/_extensions/quartoorg/scss/partials/_announcement.scss @@ -2,10 +2,10 @@ padding: 1em; font-size: 1em; font-weight: bold; - color: $white; - background-color: #447099; + color: var(--qorg-on-brand); + background-color: var(--qorg-brand); } #quarto-announcement a { - color: $white; + color: inherit; } diff --git a/_extensions/quartoorg/scss/partials/_buttons.scss b/_extensions/quartoorg/scss/partials/_buttons.scss index 8c58501aae..38ed00c23b 100644 --- a/_extensions/quartoorg/scss/partials/_buttons.scss +++ b/_extensions/quartoorg/scss/partials/_buttons.scss @@ -1,16 +1,34 @@ -.btn-action-primary { - color: white; - background-color: #447099; +// Action buttons (home hero, downloads). They set Bootstrap's --bs-btn-* +// variables, so .btn applies the colours in every state and the focus ring +// comes from _focus.scss. 44px minimum height for the target size. +.btn.btn-action { + min-width: 165px; + min-height: 44px; + border-width: 2px; + border-radius: 30px; } -.btn-action-primary:hover { - background-color: #305775; - color: white; +.btn.btn-action-primary { + --bs-btn-color: var(--qorg-on-brand); + --bs-btn-bg: var(--qorg-brand); + --bs-btn-border-color: var(--qorg-brand); + --bs-btn-hover-color: var(--qorg-on-brand); + --bs-btn-hover-bg: var(--qorg-brand-hover); + --bs-btn-hover-border-color: var(--qorg-brand-hover); + --bs-btn-active-color: var(--qorg-on-brand); + --bs-btn-active-bg: var(--qorg-brand-hover); + --bs-btn-active-border-color: var(--qorg-brand-hover); } -.btn-action { - min-width: 165px; - border-radius: 30px; - border: none; +// Outline button next to the primary one: strong line border (3:1). +.btn.btn-action.btn-secondary { + --bs-btn-color: var(--qorg-ink); + --bs-btn-bg: transparent; + --bs-btn-border-color: var(--qorg-line-strong); + --bs-btn-hover-color: var(--qorg-link); + --bs-btn-hover-bg: transparent; + --bs-btn-hover-border-color: var(--qorg-link); + --bs-btn-active-color: var(--qorg-link); + --bs-btn-active-bg: transparent; + --bs-btn-active-border-color: var(--qorg-link); } - diff --git a/_extensions/quartoorg/scss/partials/_cards.scss b/_extensions/quartoorg/scss/partials/_cards.scss index fc18814ea2..743f920355 100644 --- a/_extensions/quartoorg/scss/partials/_cards.scss +++ b/_extensions/quartoorg/scss/partials/_cards.scss @@ -1,105 +1,60 @@ - -@media (max-width: 575px) { - -.link-cards .card { - margin-bottom: 20px; - margin-right: 35px; -} - -} - -@media (min-width: 576px) { +// Link cards from assets/ejs/links.ejs (Guide and Reference listings): +// card shell with the section bar, a glyph box, and the h2 title kept for +// heading order. .link-cards { - display: flex; - flex-direction: row; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; } .link-cards .card { - width: 190px; - margin: 0 20px 12px 0; + @include qorg-card-shell; } - +.link-cards .card-body { + padding: 1rem 1rem 0.9rem; } +.link-cards .card-body > p { + margin-bottom: 0.45rem; +} -.link-cards .card { - border: none; - padding: 0; +.link-cards .card-glyph { + @include qorg-glyph-box; } -/* Card titles are h2 for heading order; keep the old h4 look */ .link-cards .card-title h2 { - font-size: 1.25rem; - font-weight: 400; - line-height: 1.2; - opacity: 0.9; - margin-top: 0; - margin-bottom: 0.5rem; + margin: 0 0 0.1rem; padding-bottom: 0; border-bottom: none; -} - -.link-cards .card-title p { - margin-bottom: 0; -} - -.link-cards .card-subtitle p { + font-size: 1.1rem; + font-weight: 700; line-height: 1.2; - opacity: 0.9; - margin-bottom: 0.5rem; } .link-cards .card-subtitle { - margin-bottom: 0.7rem; + margin-bottom: 0.6rem; + font-size: 0.88rem; + line-height: 1.35; } -.link-cards .card-body { - padding: 0.5rem; - padding-left: 0.1rem; +.link-cards .card-subtitle p { + margin-bottom: 0; } .link-cards .card-body ul { margin-bottom: 0; padding-left: 0; list-style-type: none; + font-size: 0.95rem; } .link-cards .card-body ul a { + display: inline-block; + min-height: 24px; text-decoration: none; -} - -.link-cards .card-body ul li { - padding-bottom: 0.2rem; -} - -.card .source-code { - margin-top: 3px; -} - -.carousel.card { - font-size: 16px; - padding-top: 2em; + &:hover { + text-decoration: underline; + } } - -.carousel.card a { - text-decoration: none; -} - -.carousel img { - width: 70%; - margin-bottom: 110px; -} - -.carousel .carousel-control-prev-icon, -.carousel .carousel-control-next-icon { - margin-bottom: 110px; -} - - -.gallery-category { - column-gap: 10px; -} - diff --git a/_extensions/quartoorg/scss/partials/_chrome.scss b/_extensions/quartoorg/scss/partials/_chrome.scss new file mode 100644 index 0000000000..b35a257a69 --- /dev/null +++ b/_extensions/quartoorg/scss/partials/_chrome.scss @@ -0,0 +1,174 @@ +// Shared page chrome on Quarto's own markup: active navbar item, sidebar, +// breadcrumb label, title block, section headings and the table of contents. +// Colours come from the page section (--sec, --sec-ink, _sections.scss). + +// Active navbar item: bold, with a 3px bar in the section colour. +#quarto-header .navbar-nav .nav-link.active { + font-weight: 700; + box-shadow: inset 0 -3px 0 var(--sec); +} + +// Active sidebar entry: bold body ink on a tint of the section colour. The +// padding moves into a negative margin, so the text stays where it was. +// #quarto-sidebarnav-toggle holds the copy of the sidebar (see below). +#quarto-sidebar .sidebar-link.active, +#quarto-sidebarnav-toggle .sidebar-link.active { + margin-inline: -0.5rem; + padding-inline: 0.5rem; + border-radius: var(--qorg-radius-card); + background: color-mix(in srgb, var(--sec) 16%, transparent); + color: var(--qorg-ink); + font-weight: 700; +} + +// Breadcrumb label, above the title on wide screens and in the secondary +// bar on narrow ones: small, bold, upper case, in the section ink. The +// current page comes last, in muted text. +.quarto-page-breadcrumbs { + .breadcrumb { + --bs-breadcrumb-divider: "/"; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + } + + .breadcrumb-item + .breadcrumb-item::before { + color: var(--qorg-muted); + } + + .breadcrumb-item a { + color: var(--sec-ink); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + .breadcrumb-item:last-child a { + color: var(--qorg-muted); + } +} + +// Quarto colours the secondary bar breadcrumb grey with its own selectors. +.quarto-secondary-nav nav.quarto-page-breadcrumbs .breadcrumb-item a { + color: var(--sec-ink); +} + +.quarto-secondary-nav nav.quarto-page-breadcrumbs .breadcrumb-item:last-child a { + color: var(--qorg-muted); +} + +// Title in 800, with the subtitle as a muted lede under it. +#title-block-header .quarto-title .title { + font-weight: 800; +} + +#title-block-header p.subtitle { + color: var(--qorg-muted); + font-size: 1.15rem; +} + +// Section headings in the page body: left bar, no bottom rule. The home +// page and the gallery have their own headings, and link card titles are h2 +// in a section too. The Extensions listing moves its h2 next to the filter, +// out of its section. +body:not(.quarto-home):not(.gallery) main.content section.level2:not(.card-title) > h2, +.listing-actions-group > h2 { + @include qorg-bar-heading; + padding-bottom: 0; +} + +// Upper case label: the table of contents heading, and the toggle titles. +@mixin qorg-toc-label { + color: var(--qorg-muted); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +// Table of contents as a floating panel. Quarto already keeps the margin +// sidebar sticky. +#quarto-margin-sidebar nav#TOC { + @include qorg-float-panel; + padding: 0.8rem 0.6rem 0.7rem; + + > h2 { + @include qorg-toc-label; + margin: 0 0 0.4rem 0.45rem; + } +} + +// When page content scrolls under a side column, quarto.js swaps it for a +// toggle that opens a copy of it, outside the column. The ids outrank +// Quarto's #quarto-content .quarto-sidebar-toggle rules. +#quarto-content #quarto-toc-toggle, +#quarto-content #quarto-sidebarnav-toggle { + @include qorg-float-panel; + + .quarto-sidebar-toggle-title { + @include qorg-toc-label; + margin: 0; + padding: 0.55rem 0.8rem 0.5rem 1.05rem; + border-bottom: 0; + text-align: left; + } + + .quarto-sidebar-toggle-icon { + margin-right: 0; + color: var(--qorg-muted); + } + + .quarto-sidebar-toggle-contents { + padding: 0 0.6rem; + background: transparent; + } + + &.expanded .quarto-sidebar-toggle-contents { + padding-bottom: 0.7rem; + } + + nav#TOC > h2 { + display: none; + } +} + +// The sidebar copy starts with the space kept for the sidebar title; the +// toggle title already fills it. The list's Bootstrap mt-1 is !important. +#quarto-sidebarnav-toggle .sidebar-menu-container { + padding-top: 0; + + > ul:first-child { + margin-top: 0 !important; + } +} + +#quarto-margin-sidebar nav#TOC, +#quarto-toc-toggle nav#TOC { + ul > li > a { + display: block; + min-height: 24px; + padding: 0.2rem 0.5rem; + border-left: 0; + border-radius: 8px; + } + + ul ul > li > a { + padding-left: 1.2rem; + } + + // Quarto sets the active entry colour with !important. + ul > li > a.active { + background: color-mix(in srgb, var(--sec) 16%, transparent); + color: var(--qorg-ink) !important; + font-weight: 700; + } + + .toc-actions { + margin-top: 0.5rem; + padding-top: 0.5rem; + border-top: 1px solid var(--qorg-line); + } +} diff --git a/_extensions/quartoorg/scss/partials/_color-box.scss b/_extensions/quartoorg/scss/partials/_color-box.scss index 44d484cfe4..35b55abe38 100644 --- a/_extensions/quartoorg/scss/partials/_color-box.scss +++ b/_extensions/quartoorg/scss/partials/_color-box.scss @@ -8,5 +8,5 @@ span.color-box { width: 1em; /* Width of the color box */ height: 1em; /* Height of the color box */ margin-right: 0.2em; /* Space between box and text */ - border: 1px solid #000; /* Border color */ + border: 1px solid var(--qorg-ink); /* Outline that shows in both modes */ } diff --git a/_extensions/quartoorg/scss/partials/_content.scss b/_extensions/quartoorg/scss/partials/_content.scss index de12ee0618..13ae553517 100644 --- a/_extensions/quartoorg/scss/partials/_content.scss +++ b/_extensions/quartoorg/scss/partials/_content.scss @@ -22,11 +22,7 @@ .document-example .citation { - color: blue; -} - -body.quarto-dark .document-example .citation { - color: var(--bs-link-color); + color: var(--qorg-link); } .trademark { @@ -50,9 +46,12 @@ img.ui-icon { box-sizing: content-box; } -// Quarto's title-block.scss sets `.quarto-category { opacity: 0.6 }`. -// That drops the category chips below 4.5:1 (2.77 light, 3.43 dark). -.quarto-title .quarto-categories .quarto-category { +// Category chips in title blocks and listings. Quarto's title-block.scss +// sets `.quarto-category { opacity: 0.6 }`, which drops the chips below 4.5:1. +// Gallery cards size their own chips (_gallery.scss). +.quarto-title .quarto-categories .quarto-category, +.listing-categories:not(.gal-cats) .listing-category { + @include qorg-chip; opacity: 1; } @@ -76,3 +75,43 @@ pre.sourceCode.visually-hidden { .pymd-comment { font-weight: 1000; } + +// Tables: 2px header rule and 1px row rules, both in the strong line. +// Quarto draws the outer rules on .table and the cells inherit that colour. +.table, +.table > thead { + border-color: var(--qorg-line-strong); +} + +.table > thead > tr > * { + border-bottom: 2px solid var(--qorg-line-strong); +} + +// Code blocks: code surface with a line border and an 8px radius. +div.sourceCode { + border-color: var(--qorg-line); + border-radius: 8px; +} + +// Listing filter: one rounded field with the strong line border (3:1). +.input-group.quarto-listing-filter { + > .input-group-text, + > .form-control { + border-color: var(--qorg-line-strong); + background-color: var(--qorg-panel); + } + + > .input-group-text { + border-radius: var(--qorg-radius-chip) 0 0 var(--qorg-radius-chip); + } + + > .form-control { + border-radius: 0 var(--qorg-radius-chip) var(--qorg-radius-chip) 0; + } +} + +// Search field in the search overlay. +.aa-DetachedFormContainer .aa-Form { + border: 1px solid var(--qorg-line-strong); + border-radius: var(--qorg-radius-chip); +} diff --git a/_extensions/quartoorg/scss/partials/_dark-rules.scss b/_extensions/quartoorg/scss/partials/_dark-rules.scss index efcdb87a92..5cabcf97b1 100644 --- a/_extensions/quartoorg/scss/partials/_dark-rules.scss +++ b/_extensions/quartoorg/scss/partials/_dark-rules.scss @@ -1,67 +1,9 @@ - -/*Homepage custom css overrides */ - -/* homepage hero link accessibiility hack to make contrast 4.5:1 */ - -.hero-banner a:not(.btn) { -color: #D1DBE5; -} - -.hero-banner .btn-action-primary, .get-started .btn-action-primary { - background-color: #f8f8f8; - border: 1px solid #f8f8f8; - color: $dark; -} - -.hero-banner .btn-action-primary:hover, .get-started .btn-action-primary:hover { - background-color: #D1DBE5; - border: 1px solid #f8f8f8; - color: $dark; -} - -.hero-banner .btn-secondary { - background-color: transparent; - border: 1px solid #f8f8f8; - color: #f8f8f8; -} - -.hero-banner .btn-secondary:hover { - background-color: #305775; -} - -.hello-quarto-banner .nav-pills .nav-link.active, .nav-pills .show>.nav-link { - border: none; - border-bottom: 2px solid #ffffff !important; - color: #ffffff !important; - background-color: transparent; -} - -.hello-quarto-banner .nav-link { - color: #ffffff !important; -} - -.bg-color { - background-color: $body-bg; -} - -.hello-output { - border: 1px solid $gray-700; -} - - -body.quarto-light .alt-background { - background-color: #17212B; - border-top: 1px solid #dee2e6; - border-bottom: 1px solid #dee2e6; -} - - .layout-example { background: $gray-700; } .code-with-filename .code-with-filename-file pre { - color: #ddd; + color: var(--qorg-ink); } pre code { @@ -89,9 +31,3 @@ h6 code { details > summary { color: $text-muted; } - -/*Footer Posit logo color for dark mode*/ - -img.footer-logo { - color: #ffffff; -} diff --git a/_extensions/quartoorg/scss/partials/_embeds.scss b/_extensions/quartoorg/scss/partials/_embeds.scss index e68ac90369..29ee94ec0f 100644 --- a/_extensions/quartoorg/scss/partials/_embeds.scss +++ b/_extensions/quartoorg/scss/partials/_embeds.scss @@ -6,7 +6,7 @@ iframe.reveal-demo { .slide-deck { - border: 3px solid #dee2e6; + border: 3px solid var(--qorg-line); width: 100%; height: 475px; } diff --git a/_extensions/quartoorg/scss/partials/_figures.scss b/_extensions/quartoorg/scss/partials/_figures.scss index ede1358089..6e428b574a 100644 --- a/_extensions/quartoorg/scss/partials/_figures.scss +++ b/_extensions/quartoorg/scss/partials/_figures.scss @@ -20,6 +20,6 @@ } .illustration { - border: 1px solid #dee2e6; + border: 1px solid var(--qorg-line); } diff --git a/_extensions/quartoorg/scss/partials/_focus.scss b/_extensions/quartoorg/scss/partials/_focus.scss new file mode 100644 index 0000000000..ddffd971b0 --- /dev/null +++ b/_extensions/quartoorg/scss/partials/_focus.scss @@ -0,0 +1,42 @@ +// Site-wide focus ring: 2px solid, 2px away from the control, 3:1 or more on +// every token surface. Components don't add their own focus styles. + +// Bootstrap and Quarto remove the outline on these controls with selectors +// stronger than :focus-visible, and some draw a pale box-shadow instead. +// Matching their specificity here, later in the cascade, restores the ring. +$qorg-focus-controls: ".btn:focus-visible, .nav-link:focus-visible, .navbar-brand:focus-visible, .dropdown-item:focus-visible, .form-control:focus-visible, .form-select:focus-visible, .form-check-input:focus-visible, .aa-Input:focus-visible, .aa-DetachedSearchButton:focus-visible, .sidebar-item-toggle:focus-visible, .quarto-navigation-tool:focus-visible, .accordion-button:focus-visible, .page-link:focus-visible"; + +:focus-visible, +#{$qorg-focus-controls} { + outline: 2px solid var(--qorg-ring); + outline-offset: 2px; +} + +#{$qorg-focus-controls} { + box-shadow: none; +} + +// The "Supported by Posit" badge script draws the badge frame with an outline +// on #supported-by-posit, in both modes, so the ring needs an id selector. +#quarto-header #supported-by-posit:focus-visible { + outline: 2px solid var(--qorg-ring); + outline-offset: 2px; +} + +// The navbar is fixed, so keep focused and linked elements below it: about +// 65px on wide screens, up to about 135px when the secondary bar shows. +html { + scroll-padding-top: 5rem; + + @media (max-width: 991.98px) { + scroll-padding-top: 9rem; + } +} + +@media (forced-colors: active) { + :focus-visible, + #{$qorg-focus-controls}, + #quarto-header #supported-by-posit:focus-visible { + outline-color: Highlight; + } +} diff --git a/_extensions/quartoorg/scss/partials/_gallery.scss b/_extensions/quartoorg/scss/partials/_gallery.scss new file mode 100644 index 0000000000..fff28a0c44 --- /dev/null +++ b/_extensions/quartoorg/scss/partials/_gallery.scss @@ -0,0 +1,248 @@ +// Gallery styles on the site tokens (--qorg-*, --cat-* from _tokens.scss) and +// the shared shapes (_shapes.scss). The focus ring comes from _focus.scss. + +// per-category accent, applied via a `.cat-` class +.cat-reports { --c: var(--cat-reports); } +.cat-dashboards { --c: var(--cat-dashboards); } +.cat-research { --c: var(--cat-research); } +.cat-teaching { --c: var(--cat-teaching); } +.cat-docs { --c: var(--cat-docs); } +.cat-personal { --c: var(--cat-personal); } + +// Empty title block reserves space at the top; hide only when empty so titled +// pages keep theirs. +body.gallery header#title-block-header:empty { display: none; } + +// On the landing (body.gallery-landing) the page `title:` is the eyebrow above +// the hero heading, so style it like the old `.kicker` label and drop the +// theme's title-block chrome (rule, spacing, metadata). +.gallery-landing #title-block-header { + margin-block-end: 0; border-block-end: 0; padding-block-end: 0; + .title { + letter-spacing: 0.14em; text-transform: uppercase; + font-size: 0.75rem; color: var(--qorg-link); font-weight: 700; + margin: 0; + } + .quarto-title-meta, .subtitle { display: none; } +} + +// ---------------------------------------------------------------- hero chooser +.gal-hero { padding: 0.5rem 0 0.4rem; } +.gal-hero h2 { font-size: 2.25rem; line-height: 1.1; margin: 0.4rem 0 0.3rem; font-weight: 800; } +.gal-hero .lede { color: var(--qorg-muted); font-size: 1rem; margin: 0 0 1.4rem; } + +.gal-chooser { + display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; + padding: 0 0 1.6rem; list-style: none; margin: 0; +} +@media (max-width: 900px) { .gal-chooser { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 600px) { .gal-chooser { grid-template-columns: 1fr; } } + +.gal-chooser > li { display: flex; } // stretch the card to the grid row height +.gal-choice { + display: block; text-decoration: none; + width: 100%; // fill the flex item so wrapped taglines don't make one card taller + padding: 0.95rem 1rem 0.9rem; color: var(--qorg-ink); + @include qorg-card-shell($bar: var(--c)); +} +.gal-choice:hover { border-color: var(--c); } +.gal-choice .row { display: flex; align-items: center; justify-content: space-between; } +.gal-choice .ic { font-size: 1.2rem; } +.gal-choice .gal-choice-title { display: block; margin: 0.55rem 0 0.2rem; font-size: 1rem; font-weight: 700; } +.gal-choice p { margin: 0; font-size: 0.8rem; color: var(--qorg-muted); line-height: 1.35; } + +// --------------------------------------------------------- floating jump rail +// In-page navigation as a floating rail pinned to the right edge (à la +// mickael.canouil.fr): a collapsed icon column whose labels expand leftwards on +// hover / keyboard focus, so the centred content never shifts. Scrollspy (in +// _jumpnav.ejs.md) marks the active section with .active + aria-current. Below +// $gal-rail-bp the same markup becomes a horizontal bottom dock. The listing +// wrapper carries no layout because the nav is `position: fixed`. +$gal-rail-bp: 991.98px; + +#jumpnav { margin: 0; } +.gal-rail { + position: fixed; + top: 5rem; + right: 1rem; + bottom: 1rem; + z-index: 1020; + display: flex; + align-items: center; + justify-content: flex-end; + max-width: min(18rem, 22vw); + pointer-events: none; + transition: opacity 0.25s ease, transform 0.25s ease, visibility 0.25s ease; + + &.gal-rail--tucked { + opacity: 0; + visibility: hidden; + transform: translateX(120%); + } + + ul { + display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem; + max-height: 100%; margin: 0; padding: 0.5rem; list-style: none; + overflow-y: auto; scrollbar-width: none; + pointer-events: auto; + @include qorg-float-panel($radius: 14px); + &::-webkit-scrollbar { display: none; } + } + li { width: 100%; margin: 0; } + + &:hover .gal-rail__label, + &:focus-within .gal-rail__label { max-width: 12rem; padding-inline-start: 0.5rem; opacity: 1; } +} + +.gal-rail__link { + display: flex; flex-direction: row-reverse; align-items: center; justify-content: flex-start; + padding: 0.2rem; border-radius: 10px; color: var(--qorg-ink); text-decoration: none; + transition: color 0.15s ease, background-color 0.15s ease; + &:hover, &:focus-visible { + color: var(--c, var(--qorg-link)); + background: color-mix(in srgb, var(--c, var(--qorg-link)) 16%, transparent); + text-decoration: none; + } + &.active { + color: var(--c, var(--qorg-link)); + background: color-mix(in srgb, var(--c, var(--qorg-link)) 24%, transparent); + } +} + +.gal-rail__mark { + display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; + width: 1.75rem; height: 1.75rem; font-size: 1.05rem; +} + +.gal-rail__label { + max-width: 0; overflow: hidden; white-space: nowrap; + font-size: 0.82rem; font-weight: 600; letter-spacing: 0.01em; opacity: 0; + will-change: max-width, opacity; + // Grow the box first, then fade the text in (opacity delayed), so the label + // never paints while still clipped — the main cause of the reveal flicker. + // One shared easing curve keeps width, padding and text in step. + transition: + max-width 0.3s cubic-bezier(0.4, 0, 0.2, 1), + padding 0.3s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.18s ease 0.09s; +} + +.cat-browse { --c: var(--qorg-link); } + +// Bottom dock: below the breakpoint a right-edge rail would sit on the body, so +// the same markup docks along the bottom edge as a horizontal, swipeable strip +// with the label shown on the active chip only. +@media (max-width: $gal-rail-bp) { + // Reserve a band at the page end so the fixed dock never traps the last + // content beneath it (the footer tuck only clears the footer itself). + body.gallery { padding-bottom: 4rem; } + + .gal-rail { + inset: auto 0 0 0; align-items: stretch; max-width: none; + &.gal-rail--tucked { transform: none; } + ul { + flex-direction: row; align-items: center; width: 100%; max-height: none; + padding: 0.35rem 0.5rem; overflow-x: auto; overflow-y: hidden; + border-width: 1px 0 0; border-radius: 0; + } + li { + width: auto; + &:first-child { margin-inline-start: auto; } + &:last-child { margin-inline-end: auto; } + } + // Cancel the bar-wide reveal so the whole dock does not expand; the active + // and the focused chip each still show their label (below). + &:hover .gal-rail__label, + &:focus-within .gal-rail__label { max-width: 0; padding-inline-start: 0; opacity: 0; } + } + .gal-rail__link { + flex-direction: row; min-width: 2.75rem; min-height: 2.75rem; + justify-content: center; padding: 0.25rem 0.35rem; + } + // Eased (not snapped) so hover and active-on-scroll reveals glide; kept short + // because the active chip retargets as sections pass. + .gal-rail__label { + transition: + max-width 0.24s cubic-bezier(0.4, 0, 0.2, 1), + padding 0.24s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.16s ease 0.06s; + } + // Named for screen readers at every size (label kept in the a11y tree, only + // clipped); shown visually on the active chip, the hovered chip, and the + // keyboard-focused one, so pointer and keyboard users both see the label, not + // a bare icon. Per-chip (not bar-wide) so the dock never expands as a whole. + .gal-rail__link.active .gal-rail__label, + .gal-rail__link:hover .gal-rail__label, + .gal-rail__link:focus-visible .gal-rail__label { max-width: 12rem; padding-inline-start: 0.35rem; opacity: 1; } +} + +// ---------------------------------------------------------------- sections +.gal-featured-intro { margin: 1.6rem 0 0.2rem; } +.gal-featured-intro h2 { font-size: 1.4rem; font-weight: 800; margin: 0 0 0.3rem; } +.gal-featured-intro p { color: var(--qorg-muted); font-size: 1rem; margin: 0; } + +.gal-section { padding: 1.9rem 0 0.3rem; scroll-margin-top: 5rem; } +#browse-by-format { scroll-margin-top: 5rem; } +.gal-section .shead { display: flex; align-items: center; justify-content: space-between; gap: 0.8rem; margin-bottom: 0.2rem; } +.gal-section .shead .l { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; } +.gal-section h2 { font-size: 1.4rem; margin: 0; font-weight: 800; @include qorg-bar-heading($bar: var(--c)); } +.gal-section h2 a { color: inherit; text-decoration: none; display: inline-flex; align-items: baseline; gap: 0.5rem; } +.gal-section h2 .ic { font-size: 1.15rem; line-height: 1; flex: 0 0 auto; } +.gal-section .tag { color: var(--qorg-muted); font-size: 0.85rem; } +.gal-seeall { white-space: nowrap; color: var(--qorg-link); text-decoration: none; font-weight: 600; font-size: 0.82rem; } + +.gal-caps { margin: 0.55rem 0 1rem; font-size: 0.78rem; color: var(--qorg-muted); } +.gal-caps b { color: var(--qorg-ink); font-weight: 600; } +.gal-caps a { display: inline-block; background: var(--qorg-chip); color: var(--qorg-link); text-decoration: none; padding: 0.18rem 0.6rem; border-radius: var(--qorg-radius-chip); margin: 0 0.35rem 0.35rem 0; font-weight: 600; font-size: 0.72rem; border: 1px solid var(--qorg-chip-line); } + +// ------------------------------------------------------------------- cards +.gal-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.1rem; list-style: none; margin: 0; padding: 0; } +@media (max-width: 900px) { .gal-cards { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 600px) { .gal-cards { grid-template-columns: 1fr; } } + +.gal-card { display: flex; flex-direction: column; @include qorg-card-shell($bar: null); } +// Image-less card header: white label on the category colour darkened with +// black, the same in both modes, so these three colours stay literal. +.gal-card .thumb { height: 172px; display: flex; align-items: flex-end; padding: 0.6rem 0.75rem; color: #fff; font-size: 0.74rem; font-weight: 600; background: linear-gradient(135deg, color-mix(in srgb, var(--c, var(--qorg-link)) 55%, #000), color-mix(in srgb, var(--c, var(--qorg-link)) 32%, #000)); } +// `contain` shows the whole screenshot; the letterbox blanks take the card's +// category colour (the same accent used for image-less card headers), falling back +// to the neutral surface where a card is not scoped to one category (browse grid). +.gal-card img.thumb-img, .gal-card video.thumb-img { width: 100%; height: 172px; object-fit: contain; object-position: center; display: block; background: var(--c, var(--qorg-chip)); } +// Featured hover preview: static poster with the