├── .editorconfig ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js └── src ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts ├── source-code-pro.woff2 └── work-sans.woff2 ├── images ├── tiny-ui-toggle-logo.svg └── tiny-ui-toggle-social.png ├── index.html ├── scripts ├── main.js ├── share-url.js └── tiny-ui-toggle.js └── styles ├── main.css ├── site ├── base.css ├── components.css ├── demo.css ├── layout.css ├── main.css ├── reset.css └── variables.css ├── tiny-ui-toggle.css └── tiny-ui-toggle └── tiny-ui-toggle.css /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # Matches multiple files with brace expansion notation 14 | # Set default charset 15 | [*.{js,html}] 16 | charset = utf-8 17 | 18 | # 2 space indentation 19 | [*.{html,css,scss,js}] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | # Markdown files 24 | [*.md] 25 | trim_trailing_whitespace = false 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/starter-workflows/blob/main/pages/static.yml + https://github.com/JamesIves/github-pages-deploy-action 2 | # Simple workflow for deploying static content to GitHub Pages 3 | name: Deploy static content to Pages 4 | 5 | on: 6 | # Runs on pushes targeting the default branch 7 | push: 8 | branches: ["master"] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 20 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 21 | concurrency: 22 | group: "pages" 23 | cancel-in-progress: false 24 | 25 | jobs: 26 | # Single deploy job since we're just deploying 27 | deploy: 28 | environment: 29 | name: github-pages 30 | url: ${{ steps.deployment.outputs.page_url }} 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | # - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 36 | # run: | 37 | # npm ci 38 | # npm run build 39 | - name: Setup Pages 40 | uses: actions/configure-pages@v5 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v3 43 | with: 44 | # Upload entire repository 45 | path: 'src' 46 | - name: Deploy to GitHub Pages 47 | id: deployment 48 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .DS_Store 4 | Thumbs.db 5 | 6 | *.map 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Nigel O Toole 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny UI Toggle 2 | ### Toggle the state of a UI element to easily create components e.g. collapse, accordion, tabs, dropdown, dialog/modal. 3 | 4 | ### [Demo and documentation](http://nigelotoole.github.io/tiny-ui-toggle/) 5 | 6 | --- 7 | ## Quick start 8 | ```javascript 9 | $ npm install tiny-ui-toggle --save-dev 10 | ``` 11 | 12 | Import the JS and CSS into your project, add the elements to your HTML and initialize the plugin. 13 | 14 | [Full documentation](http://nigelotoole.github.io/tiny-ui-toggle/) 15 | 16 | --- 17 | ### License 18 | MIT © Nigel O Toole 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-ui-toggle", 3 | "homepage": "https://nigelotoole.github.io/tiny-ui-toggle/", 4 | "author": "Nigel O Toole (http://www.purestructure.com)", 5 | "description": "Toggle the state of a UI element to easily create components e.g. collapse, accordion, tabs, dropdown, dialog / modal.", 6 | "keywords": [ 7 | "toggle", 8 | "collapse", 9 | "accordion", 10 | "tabs", 11 | "dropdown", 12 | "menu", 13 | "dialog", 14 | "modal", 15 | "javascript" 16 | ], 17 | "main": "src/scripts/tiny-ui-toggle.js", 18 | "version": "2.0.1", 19 | "license": "MIT", 20 | "engines": { 21 | "node": ">=14" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/NigelOToole/tiny-ui-toggle.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/NigelOToole/tiny-ui-toggle/issues" 29 | }, 30 | "browserslist": [ 31 | "defaults" 32 | ], 33 | "devDependencies": { 34 | "@11ty/eleventy-dev-server": "^1.0.4", 35 | "concurrently": "^8.2.1", 36 | "cross-env": "^7.0.3", 37 | "postcss": "^8.4.31", 38 | "postcss-cli": "^10.1.0", 39 | "postcss-custom-media": "^10.0.2", 40 | "postcss-import": "^15.1.0", 41 | "postcss-preset-env": "^9.1.4", 42 | "rimraf": "^5.0.5" 43 | }, 44 | "scripts": { 45 | "clean": "rimraf src/**/*.map", 46 | "dev": "cross-env NODE_ENV=development && concurrently \"npm:dev:*\"", 47 | "dev:server": "npx @11ty/eleventy-dev-server --dir=src", 48 | "dev:styles": "postcss src/styles/site/main.css src/styles/tiny-ui-toggle/tiny-ui-toggle.css --dir src/styles --watch", 49 | "build": "npm run clean && cross-env NODE_ENV=production concurrently \"npm:build:*\"", 50 | "build:styles": "postcss src/styles/site/main.css src/styles/tiny-ui-toggle/tiny-ui-toggle.css --dir src/styles", 51 | "publish:npm": "npm run build && npm publish" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const isProduction = process.env.NODE_ENV === "production"; 2 | const plugins = [ 3 | require('postcss-import'), 4 | require('postcss-custom-media'), 5 | require('postcss-preset-env')({ 6 | stage: 1 7 | }), 8 | ]; 9 | 10 | module.exports = { 11 | map: isProduction ? false : { annotation: true, inline: false }, 12 | plugins 13 | } 14 | -------------------------------------------------------------------------------- /src/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NigelOToole/tiny-ui-toggle/b571b63e6884f245d40f74457969e0d5b7d9120d/src/apple-touch-icon.png -------------------------------------------------------------------------------- /src/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NigelOToole/tiny-ui-toggle/b571b63e6884f245d40f74457969e0d5b7d9120d/src/favicon-16x16.png -------------------------------------------------------------------------------- /src/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NigelOToole/tiny-ui-toggle/b571b63e6884f245d40f74457969e0d5b7d9120d/src/favicon-32x32.png -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NigelOToole/tiny-ui-toggle/b571b63e6884f245d40f74457969e0d5b7d9120d/src/favicon.ico -------------------------------------------------------------------------------- /src/fonts/source-code-pro.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NigelOToole/tiny-ui-toggle/b571b63e6884f245d40f74457969e0d5b7d9120d/src/fonts/source-code-pro.woff2 -------------------------------------------------------------------------------- /src/fonts/work-sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NigelOToole/tiny-ui-toggle/b571b63e6884f245d40f74457969e0d5b7d9120d/src/fonts/work-sans.woff2 -------------------------------------------------------------------------------- /src/images/tiny-ui-toggle-logo.svg: -------------------------------------------------------------------------------- 1 | tiny-ui-toggle-logo -------------------------------------------------------------------------------- /src/images/tiny-ui-toggle-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NigelOToole/tiny-ui-toggle/b571b63e6884f245d40f74457969e0d5b7d9120d/src/images/tiny-ui-toggle-social.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Tiny UI Toggle 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 |

Tiny UI Toggle

37 | 38 | 43 |
44 |
45 | 46 | 47 |
48 |
49 |
50 | 51 |
52 |

Toggle the state of a UI element to easily create components like collapse, accordion, tabs, dropdown and dialog / modal.

53 | 54 | Tiny UI Toggle logo 55 |
56 | 57 |

Features

58 |
    59 |
  • Height animation
  • 60 |
  • Syncs the state of triggers and targets
  • 61 |
  • Open on hover, on click or through API
  • 62 |
  • Close on exit, click outside or through API
  • 63 |
  • Supports native elements (details, dialog, popover)
  • 64 |
  • Toggles ARIA attributes
  • 65 |
  • Toggles text on the trigger element
  • 66 |
67 | 68 |
69 |
70 | 71 | 72 |
73 |
74 | 75 |

Examples

76 |

Collapse

77 | 78 |
79 | 80 | 81 | 82 | 87 | 88 |
89 | 90 | 91 |
92 | 93 |
94 | Toggle details 95 |
96 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit. 97 |
98 |
99 | 100 |
101 | 102 | 103 |
104 | 105 | 106 | 107 |
108 |
109 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit. 110 |
111 |
112 | 113 |
114 | 115 | 116 |
117 | 118 | 119 | 120 |
121 |
122 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit. 123 |
124 |
125 | 126 |
127 | 128 | 129 |
130 | 131 | 132 | 133 | 138 | 139 |
140 | 141 | 142 |

Accordion

143 | 144 |
145 | 146 |
147 | 148 | 149 |
150 |
151 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit. 152 |
153 |
154 |
155 | 156 |
157 | 158 | 159 | 164 |
165 | 166 |
167 | 168 | 169 | 174 |
175 | 176 |
177 | 178 | 179 |

Tabs

180 | 181 |
182 | 183 |
184 | 185 | 186 | 187 | 188 | 189 |
190 | 191 |
192 |
193 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit. 194 |
195 |
196 | 197 | 198 | 203 | 204 | 209 |
210 | 211 | 212 | 213 |

Dropdown

214 | 215 |
216 | 217 |
218 | 219 | 220 | 227 |
228 | 229 |
230 | 231 | 232 |

Menu

233 | 234 |

This example is setup so the dropdowns only close when you go outside of the whole menu box and styles have been added to transform the dropdowns to an accordion on small screens.

235 | 236 |
237 | 238 |
239 |
Menu logo
240 | 241 | 266 | 267 | 268 |
269 | 270 |
271 | 272 | 273 |

Dialog / Modal

274 | 275 |

A dialog is a popup that appears over the page while a modal is a popup that prevents the user from interacting with the page until the modal is closed.

276 | 277 |

A dialog can use the dialog element or by adding add role="dialog" to another element. A modal is the same with aria-modal="true" added to the element.

278 | 279 |
280 | 281 |
282 | 283 | 284 | 285 | 286 | 287 |
288 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit. 289 |
290 |
291 | 292 |
293 | 294 |
295 | 296 | 297 | 298 | 305 | 306 |
307 | 308 |
309 | 310 | 311 |

Popover / Tooltip

312 | 313 |
314 | 315 |
316 | 317 | 318 | 319 | 326 | 327 |
328 | 329 | 330 |
331 | 332 |
333 | 334 | 335 | 340 |
341 | 342 |
343 | 344 |
345 | 346 |
347 |
348 | 349 | 350 |
351 |
352 | 353 |

Installation

354 | 355 |
$ npm install tiny-ui-toggle --save-dev
356 | 357 |

Import the files into your project, add the elements to your markup and initialize the plugin.

358 | 359 |
    360 |
  • JS: node_modules/tiny-ui-toggle/src/scripts/tiny-ui-toggle.js
  • 361 |
  • CSS: node_modules/tiny-ui-toggle/src/styles/tiny-ui-toggle/tiny-ui-toggle.css
  • 362 |
  • Compiled CSS: node_modules/tiny-ui-toggle/src/styles/tiny-ui-toggle.css
  • 363 |
364 | 365 |

JavaScript

366 | 367 |
import { Toggle, toggleAutoInit } from 'tiny-ui-toggle.js';
368 | 
369 | // Initialize all elements with default options, these can be overridden by reinitializing or by data attributes on the element.
370 | toggleAutoInit();
371 | 
372 | // Initialize all elements manually which allows you to pass in custom options.
373 | const toggleElements = document.querySelectorAll('.toggle');
374 | for (const item of toggleElements) {
375 |   Toggle({ selector: item });
376 | };
377 | 
378 | // Initialize a single instance.
379 | const defaultToggle = Toggle({ selector: '.toggle' });
380 | 
381 | 382 |

Options

383 |

The options can be set via initialization in the JS or by data attributes on the elements with the prefix 'toggle'. e.g. data-toggle-active-class="is-open". Attributes will be given the highest priority.

384 | 385 |
386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 |
PropertyDefaultTypeDescription
selector'.toggle'String || elementTrigger element selector.
target''String || elementTarget element selector.
group''String || elementGroup element selector.
activeClass'is-active'StringCSS class when element is active.
animClass'is-anim'StringCSS class when element is animating.
bodyClass''StringCSS class added to the body when the element is active.
animateHeighttrueBooleanAnimate the height of the target element.
textSelector'.toggle-text'StringElement for toggle text.
textActive''StringText of element when it is active.
textInactive''StringText of element when it is inactive.
closeAutofalseboolean || stringAutomatically close the element when the pointer stops hovering the element or there is a click outside the element. It can be scoped to only trigger for when hover ends with 'hover' or on an outside click with 'click'.
closeAutoDelay500Integer(ms)Delay in auto closing an element when it is not focused.
closeOnEscapefalseBooleanClose the target element when the escape key is pressed.
openAutofalseBooleanAutomatically open the target element on hover.
478 |
479 | 480 |

API

481 | 482 |
483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 |
PropertyTypeDescription
instance.toggle()MethodToggles the state of the element and any target elements.
instance.set(state)MethodSets the state of the element and any target elements.
instance.toggleElement()MethodToggles the state of the element.
instance.setElement(state)MethodSets the state of the element.
instance.getInfo()ObjectReturns the element and its properties.
515 |
516 | 517 | 518 |

Markup

519 | 520 |
<button class="toggle" data-toggle-target="#demo-panel-collapse" aria-expanded="false">Toggle panel</button>
521 | 
522 | <div id="demo-panel-collapse" class="toggle-panel" aria-hidden="true">
523 |   ...
524 | </div>
525 | 
526 | 527 | 528 |

Events

529 | 530 |

A toggle fires an event on each toggle state i.e. toggleOpening, toggleOpened, toggleClosing and toggleClosed. This can be observed on the element itself or the document.

531 | 532 |
document.querySelector('.toggle').addEventListener('toggleOpening', (event) => { 
533 |   console.log('Toggle Opening', event.target);
534 | });
535 | 
536 | 537 | 538 |

Compatibility

539 |

Supports all modern browsers at the time of release.

540 | 541 | 542 |

Demo site

543 |

Clone the repo from Github and run the commands below.

544 | 545 |
$ npm install
546 | $ npm run dev
547 | 
548 | 549 |
550 |
551 |
552 | 553 | 554 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | -------------------------------------------------------------------------------- /src/scripts/main.js: -------------------------------------------------------------------------------- 1 | import { ShareUrl, ShareUrlAuto } from './share-url.js'; 2 | import { Toggle, toggleAutoInit } from './tiny-ui-toggle.js'; 3 | 4 | window.addEventListener('DOMContentLoaded', (event) => { 5 | 6 | // Initialize all elements with default options, these can be overridden by reinitializing or with data attributes on the element. 7 | toggleAutoInit(); 8 | 9 | 10 | // Initialize all elements manually which allows you to pass in custom options. 11 | // const toggleElements = document.querySelectorAll('.toggle'); 12 | // for (const item of toggleElements) { 13 | // Toggle({ selector: item }); 14 | // }; 15 | 16 | 17 | // Initialize a single instance. 18 | // const toggleDefault = Toggle({ selector: '.toggle' }); 19 | 20 | 21 | // Menu example shows passing in custom options to the toggle. 22 | const menuElements = document.querySelectorAll('.menu .toggle'); 23 | 24 | for (const item of menuElements) { 25 | Toggle({ selector: item, target: 'next', group: '.menu .toggle-panel', wrapper: '.demo--menu', closeAuto: true }); 26 | }; 27 | 28 | 29 | 30 | // Examples of how to use toggle methods using the first toggle on the page 31 | // const defaultToggle = Toggle({ selector: '.toggle' }); 32 | 33 | 34 | // Returns the properties of the toggle 35 | // console.log(defaultToggle.getInfo()); 36 | 37 | 38 | // Toggles the state of the trigger and the target 39 | // defaultToggle.toggle(); 40 | 41 | 42 | // Equivalent to the above without initializing 43 | // Toggle().toggle(); 44 | 45 | 46 | // Sets the state of the trigger and the target 47 | // defaultToggle.set(true); 48 | 49 | 50 | // Toggles the state of an element, the trigger element is the default 51 | // defaultToggle.toggleElement(); 52 | // defaultToggle.toggleElement(document.querySelector('.toggle')); 53 | // defaultToggle.toggleElement(document.querySelector('.toggle-panel')); 54 | 55 | 56 | // Sets the state of an element, the trigger element is the default 57 | // defaultToggle.setElement(true); 58 | // defaultToggle.setElement(true, document.querySelector('.toggle')); 59 | // defaultToggle.setElement(true, document.querySelector('.toggle-panel')); 60 | 61 | 62 | // Event listeners - toggleOpening, toggleOpened, toggleClosing, toggleClosed 63 | // document.querySelector('.toggle').addEventListener('toggleOpening', (event) => { 64 | // console.log('Toggle Opening', event.target); 65 | // }); 66 | 67 | // document.addEventListener('toggleOpening', (event) => { 68 | // console.log('Toggle Opening', event.target); 69 | // }); 70 | 71 | 72 | 73 | // Share links 74 | ShareUrlAuto(); 75 | 76 | // Encoded text 77 | const encodeElements = document.querySelectorAll('.encode'); 78 | for (const item of encodeElements) { 79 | let decode = atob(item.dataset['encode']); 80 | 81 | if (item.dataset['encodeAttribute']) { 82 | item.setAttribute(`${item.dataset['encodeAttribute']}`, `${decode}`); 83 | } 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /src/scripts/share-url.js: -------------------------------------------------------------------------------- 1 | const ShareUrl = function (args) { 2 | const defaults = { 3 | selector: '.share-url', 4 | action: 'share', 5 | url: document.location.href, 6 | title: document.title, 7 | textSelector: null, 8 | textLabel: '', 9 | textSuccess: 'Shared', 10 | successClass: 'is-active', 11 | maintainSize: true 12 | } 13 | 14 | let options = {...defaults, ...args}; 15 | let element; 16 | let textElement; 17 | 18 | 19 | // Utilities 20 | const checkBoolean = function (string) { 21 | if (string.toLowerCase() === 'true') return true; 22 | if (string.toLowerCase() === 'false') return false; 23 | return string; 24 | }; 25 | 26 | 27 | // Methods 28 | const shareEvent = async () => { 29 | try { 30 | if (options.action === 'share') { 31 | await navigator.share({ title: options.title, text: options.title, url: options.url }); 32 | } 33 | 34 | if (options.action === 'clipboard') { 35 | await navigator.clipboard.writeText(options.url); 36 | } 37 | 38 | let textWidth = textElement.offsetWidth; 39 | textElement.innerText = options.textSuccess; 40 | if (options.maintainSize) textElement.style.width = `${Math.max(textWidth, textElement.offsetWidth)}px`; 41 | element.classList.add(options.successClass); 42 | } 43 | catch (error) { 44 | if (error.name !== 'AbortError') console.error(error.name, error.message); 45 | } 46 | } 47 | 48 | 49 | const setup = function() { 50 | let datasetOptions = {...element.dataset}; 51 | let datasetPrefix = 'share'; 52 | 53 | for (const item in datasetOptions) { 54 | if (!item.startsWith(datasetPrefix)) continue; 55 | 56 | let prop = item.substring(datasetPrefix.length); 57 | prop = prop.charAt(0).toLowerCase() + prop.substring(1); 58 | let value = checkBoolean(datasetOptions[item]); 59 | 60 | options[prop] = value; 61 | // console.log(`${prop}: ${value}`) 62 | }; 63 | 64 | // console.log(options); 65 | 66 | textElement = element.querySelector(options.textSelector); 67 | if (textElement === null) textElement = element; 68 | if (options.textLabel) textElement.innerText = options.textLabel; 69 | 70 | 71 | if (navigator[options.action]) { 72 | // element.addEventListener('click', (event) => shareEvent(event, options.action)); 73 | element.addEventListener('click', () => shareEvent()); 74 | } 75 | else { 76 | element.style.display = 'none'; 77 | } 78 | }; 79 | 80 | const init = function() { 81 | element = (typeof options.selector === 'string') ? document.querySelector(options.selector) : options.selector; 82 | if (!element) return; 83 | 84 | setup(); 85 | }; 86 | 87 | init(); 88 | 89 | }; 90 | 91 | 92 | const ShareUrlAuto = function () { 93 | const elements = document.querySelectorAll('.share-url'); 94 | for (const item of elements) { 95 | ShareUrl({ selector: item }); 96 | }; 97 | }; 98 | 99 | 100 | export { ShareUrl, ShareUrlAuto }; 101 | -------------------------------------------------------------------------------- /src/scripts/tiny-ui-toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | Toggle the state of a UI element 3 | @param {string || element} selector - Trigger element. 4 | @param {string || element} target - Target element. 5 | @param {string || element} group - Group element. 6 | @param {string || element} wrapper - Wrapper element, used to scope CloseAuto to this element rather than just the trigger and target elements. 7 | @param {string} activeClass - CSS class when element is active. 8 | @param {string} animClass - CSS class when element is animating. 9 | @param {string} bodyClass - CSS class added to the body when the element is active. 10 | @param {boolean} animateHeight - Animate the height of the target element. 11 | @param {string} textSelector - Element for toggle text. 12 | @param {string} textActive - Text of element when it is active. 13 | @param {string} textInactive - Text of element when it is inactive. 14 | @param {boolean || string} closeAuto - Automatically close the element when the pointer stops hovering the element or there is a click outside the element. It can be scoped to only trigger for when hover ends with 'hover' or on an outside click with 'click'. 15 | @param {integer(ms)} closeAutoDelay - Delay in auto closing the element when it is not focused. 16 | @param {boolean} closeOnEscape - Close the target element when the escape key is pressed. 17 | @param {boolean} openAuto - Automatically opens the element on hover. 18 | @param {boolean} focusTrap - Trap the focus in the target element when it is active e.g. modal. 19 | */ 20 | 21 | const Toggle = function (options) { 22 | 23 | const defaults = { 24 | selector: '.toggle', 25 | target: '', 26 | group: '', 27 | wrapper: '', 28 | activeClass: 'is-active', 29 | animClass: 'is-anim', 30 | bodyClass: '', 31 | animateHeight: true, 32 | textSelector: '.toggle-text', 33 | textActive: '', 34 | textInactive: '', 35 | closeAuto: false, 36 | closeAutoDelay: 500, 37 | closeOnEscape: false, 38 | openAuto: false, 39 | focusTrap: false 40 | } 41 | 42 | let defaultOptions = {...defaults, ...options}; 43 | 44 | let element; 45 | let elementDefault; 46 | let closeTimeout; 47 | 48 | 49 | 50 | // Utilities 51 | const getElement = (selector, scope = 'single', returnArray = false) => { 52 | let elements; 53 | if (selector === '') return returnArray ? [] : null; 54 | 55 | // String 56 | if (typeof selector === 'string') { 57 | elements = (scope === 'single') ? document.querySelector(selector) : document.querySelectorAll(selector); 58 | elements = returnArray ? [...elements] : elements; 59 | } 60 | // Array 61 | else if (typeof selector === 'object') { 62 | elements = selector; 63 | } 64 | // Element 65 | else { 66 | elements = selector; 67 | elements = returnArray ? [elements] : elements; 68 | } 69 | 70 | return elements; 71 | } 72 | 73 | const getEventName = (active, position) => { 74 | let eventName; 75 | if (active && position === 'start') eventName = 'Opening'; 76 | if (active && position === 'end') eventName = 'Opened'; 77 | if (!active && position === 'start') eventName = 'Closing'; 78 | if (!active && position === 'end') eventName = 'Closed'; 79 | return `toggle${eventName}`; 80 | }; 81 | 82 | const fireEvent = (element, eventName, eventDetail) => { 83 | const event = new CustomEvent(eventName, { 84 | bubbles: true, 85 | detail: eventDetail, 86 | }); 87 | 88 | element.dispatchEvent(event); 89 | }; 90 | 91 | const getTransitionDuration = function (element) { 92 | if (element === undefined) return 0; 93 | 94 | let transitionDuration = getComputedStyle(element)['transitionDuration']; 95 | let animationDuration = getComputedStyle(element)['animationDuration']; 96 | 97 | let transitionDurationNumber = parseFloat(transitionDuration); 98 | let animationDurationNumber = parseFloat(animationDuration); 99 | 100 | let duration = animationDurationNumber > 0 ? animationDuration : transitionDuration; 101 | let durationNumber = animationDurationNumber > 0 ? animationDurationNumber : transitionDurationNumber; 102 | 103 | duration = duration.includes('ms') ? durationNumber : durationNumber*1000; 104 | return duration; 105 | }; 106 | 107 | const checkBoolean = function (string) { 108 | if (string.toLowerCase() === 'true') return true; 109 | if (string.toLowerCase() === 'false') return false; 110 | return string; 111 | }; 112 | 113 | 114 | 115 | // Methods 116 | const toggleAria = function (element) { 117 | const ariaAttributes = { 'aria-hidden': !element.toggle.active, 'aria-checked': element.toggle.active, 'aria-expanded': element.toggle.active, 'aria-selected': element.toggle.active, 'aria-pressed': element.toggle.active, 'tabindex': element.toggle.active ? 0 : -1 }; 118 | Object.keys(ariaAttributes).forEach(key => element.hasAttribute(key) && element.setAttribute(key, ariaAttributes[key])); 119 | }; 120 | 121 | const toggleText = function (element) { 122 | if (!element.toggle.textActive || !element.toggle.textInactive) return; 123 | 124 | let toggleTextElement = element.querySelector(element.toggle.textSelector); 125 | toggleTextElement = (toggleTextElement !== null) ? toggleTextElement : element; 126 | 127 | toggleTextElement.innerHTML = element.toggle.active ? element.toggle.textActive : element.toggle.textInactive; 128 | }; 129 | 130 | // Trap focus for a modal dialog - https://codepen.io/vaskort/pen/LYpwjoj 131 | const setupFocus = (element, trap = true) => { 132 | const focusableElements = element.toggle.focusableElements; 133 | if (focusableElements.length === 0) return; 134 | 135 | const firstFocusableElement = focusableElements[0]; 136 | const lastFocusableElement = focusableElements[focusableElements.length - 1]; 137 | const initFocus = document.activeElement; 138 | let currentFocus = firstFocusableElement; 139 | 140 | firstFocusableElement.focus(); 141 | 142 | if (!trap) return; 143 | 144 | const handleFocus = (event) => { 145 | event.preventDefault(); 146 | 147 | if (focusableElements.includes(event.target)) { 148 | currentFocus = event.target; 149 | } 150 | else { 151 | (currentFocus === firstFocusableElement) ? lastFocusableElement.focus() : firstFocusableElement.focus(); 152 | currentFocus = document.activeElement; 153 | } 154 | }; 155 | 156 | document.addEventListener('focus', handleFocus, true); 157 | 158 | return { 159 | releaseFocus: () => { 160 | document.removeEventListener('focus', handleFocus, true); 161 | initFocus.focus(); 162 | } 163 | }; 164 | }; 165 | 166 | 167 | const animateElementHeight = function (element) { 168 | clearTimeout(element.toggle.heightTimeout); 169 | 170 | if (element.toggle.active && element.toggle.isDetails) { 171 | element.style.height = `${element.scrollHeight}px`; 172 | } 173 | 174 | if (!element.toggle.active) element.style.height = 'auto'; 175 | 176 | requestAnimationFrame(() => { 177 | element.style.height = `${element.scrollHeight}px`; 178 | element.getBoundingClientRect(); 179 | 180 | if (!element.toggle.active) { 181 | element.style.height = element.toggle.isDetails ? `${element.querySelector('summary').scrollHeight}px` : ''; 182 | } 183 | 184 | element.toggle.heightTimeout = setTimeout(() => { 185 | element.style.height = element.toggle.active ? 'auto' : ''; 186 | }, element.toggle.transitionDuration); 187 | }); 188 | 189 | }; 190 | 191 | 192 | // Sets the state of an element 193 | const setState = function (state, element = elementDefault) { 194 | if (element.toggle.active === state) return; 195 | clearTimeout(element.toggle.transitionTimeout); 196 | fireEvent(element, getEventName(state, 'start')); 197 | element.toggle.active = state; 198 | 199 | element.classList.add(element.toggle.animClass); 200 | element.getBoundingClientRect(); 201 | 202 | element.classList.toggle(element.toggle.activeClass, element.toggle.active); 203 | 204 | element.toggle.transitionDuration = (element.toggle.type === 'trigger') ? getTransitionDuration(element.toggle.target[0]) : getTransitionDuration(element); 205 | 206 | if (element.toggle.type === 'target' && element.toggle.animateHeight) animateElementHeight(element); 207 | 208 | if (element.toggle.active && (element.toggle.isDetails || element.toggle.isDialog)) { 209 | element.setAttribute('open', ''); 210 | } 211 | 212 | if (element.toggle.bodyClass) { 213 | document.body.classList.toggle(element.toggle.bodyClass, element.toggle.active); 214 | } 215 | 216 | if (element.toggle.type === 'trigger') { 217 | toggleText(element); 218 | } 219 | 220 | toggleAria(element); 221 | 222 | 223 | element.toggle.transitionTimeout = setTimeout(() => { 224 | fireEvent(element, getEventName(state, 'end')); 225 | element.classList.remove(element.toggle.animClass); 226 | 227 | if (!element.toggle.active && (element.toggle.isDetails || element.toggle.isDialog)) { 228 | element.removeAttribute('open'); 229 | } 230 | 231 | if (element.toggle.type === 'target') { 232 | if (element.toggle.focusTrap || element.toggle.isModal) { 233 | element.toggle.active ? element.toggle.events['focus'] = setupFocus(element) : element.toggle.events['focus'].releaseFocus(); 234 | } 235 | else { 236 | if (element.toggle.active) setupFocus(element, false); 237 | } 238 | } 239 | }, element.toggle.transitionDuration); 240 | }; 241 | 242 | const setStateBoth = function (state, trigger = element, target = undefined) { 243 | if (target === undefined) target = trigger.toggle.target[0]; 244 | setState(state, trigger); 245 | setState(state, target); 246 | }; 247 | 248 | const toggleState = function (element = elementDefault) { 249 | setState(!element.toggle.active, element); 250 | }; 251 | 252 | const toggleStateBoth = function () { 253 | 254 | if (element.toggle.group.length > 1) { 255 | let groupElements = [...element.toggle.group]; 256 | 257 | for (const item of element.toggle.group) { 258 | groupElements.push(...item.toggle.trigger); 259 | }; 260 | 261 | for (const item of groupElements) { 262 | if (item !== element && item !== element.toggle.target[0]) setState(false, item); 263 | }; 264 | } 265 | 266 | toggleState(element); 267 | 268 | for (const target of element.toggle.target) { 269 | toggleState(target); 270 | 271 | for (const trigger of target.toggle.trigger) { 272 | if (trigger.toggle.active !== target.toggle.active) setState(target.toggle.active, trigger); 273 | }; 274 | }; 275 | 276 | }; 277 | 278 | 279 | const resetCloseAuto = function () { 280 | clearTimeout(closeTimeout); 281 | }; 282 | 283 | const startCloseAuto = function (trigger, target) { 284 | closeTimeout = setTimeout(() => { 285 | setStateBoth(false, trigger, target); 286 | }, trigger.toggle.closeAutoDelay); 287 | }; 288 | 289 | const addMouseEventListeners = function(element, trigger, target) { 290 | element.toggle.events['resetCloseAuto'] = resetCloseAuto; 291 | element.addEventListener('mouseover', element.toggle.events.resetCloseAuto); 292 | 293 | element.toggle.events['startCloseAuto'] = () => startCloseAuto(trigger, target); 294 | element.addEventListener('mouseleave', element.toggle.events.startCloseAuto); 295 | }; 296 | 297 | const handleClick = function (event) { 298 | event.preventDefault(); 299 | toggleStateBoth(); 300 | }; 301 | 302 | const addEventListeners = function() { 303 | element.toggle.events = { handleClick }; 304 | element.addEventListener('click', element.toggle.events.handleClick); 305 | 306 | let targetFirst = element.toggle.target[0]; 307 | if (targetFirst === undefined) return; 308 | let triggerFirst = targetFirst.toggle.trigger[0]; 309 | 310 | if (element.toggle.openAuto) { 311 | element.toggle.events['startOpenAuto'] = () => setStateBoth(true, element, targetFirst); 312 | element.addEventListener('mouseenter', element.toggle.events['startOpenAuto']); 313 | } 314 | 315 | if (element.toggle.closeAuto === true || element.toggle.closeAuto === 'hover') { 316 | if (element.toggle.wrapper) { 317 | element.toggle.wrapper.addEventListener('mouseover', resetCloseAuto); 318 | element.toggle.wrapper.addEventListener('mouseleave', () => startCloseAuto(element, targetFirst)); 319 | } 320 | else { 321 | addMouseEventListeners(element, element, targetFirst); 322 | 323 | for (const item of element.toggle.target) { 324 | addMouseEventListeners(item, element, item); 325 | }; 326 | } 327 | } 328 | 329 | if (element.toggle.closeAuto === true || element.toggle.closeAuto === 'click' || (element === triggerFirst && targetFirst.toggle.isModal)) { 330 | element.toggle.events['handleClickOutside'] = (event) => { 331 | let clickInsideTrigger, clickInsideTarget, clickInsideWrapper, clickOnModalBackdrop = false; 332 | clickInsideTrigger = element.contains(event.target); 333 | clickInsideTarget = targetFirst.contains(event.target); 334 | if (element.toggle.wrapper) clickInsideWrapper = element.toggle.wrapper.contains(event.target); 335 | if (event.target.toggle) clickOnModalBackdrop = event.target.toggle.isModal; 336 | 337 | if ((!clickInsideTrigger && !clickInsideTarget && !clickInsideWrapper) || clickOnModalBackdrop) setStateBoth(false, element, targetFirst); 338 | }; 339 | 340 | document.addEventListener('click', element.toggle.events.handleClickOutside); 341 | } 342 | 343 | for (const item of element.toggle.target) { 344 | if (item.toggle.closeOnEscape || item.toggle.isDialog) { 345 | item.toggle.events['handleEscape'] = (event) => { 346 | if (event.keyCode === 27) setStateBoth(false, element, item); 347 | }; 348 | 349 | item.addEventListener('keydown', item.toggle.events['handleEscape']); 350 | } 351 | }; 352 | }; 353 | 354 | const removeEventListeners = function(element) { 355 | element.removeEventListener('click', element.toggle.events.handleClick); 356 | element.removeEventListener('mouseenter', element.toggle.events.startOpenAuto); 357 | element.removeEventListener('mouseover', element.toggle.events.resetCloseAuto); 358 | element.removeEventListener('mouseleave', element.toggle.events.startCloseAuto); 359 | element.removeEventListener('keydown', element.toggle.events.handleEscape); 360 | document.removeEventListener('click', element.toggle.events.handleClickOutside); 361 | }; 362 | 363 | const getTarget = function () { 364 | let target = element.toggle.target; 365 | if (target === '') target = 'self'; 366 | 367 | const targetOptions = { 368 | 'next': [element.nextElementSibling], 369 | 'self': [] 370 | }; 371 | 372 | return targetOptions[target] || getElement(target, 'all', true); 373 | }; 374 | 375 | 376 | const assignProps = function (element, elementTrigger = undefined) { 377 | if (element.toggle === undefined) { 378 | element.toggle = defaultOptions; 379 | if (elementTrigger !== undefined) element.toggle = {...element.toggle, ...elementTrigger.toggle}; 380 | } 381 | else { 382 | element.toggle = {...element.toggle, ...options}; 383 | } 384 | 385 | element.toggle.type = !elementTrigger ? 'trigger' : 'target'; 386 | element.toggle.active = element.classList.contains(element.toggle.activeClass); 387 | element.toggle.events = {}; 388 | 389 | 390 | let datasetOptions = {...element.dataset}; 391 | 392 | for (const item in datasetOptions) { 393 | if (!item.startsWith('toggle')) continue; 394 | 395 | let datasetProp = item.substring(6); 396 | datasetProp = datasetProp.charAt(0).toLowerCase() + datasetProp.substring(1); 397 | element.toggle[`${datasetProp}`] = checkBoolean(datasetOptions[item]); 398 | }; 399 | 400 | if (element.toggle.type === 'trigger') { 401 | element.toggle.target = getTarget(); 402 | element.toggle.isInsideTarget = (element.toggle.target.length === 1) ? element.toggle.target[0].contains(element) : false; 403 | element.toggle.group = getElement(element.toggle.group, 'all', true); 404 | element.toggle.wrapper = getElement(element.toggle.wrapper); 405 | } 406 | 407 | if (element.toggle.type === 'target') { 408 | if (element.toggle.trigger === undefined) element.toggle.trigger = []; 409 | element.toggle.trigger.push(elementTrigger); 410 | let uniqueTrigger = [...new Set(element.toggle.trigger)]; 411 | element.toggle.trigger = uniqueTrigger; 412 | 413 | element.toggle.isDetails = (element.tagName === 'DETAILS' && element.querySelector('summary') !== null); 414 | element.toggle.isDialog = (element.tagName === 'DIALOG' || element.getAttribute('role') === 'dialog'); 415 | element.toggle.isModal = element.getAttribute('aria-modal') === 'true'; 416 | 417 | if (element.getAttribute('popover') === '' || element.getAttribute('popover') === 'auto') { 418 | element.toggle.closeOnEscape = true; 419 | element.toggle.closeAuto = 'click'; 420 | } 421 | 422 | element.toggle.focusableElements = Array.from(element.querySelectorAll(':is(input, button, select, textarea, details, [href], [tabindex]):not([disabled]):not([tabindex="-1"])')); 423 | } 424 | 425 | element.toggle.transitionDuration = (element.toggle.type === 'trigger') ? getTransitionDuration(element.toggle.target[0]) : getTransitionDuration(element); 426 | }; 427 | 428 | 429 | const setup = function(element, elementTrigger = undefined) { 430 | if (element.toggle !== undefined) removeEventListeners(element); 431 | assignProps(element, elementTrigger); 432 | }; 433 | 434 | const init = function() { 435 | element = getElement(defaultOptions.selector); 436 | if (element === null) return; 437 | 438 | setup(element); 439 | for (const item of element.toggle.target) { 440 | setup(item, element); 441 | }; 442 | 443 | addEventListeners(); 444 | elementDefault = element; 445 | }; 446 | 447 | init(); 448 | 449 | 450 | return { 451 | toggle: toggleStateBoth, 452 | set: setStateBoth, 453 | toggleElement: toggleState, 454 | setElement: setState, 455 | getInfo: () => { return { element: element, ...element.toggle } } 456 | }; 457 | 458 | }; 459 | 460 | 461 | const toggleAutoInit = function () { 462 | const toggleElements = document.querySelectorAll('.toggle'); 463 | for (const item of toggleElements) { 464 | Toggle({ selector: item }); 465 | }; 466 | }; 467 | 468 | 469 | export { Toggle, toggleAutoInit }; 470 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | /* https://www.joshwcomeau.com/css/custom-css-reset/, https://andy-bell.co.uk/a-more-modern-css-reset/ */ 2 | *, *::before, *::after { 3 | box-sizing: border-box; 4 | } 5 | * { 6 | margin: 0; 7 | } 8 | html { 9 | -moz-text-size-adjust: none; 10 | -webkit-text-size-adjust: none; 11 | text-size-adjust: none; 12 | } 13 | body { 14 | line-height: 1.5; 15 | -webkit-font-smoothing: antialiased; 16 | 17 | min-height: 100vh; 18 | } 19 | img, picture, video, canvas, svg { 20 | display: block; 21 | max-width: 100%; 22 | } 23 | input, button, textarea, select { 24 | font: inherit; 25 | } 26 | p, h1, h2, h3, h4, h5, h6 { 27 | word-wrap: break-word; 28 | } 29 | /* Opinionated */ 30 | h1, h2, h3, h4, 31 | button, input, label { 32 | line-height: 1.2; 33 | } 34 | h1, h2, h3, h4 { 35 | text-wrap: balance; 36 | } 37 | @font-face { 38 | font-family: 'Source Code Pro'; 39 | src: url('../fonts/source-code-pro.woff2') format('woff2'); 40 | display: swap; 41 | } 42 | @font-face { 43 | font-family: 'Work Sans'; 44 | src: url('../fonts/work-sans.woff2') format('woff2'); 45 | display: swap; 46 | } 47 | :root { 48 | /* https://oklch-palette.vercel.app/#53.67,0.257,262.51,100, https://oklch.com/#53.67,0.257,262.51,100 */ 49 | --color-50: rgb(245, 248, 255); 50 | --color-100: rgb(223, 234, 255); 51 | --color-200: rgb(153, 189, 255); 52 | --color-300: rgb(109, 159, 255); 53 | --color-400: rgb(63, 126, 255); 54 | --color-500: rgb(1, 87, 255); 55 | --color-600: rgb(0, 72, 215); 56 | --color-700: rgb(0, 52, 163); 57 | --color-800: rgb(0, 33, 112); 58 | --color-900: rgb(0, 17, 69); 59 | 60 | --color-accent-500: rgb(40, 209, 180); 61 | --color-accent-700: rgb(1, 119, 101); 62 | 63 | /* Type */ 64 | --sans-serif-font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, Arial, sans-serif; 65 | --serif-font-family: 'Times New Roman', Times, serif; 66 | /* --body-font-family: 'Work Sans', var(--sans-serif-font-family); 67 | --heading-font-family: 'Source Code Pro', var(--sans-serif-font-family); */ 68 | --body-font-family: 'Work Sans', sans-serif; 69 | --heading-font-family: 'Source Code Pro', monospace; 70 | 71 | --text-color: var(--color-700); 72 | --link-color: var(--color-500); 73 | --link-color-hover: var(--color-700); 74 | 75 | /* Layout */ 76 | --layout-breakpoint-xs: 0; 77 | --layout-breakpoint-sm: 576px; 78 | --layout-breakpoint-md: 768px; 79 | --layout-breakpoint-lg: 992px; 80 | --layout-breakpoint-xl: 1200px; 81 | --layout-breakpoint-xxl: 1400px; 82 | 83 | --layout-gutter-inline: 16px; 84 | --layout-gutter-block: 0px; 85 | 86 | --layout-space-xxs: 4px; 87 | --layout-space-xs: 8px; 88 | --layout-space-sm: 16px; 89 | --layout-space-md: 32px; 90 | --layout-space-lg: 48px; 91 | --layout-space-xl: 64px; 92 | --layout-space-xxl: 80px; 93 | 94 | --ease-out-cubic: cubic-bezier(.215, .610, .355, 1); 95 | --ease-in-out-cubic: cubic-bezier(.65, .05, .36, 1); 96 | 97 | --bg-grid-color: rgba(238, 238, 238, .75); 98 | --bg-grid-line: 2px; 99 | --bg-grid-box: 48px; 100 | /* Colours */ 101 | } 102 | @supports (color: color(display-p3 0 0 0)) { 103 | :root { 104 | --color-50: color(display-p3 0.96281 0.97222 0.99781); 105 | --color-100: color(display-p3 0.8822 0.91623 0.99269); 106 | --color-300: color(display-p3 0.46998 0.61827 0.97252); 107 | --color-700: color(display-p3 0.07062 0.20014 0.6152); 108 | --color-800: color(display-p3 0.03454 0.12659 0.42217); 109 | --color-900: color(display-p3 0.01272 0.0648 0.25957); 110 | } 111 | } 112 | @media (min-width: 768px) { 113 | :root { 114 | --layout-gutter-inline: 24px; 115 | } 116 | } 117 | @supports not (background-color: oklch(0%, 0, 0)) { 118 | :root { 119 | /* --color-50: #f0f5ff; */ 120 | --color-50: #f5f8ff; 121 | /* --color-100: #c5d9ff; */ 122 | --color-100: #dfeaff; 123 | --color-200: #99bdff; 124 | --color-300: #6d9fff; 125 | --color-400: #3f7eff; 126 | --color-500: #0157ff; 127 | --color-600: #0048d7; 128 | --color-700: #0034a3; 129 | --color-800: #002170; 130 | --color-900: #001145; 131 | 132 | --color-accent-500: #28d1b4; 133 | --color-accent-700: #007765; 134 | } 135 | } 136 | /* Custom media queries */ 137 | /* Type and background */ 138 | body { 139 | font-family: 'Work Sans', sans-serif; 140 | font-family: var(--body-font-family); 141 | color: rgb(0, 52, 163); 142 | color: color(display-p3 0.07062 0.20014 0.6152); 143 | color: var(--color-700); 144 | background-color: #fff; 145 | background-image: repeating-linear-gradient(90deg, rgba(238, 238, 238, .75) 0, rgba(238, 238, 238, .75) 2px, transparent 0, transparent 50%), repeating-linear-gradient(180deg, rgba(238, 238, 238, .75) 0, rgba(238, 238, 238, .75) 2px, transparent 0, transparent 50%); 146 | background-image: repeating-linear-gradient(90deg, var(--bg-grid-color) 0, var(--bg-grid-color) var(--bg-grid-line), transparent 0, transparent 50%), repeating-linear-gradient(180deg, var(--bg-grid-color) 0, var(--bg-grid-color) var(--bg-grid-line), transparent 0, transparent 50%); 147 | background-size: 48px 48px; 148 | background-size: var(--bg-grid-box) var(--bg-grid-box); 149 | background-position: calc(50% - (2px/2)) top; 150 | background-position: calc(50% - (var(--bg-grid-line)/2)) top; 151 | } 152 | .content { 153 | background-color: #fff; 154 | border-left: calc(2px/2) solid rgba(238, 238, 238, .75); 155 | border-right: calc(2px/2) solid rgba(238, 238, 238, .75); 156 | border-left: calc(var(--bg-grid-line)/2) solid var(--bg-grid-color); 157 | border-right: calc(var(--bg-grid-line)/2) solid var(--bg-grid-color); 158 | } 159 | ::-moz-selection { 160 | color: #fff; 161 | background-color: rgb(0, 52, 163); 162 | background-color: color(display-p3 0.07062 0.20014 0.6152); 163 | background-color: var(--color-700); 164 | } 165 | ::selection { 166 | color: #fff; 167 | background-color: rgb(0, 52, 163); 168 | background-color: color(display-p3 0.07062 0.20014 0.6152); 169 | background-color: var(--color-700); 170 | } 171 | h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 { 172 | font-family: 'Source Code Pro', monospace; 173 | font-family: var(--heading-font-family); 174 | font-weight: 400; 175 | word-spacing: -.5ch; 176 | } 177 | h1, h2, h3 { 178 | letter-spacing: -.025em; 179 | } 180 | h1, h2 { 181 | font-weight: 600; 182 | } 183 | /* Clamp from 375 - 768px */ 184 | h1 { font-size: 3rem; font-size: clamp(2.5rem, 2.0229rem + 2.0356vw, 3rem); } 185 | h2 { font-size: 2rem; font-size: clamp(1.5rem, 1.0229rem + 2.0356vw, 2rem); } 186 | h3 { font-size: 1.5rem; font-size: clamp(1.25rem, 1.0115rem + 1.0178vw, 1.5rem); } 187 | h4 { font-size: 1.25rem; font-size: clamp(1.125rem, 1.0057rem + 0.5089vw, 1.25rem); } 188 | h5 { font-size: 1rem; } 189 | h6 { font-size: 1rem; } 190 | p { 191 | 192 | } 193 | ul, ol { 194 | padding-left: 32px; 195 | padding-left: var(--layout-space-md); 196 | } 197 | pre, code { 198 | font-family: 'Source Code Pro', monospace; 199 | font-family: var(--heading-font-family); 200 | } 201 | a { 202 | color: rgb(1, 87, 255); 203 | color: var(--link-color); 204 | transition: color 0.3s; 205 | } 206 | a:hover, a:focus { 207 | color: rgb(0, 52, 163); 208 | color: color(display-p3 0.07062 0.20014 0.6152); 209 | color: var(--link-color-hover); 210 | } 211 | a:focus-within { 212 | outline: 2px solid rgb(1, 87, 255); 213 | outline: 2px solid var(--link-color); 214 | } 215 | /* Elements and utilities */ 216 | .img-fluid { 217 | max-width: 100%; 218 | height: auto; 219 | } 220 | .w-100 { 221 | width: 100%; 222 | } 223 | code { 224 | padding: .125rem; 225 | background-color: rgb(245, 248, 255); 226 | background-color: color(display-p3 0.96281 0.97222 0.99781); 227 | background-color: var(--color-50); 228 | } 229 | pre code { 230 | display: block; 231 | padding: 1rem; 232 | white-space: pre; 233 | overflow: auto; 234 | border: 1px solid rgb(1, 87, 255); 235 | border: 1px solid var(--color-500); 236 | } 237 | .table-outer { 238 | display: block; 239 | width: 100%; 240 | } 241 | .table { 242 | width: 100%; 243 | border-collapse: collapse; 244 | } 245 | .table th, .table td { 246 | padding: 16px; 247 | padding: var(--layout-space-sm); 248 | text-align: left; 249 | vertical-align: top; 250 | border: 1px solid rgb(1, 87, 255); 251 | border: 1px solid var(--color-500); 252 | } 253 | .table th { 254 | background-color: rgb(223, 234, 255); 255 | background-color: color(display-p3 0.8822 0.91623 0.99269); 256 | background-color: var(--color-100); 257 | } 258 | /* Inline overflow elements */ 259 | /* https://daverupert.com/2023/08/animation-timeline-scroll-shadows/ */ 260 | .overflow-inline { 261 | --shadow-color: rgba(0, 0, 0, .15); 262 | --shadow-size: 8px; 263 | --shadow-spread: calc(var(--shadow-size) * -.5); 264 | 265 | animation: scroll-shadow-inset linear; 266 | scroll-timeline: --scroll-timeline inline; 267 | animation-timeline: --scroll-timeline; 268 | 269 | overflow-x: auto; 270 | overflow-inline: auto; 271 | border: 1px solid rgb(1, 87, 255); 272 | border: 1px solid var(--color-500); 273 | } 274 | .overflow-inline > * { 275 | mix-blend-mode: multiply; 276 | } 277 | .overflow-inline.table-outer { 278 | border-left: 2px solid rgb(1, 87, 255); 279 | border-right: 2px solid rgb(1, 87, 255); 280 | border-left: 2px solid oklch(53.67% 0.257 262.51); 281 | border-left: 2px solid var(--color-500); 282 | border-right: 2px solid oklch(53.67% 0.257 262.51); 283 | border-right: 2px solid var(--color-500); 284 | } 285 | .overflow-inline.table-outer th:first-child, .overflow-inline.table-outer td:first-child { 286 | border-left: none; 287 | } 288 | .overflow-inline.table-outer th:last-child, .overflow-inline.table-outer td:last-child { 289 | border-right: none; 290 | } 291 | /* Negative spread to stop the shadow on the top and bottom of the element */ 292 | @keyframes scroll-shadow-inset { 293 | from { 294 | box-shadow: 295 | inset calc(var(--shadow-size) * -2) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color), 296 | inset calc(var(--shadow-size) * 0) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color); 297 | } 298 | 10%, 90% { 299 | box-shadow: 300 | inset calc(var(--shadow-size) * -1) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color), 301 | inset calc(var(--shadow-size) * 1) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color); 302 | } 303 | to { 304 | box-shadow: 305 | inset calc(var(--shadow-size) * 0) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color), 306 | inset calc(var(--shadow-size) * 2) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color); 307 | } 308 | } 309 | /* Spacing of components */ 310 | .space, .fullwidth { 311 | --layout-space: var(--layout-space-md); 312 | } 313 | .space--zero { 314 | --layout-space: 0; 315 | } 316 | .space--xxs { 317 | --layout-space: var(--layout-space-xxs); 318 | } 319 | .space--xs { 320 | --layout-space: var(--layout-space-xs); 321 | } 322 | .space--sm { 323 | --layout-space: var(--layout-space-sm); 324 | } 325 | .space--md { 326 | --layout-space: var(--layout-space-md); 327 | } 328 | .space--lg { 329 | --layout-space: var(--layout-space-lg); 330 | } 331 | .space--xl { 332 | --layout-space: var(--layout-space-xl); 333 | } 334 | .space--xxl { 335 | --layout-space: var(--layout-space-xxl); 336 | } 337 | .space { 338 | padding: var(--layout-space); 339 | } 340 | .space--block { 341 | padding-left: 0; 342 | padding-right: 0; 343 | } 344 | .space--inline { 345 | padding-top: 0; 346 | padding-bottom: 0; 347 | } 348 | /* Fullwidth */ 349 | .fullwidth { 350 | 351 | } 352 | @media (min-width: 576px) { 353 | .main, .footer { 354 | padding-left: 16px; 355 | padding-right: 16px; 356 | padding-left: var(--layout-gutter-inline); 357 | padding-right: var(--layout-gutter-inline); 358 | } 359 | } 360 | .container { 361 | max-width: 768px; 362 | max-width: var(--layout-breakpoint-md); 363 | padding-left: 16px; 364 | padding-right: 16px; 365 | padding-left: var(--layout-gutter-inline); 366 | padding-right: var(--layout-gutter-inline); 367 | margin-left: auto; 368 | margin-right: auto; 369 | } 370 | .fullwidth > .container { 371 | padding-top: var(--layout-space); 372 | padding-bottom: var(--layout-space); 373 | } 374 | /* Grid */ 375 | .row { 376 | display: flex; 377 | flex-wrap: wrap; 378 | margin-left: calc(-1 * 16px); 379 | margin-right: calc(-1 * 16px); 380 | margin-left: calc(-1 * var(--layout-gutter-inline)); 381 | margin-right: calc(-1 * var(--layout-gutter-inline)); 382 | } 383 | .row > * { 384 | flex-shrink: 0; 385 | width: 100%; 386 | max-width: 100%; 387 | padding-left: 16px; 388 | padding-right: 16px; 389 | padding-left: var(--layout-gutter-inline); 390 | padding-right: var(--layout-gutter-inline); 391 | } 392 | @media (min-width: 768px) { 393 | .col-md-4 { 394 | flex: 0 0 auto; 395 | width: 33.33333333%; 396 | } 397 | 398 | .col-md-8 { 399 | flex: 0 0 auto; 400 | width: 66.66666667%; 401 | } 402 | } 403 | @media (min-width: 992px) { 404 | .col-lg-4 { 405 | flex: 0 0 auto; 406 | width: 33.33333333%; 407 | } 408 | 409 | .col-lg-8 { 410 | flex: 0 0 auto; 411 | width: 66.66666667%; 412 | } 413 | } 414 | /* Flow */ 415 | .flow > * + * { 416 | margin-top: 1.5rem; 417 | margin-top: var(--flow-space, 1.5rem); 418 | } 419 | /* Large gap before headings and after h1 */ 420 | .flow .heading:has(h1) + * { 421 | --flow-space: var(--layout-space-lg); 422 | } 423 | .flow h1, .flow h2, .flow h3, .flow h4, .flow h1 + * { 424 | --flow-space: var(--layout-space-lg); 425 | } 426 | /* Medium gap if a heading follows a heading */ 427 | .flow h1 + h2, .flow h2 + h3, .flow h3 + h4, .flow .heading + h2, .flow .heading + h3, .flow .heading + h4 { 428 | --flow-space: var(--layout-space-md); 429 | } 430 | /* Small gap directly after heading and inside heading wrapper */ 431 | .flow h2:not(.does-not-exist) + *, .flow h3:not(.does-not-exist) + *, .flow h4:not(.does-not-exist) + *, .flow .heading + *, .flow.heading > * + * { 432 | --flow-space: var(--layout-space-sm); 433 | } 434 | /* Group */ 435 | .group, .nav { 436 | --layout-space: var(--layout-space-sm); 437 | 438 | display: flex; 439 | flex-wrap: wrap; 440 | gap: 16px; 441 | gap: var(--layout-space); 442 | } 443 | .group--min > * > *:first-child, .group--min > * > *:first-child > *:first-child { 444 | min-width: 144px; 445 | text-align: center; 446 | } 447 | /* ----- Buttons ----- */ 448 | .btn { 449 | --padding-inline: var(--layout-space-sm); 450 | --padding-block: var(--layout-space-xs); 451 | 452 | --text-color: #fff; 453 | --bg-color: var(--color-500); 454 | --border-color: var(--color-500); 455 | --border-size: 0px; 456 | 457 | --text-color-hover: #fff; 458 | --bg-color-hover: var(--color-700); 459 | --border-color-hover: var(--color-700); 460 | 461 | display: inline-flex; 462 | gap: .25em; 463 | justify-content: center; 464 | align-items: center; 465 | padding: 8px 16px; 466 | padding: var(--padding-block) var(--padding-inline); 467 | border: 0px solid rgb(1, 87, 255); 468 | border: var(--border-size) solid var(--border-color); 469 | background-color: rgb(1, 87, 255); 470 | background-color: var(--bg-color); 471 | transition: all .25s; 472 | 473 | color: #fff; 474 | 475 | color: var(--text-color); 476 | font-size: 1rem; 477 | line-height: 1.5; 478 | -webkit-text-decoration: none; 479 | text-decoration: none; 480 | text-align: center; 481 | cursor: pointer; 482 | 483 | position: relative; 484 | overflow: hidden; 485 | } 486 | .btn > span:not(.does-not-exist), .btn > .icon { 487 | position: relative; 488 | z-index: 1; 489 | } 490 | .btn > span { 491 | position: relative; 492 | z-index: 1; 493 | 494 | display: inline-flex; 495 | gap: .25em; 496 | justify-content: center; 497 | align-items: center; 498 | } 499 | .btn::after { 500 | content: ""; 501 | position: absolute; 502 | top: 0; 503 | left: 0; 504 | width: 100%; 505 | height: 100%; 506 | z-index: 0; 507 | background-color: var(--bg-color-hover); 508 | scale: 0 1; 509 | transform-origin: 100% 50%; 510 | transition-property: scale; 511 | transition-duration: inherit; 512 | transition-timing-function: cubic-bezier(.215, .610, .355, 1); 513 | transition-timing-function: var(--ease-out-cubic); 514 | } 515 | .btn:hover, .btn:focus, .btn.is-active, a:hover .btn { 516 | color: var(--text-color-hover); 517 | border-color: var(--border-color-hover); 518 | } 519 | .btn:hover::after { 520 | scale: 1; 521 | transform-origin: 0 50%; 522 | } 523 | .btn:focus::after { 524 | scale: 1; 525 | transform-origin: 0 50%; 526 | } 527 | .btn.is-active::after { 528 | scale: 1; 529 | transform-origin: 0 50%; 530 | } 531 | a:hover .btn::after { 532 | scale: 1; 533 | transform-origin: 0 50%; 534 | } 535 | .btn:disabled { 536 | pointer-events: none; 537 | opacity: .75; 538 | } 539 | .btn.disabled { 540 | pointer-events: none; 541 | opacity: .75; 542 | } 543 | .btn .icon, .btn svg { 544 | pointer-events: none; 545 | } 546 | .btn.btn--outline { 547 | --text-color: var(--color-500); 548 | } 549 | .btn--white { 550 | --text-color: var(--color-500); 551 | --bg-color: var(--color-50); 552 | --border-color: #fff; 553 | 554 | --text-color-hover: var(--color-700); 555 | --bg-color-hover: #fff; 556 | --border-color-hover: #fff; 557 | } 558 | .btn--white.btn--outline { 559 | --text-color: #fff; 560 | } 561 | .btn--green { 562 | --bg-color: var(--color-accent-500); 563 | --border-color: var(--color-accent-500); 564 | 565 | --bg-color-hover: var(--color-accent-700); 566 | --border-color-hover: var(--color-accent-700); 567 | } 568 | .btn--green.btn--outline { 569 | --text-color: var(--color-accent-700); 570 | } 571 | .btn--outline, .btn--ghost { 572 | --bg-color: transparent; 573 | --border-size: 1px; 574 | } 575 | .btn--ghost { 576 | --text-color: #fff; 577 | --text-color-hover: var(--color-700); 578 | --bg-color-hover: #fff; 579 | --border-color-hover: #fff; 580 | } 581 | .btn--icon, .btn--round { 582 | --padding-inline: 1rem; 583 | --padding-block: 1rem; 584 | } 585 | .btn--round { 586 | align-items: center; 587 | justify-content: center; 588 | border-radius: 50%; 589 | aspect-ratio: 1 / 1; 590 | } 591 | .btn--icon-multi:not(.is-active) .icon use:last-child, .btn--icon-multi.is-active .icon use:first-child { 592 | opacity: 0; 593 | } 594 | .btn--icon-multi:not(.is-active) .icon:last-of-type, .btn--icon-multi.is-active .icon:first-of-type { 595 | display: none; 596 | } 597 | .icon { 598 | display: inline-flex; 599 | justify-content: center; 600 | align-items: center; 601 | width: 1em; 602 | height: 1em; 603 | fill: currentColor; 604 | stroke: currentColor; 605 | transition: inherit; 606 | } 607 | .icon use { 608 | transition: inherit; 609 | } 610 | a .icon, button .icon { 611 | pointer-events: none; 612 | } 613 | /* ----- Header and footer ----- */ 614 | .header .container, .footer .container { 615 | --layout-space: var(--layout-space-sm); 616 | } 617 | .header { 618 | position: sticky; 619 | top: 0; 620 | z-index: 100; 621 | width: 100%; 622 | color: #fff; 623 | background-color: rgb(1, 87, 255); 624 | background-color: var(--color-500); 625 | border-bottom: 1px solid #fff; 626 | } 627 | @media (min-width: 768px) { 628 | .header { 629 | min-height: 80px; 630 | } 631 | } 632 | .header .container { 633 | display: flex; 634 | gap: 16px; 635 | gap: var(--layout-space-sm); 636 | flex-wrap: wrap; 637 | align-items: center; 638 | } 639 | .header a:not(.btn) { 640 | color: #fff; 641 | -webkit-text-decoration: none; 642 | text-decoration: none; 643 | } 644 | .header a:not(.btn):hover, .header a:not(.btn):focus { 645 | color: rgb(223, 234, 255); 646 | color: color(display-p3 0.8822 0.91623 0.99269); 647 | color: var(--color-100); 648 | } 649 | .logo-text { 650 | flex: 1 0 0%; 651 | margin: 0; 652 | font-family: 'Source Code Pro', monospace; 653 | font-family: var(--heading-font-family); 654 | color: #fff; 655 | 656 | font-size: 2.5rem; 657 | font-size: clamp(2rem, 1.5229rem + 2.0356vw, 2.5rem); 658 | } 659 | .header-nav { 660 | margin-left: auto; 661 | } 662 | .header-nav:not(:has(.btn)) { 663 | row-gap: 0; 664 | } 665 | @media (max-width: 575px) { 666 | .header-nav { 667 | width: 100%; 668 | } 669 | } 670 | .header-nav a:not(.btn) { 671 | position: relative; 672 | color: rgb(223, 234, 255); 673 | color: color(display-p3 0.8822 0.91623 0.99269); 674 | color: var(--color-100); 675 | text-transform: uppercase; 676 | 677 | } 678 | .header-nav a:not(.btn)::after { 679 | content: ""; 680 | position: absolute; 681 | bottom: 0; 682 | left: 0; 683 | width: 100%; 684 | height: 2px; 685 | transition: transform .2s ease-in-out; 686 | 687 | z-index: -1; 688 | background-color: currentColor; 689 | transform: scaleX(0); 690 | transform-origin: 100% 50%; 691 | transition-timing-function: cubic-bezier(.65, .05, .36, 1); 692 | transition-timing-function: var(--ease-in-out-cubic); 693 | } 694 | .header-nav a:not(.btn):hover, .header-nav a:not(.btn):focus, .header-nav a.is-active:not(.btn) { 695 | color: #fff; 696 | } 697 | .header-nav a:not(.btn):hover::after { 698 | transform: scaleX(1); 699 | transform-origin: 0 50%; 700 | } 701 | .header-nav a:not(.btn):focus::after { 702 | transform: scaleX(1); 703 | transform-origin: 0 50%; 704 | } 705 | .header-nav a.is-active:not(.btn)::after { 706 | transform: scaleX(1); 707 | transform-origin: 0 50%; 708 | } 709 | @media (max-width: 575px) { 710 | .header-nav .btn { 711 | width: calc(50% - (var(--layout-space) / 2)); 712 | --padding-block: var(--layout-space-xxs); 713 | } 714 | } 715 | .footer { 716 | text-align: center; 717 | } 718 | .footer .container > * { 719 | padding-top: var(--layout-space); 720 | border-top: 2px solid rgb(1, 87, 255); 721 | border-top: 2px solid oklch(53.67% 0.257 262.51); 722 | border-top: 2px solid var(--color-500); 723 | } 724 | .footer .container > *:not(:first-child) { 725 | margin-top: var(--layout-space); 726 | } 727 | .footer .group, .footer .nav { 728 | align-items: center; 729 | justify-content: center; 730 | } 731 | @media (max-width: 575px) { 732 | .footer .share-title { 733 | width: 100%; 734 | } 735 | } 736 | .footer-nav { 737 | row-gap: 8px; 738 | row-gap: var(--layout-space-xs); 739 | font-size: .875rem; 740 | } 741 | /* Page heading */ 742 | .page-intro { 743 | display: flex; 744 | gap: 32px; 745 | gap: var(--layout-space-md); 746 | align-items: center; 747 | } 748 | .page-heading { 749 | flex-grow: 1; 750 | text-wrap: pretty; 751 | } 752 | .page-intro-img { 753 | width: 80px; 754 | height: auto; 755 | border-radius: 50%; 756 | } 757 | @media (min-width: 576px) { 758 | .columns-sm-2 { 759 | -moz-column-count: 2; 760 | column-count: 2; 761 | -moz-column-gap: 32px; 762 | column-gap: 32px; 763 | -moz-column-gap: var(--layout-space-md); 764 | column-gap: var(--layout-space-md); 765 | } 766 | } 767 | .columns-sm-2 > * { 768 | page-break-inside: avoid; 769 | -moz-column-break-inside: avoid; 770 | break-inside: avoid; 771 | text-wrap: pretty; 772 | } 773 | .demo--collapse .toggle { 774 | width: 100%; 775 | } 776 | .demo--accordion .toggle-outer, .demo--tabs .toggle-outer { 777 | border-bottom: var(--toggle-border-width) solid var(--toggle-border-color); 778 | } 779 | .demo--accordion .toggle { 780 | display: flex; 781 | align-items: center; 782 | gap: 8px; 783 | width: 100%; 784 | } 785 | .demo--accordion .toggle-text { 786 | margin: -.5em 0; 787 | margin-left: auto; 788 | font-size: 2rem; 789 | } 790 | .demo--tabs .toggle-outer { 791 | display: flex; 792 | } 793 | .demo--tabs .toggle { 794 | border: var(--toggle-border-width) solid var(--toggle-border-color); 795 | border-bottom: none; 796 | } 797 | .demo--tabs .toggle:not(:last-child) { 798 | border-right: none; 799 | } 800 | .demo--menu .toggle, .demo--dropdown .toggle { 801 | display: flex; 802 | align-items: center; 803 | gap: 8px; 804 | } 805 | .demo--menu .toggle span, .demo--dropdown .toggle span { 806 | rotate: -90deg; 807 | scale: .5 1; 808 | margin: -.5em 0; 809 | margin-left: auto; 810 | font-size: 1.5rem; 811 | font-weight: 600; 812 | transition: all .35s ease-out; 813 | } 814 | .demo--menu .toggle.is-active span, .demo--dropdown .toggle.is-active span { 815 | rotate: 90deg; 816 | } 817 | .demo--menu .toggle-panel-content, .demo--dropdown .toggle-panel-content { 818 | border: var(--toggle-border-width) solid var(--toggle-border-color); 819 | } 820 | .demo--dropdown .toggle-outer { 821 | display: inline-block; 822 | } 823 | .demo--dropdown .toggle-panel { 824 | width: 100%; 825 | } 826 | .demo--menu > .toggle-outer { 827 | display: flex; 828 | gap: var(--toggle-padding-sm); 829 | align-items: center; 830 | padding: var(--toggle-padding-sm); 831 | border: var(--toggle-border-width) solid var(--toggle-border-color); 832 | } 833 | .demo--menu .toggle:last-child { 834 | margin-left: auto; 835 | } 836 | @media (min-width: 992px) { 837 | .demo--menu .toggle:last-child { 838 | display: none; 839 | } 840 | } 841 | .demo--menu .menu a { 842 | display: block; 843 | } 844 | @media (max-width: 991px) { 845 | .demo--menu .menu { 846 | position: absolute; 847 | top: 100%; 848 | left: 0; 849 | z-index: 1000; 850 | width: 100%; 851 | 852 | left: calc(var(--toggle-border-width) * -1); 853 | width: calc(100% + (var(--toggle-border-width) * 2)); 854 | border: var(--toggle-border-width) solid var(--toggle-border-color); 855 | border-top: none; 856 | } 857 | } 858 | @media (min-width: 992px) { 859 | .demo--menu .menu { 860 | display: flex; 861 | gap: var(--toggle-padding-sm); 862 | height: auto; 863 | overflow: visible; 864 | } 865 | } 866 | @media (max-width: 991px) { 867 | .demo--menu .menu .toggle, .demo--menu .menu .toggle-panel { 868 | width: 100%; 869 | } 870 | } 871 | @media (max-width: 991px) { 872 | .demo--menu .menu .toggle-panel-content { 873 | border: none; 874 | } 875 | } 876 | @media (min-width: 992px) { 877 | .demo--menu .menu .toggle-panel { 878 | width: -moz-max-content; 879 | width: max-content; 880 | position: absolute; 881 | top: 100%; 882 | left: 0; 883 | z-index: 1000; 884 | } 885 | } 886 | .demo--dialog .toggle-dialog-content, .demo--modal .toggle-dialog-content, .demo--popover .toggle-dialog-content { 887 | padding: var(--toggle-padding-lg); 888 | border: var(--toggle-border-width) solid var(--toggle-border-color); 889 | box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.2); 890 | } 891 | .demo--dialog .toggle-dialog > .toggle, .demo--modal .toggle-dialog > .toggle, .demo--popover .toggle-dialog > .toggle, .demo--dialog .toggle-dialog-content > .toggle, .demo--modal .toggle-dialog-content > .toggle, .demo--popover .toggle-dialog-content > .toggle { 892 | position: absolute; 893 | top: 0; 894 | right: 0; 895 | width: 1.375em; 896 | padding: var(--toggle-padding-xs) var(--toggle-padding-sm); 897 | aspect-ratio: 1; 898 | font-size: 2rem; 899 | background: none; 900 | z-index: 100; 901 | } 902 | .demo--tooltip .toggle-tooltip { 903 | border: var(--toggle-border-width) solid var(--toggle-border-color); 904 | box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.2); 905 | } 906 | /* Social image */ 907 | /* body { 908 | scale: 1.5; 909 | transform-origin: top center; 910 | } 911 | 912 | .header-nav { 913 | display: none; 914 | } 915 | 916 | .page-intro { 917 | padding-block: 40px 74px; 918 | } 919 | 920 | .page-intro-img { 921 | width: 159px; 922 | } */ 923 | /* Portfolio Image */ 924 | /* body { 925 | scale: 1.565; 926 | transform-origin: top center; 927 | } 928 | 929 | .header { 930 | display: none; 931 | } */ 932 | -------------------------------------------------------------------------------- /src/styles/site/base.css: -------------------------------------------------------------------------------- 1 | /* Type and background */ 2 | body { 3 | font-family: var(--body-font-family); 4 | color: var(--color-700); 5 | background-color: #fff; 6 | background-image: repeating-linear-gradient(90deg, var(--bg-grid-color) 0, var(--bg-grid-color) var(--bg-grid-line), transparent 0, transparent 50%), repeating-linear-gradient(180deg, var(--bg-grid-color) 0, var(--bg-grid-color) var(--bg-grid-line), transparent 0, transparent 50%); 7 | background-size: var(--bg-grid-box) var(--bg-grid-box); 8 | background-position: calc(50% - (var(--bg-grid-line)/2)) top; 9 | } 10 | 11 | .content { 12 | background-color: #fff; 13 | border-inline: calc(var(--bg-grid-line)/2) solid var(--bg-grid-color); 14 | } 15 | 16 | ::selection { 17 | color: #fff; 18 | background-color: var(--color-700); 19 | } 20 | 21 | h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 { 22 | font-family: var(--heading-font-family); 23 | font-weight: 400; 24 | word-spacing: -.5ch; 25 | } 26 | 27 | h1, h2, h3 { 28 | letter-spacing: -.025em; 29 | } 30 | 31 | h1, h2 { 32 | font-weight: 600; 33 | } 34 | 35 | /* Clamp from 375 - 768px */ 36 | h1 { font-size: 3rem; font-size: clamp(2.5rem, 2.0229rem + 2.0356vw, 3rem); } 37 | h2 { font-size: 2rem; font-size: clamp(1.5rem, 1.0229rem + 2.0356vw, 2rem); } 38 | h3 { font-size: 1.5rem; font-size: clamp(1.25rem, 1.0115rem + 1.0178vw, 1.5rem); } 39 | h4 { font-size: 1.25rem; font-size: clamp(1.125rem, 1.0057rem + 0.5089vw, 1.25rem); } 40 | h5 { font-size: 1rem; } 41 | h6 { font-size: 1rem; } 42 | 43 | p { 44 | 45 | } 46 | 47 | ul, ol { 48 | padding-inline-start: var(--layout-space-md); 49 | } 50 | 51 | pre, code { 52 | font-family: var(--heading-font-family); 53 | } 54 | 55 | a { 56 | color: var(--link-color); 57 | transition: color 0.3s; 58 | 59 | &:hover, &:focus { 60 | color: var(--link-color-hover); 61 | } 62 | 63 | &:focus-within { 64 | outline: 2px solid var(--link-color); 65 | } 66 | } 67 | 68 | 69 | /* Elements and utilities */ 70 | .img-fluid { 71 | max-width: 100%; 72 | height: auto; 73 | } 74 | 75 | .w-100 { 76 | width: 100%; 77 | } 78 | 79 | 80 | code { 81 | padding: .125rem; 82 | background-color: var(--color-50); 83 | 84 | pre & { 85 | display: block; 86 | padding: 1rem; 87 | white-space: pre; 88 | overflow: auto; 89 | border: 1px solid var(--color-500); 90 | } 91 | } 92 | 93 | 94 | .table-outer { 95 | display: block; 96 | width: 100%; 97 | } 98 | 99 | .table { 100 | width: 100%; 101 | border-collapse: collapse; 102 | 103 | th, td { 104 | padding: var(--layout-space-sm); 105 | text-align: left; 106 | vertical-align: top; 107 | border: 1px solid var(--color-500); 108 | } 109 | 110 | th { 111 | background-color: var(--color-100); 112 | } 113 | } 114 | 115 | /* Inline overflow elements */ 116 | /* https://daverupert.com/2023/08/animation-timeline-scroll-shadows/ */ 117 | .overflow-inline { 118 | --shadow-color: rgb(0 0 0 / .15); 119 | --shadow-size: 8px; 120 | --shadow-spread: calc(var(--shadow-size) * -.5); 121 | 122 | animation: scroll-shadow-inset linear; 123 | scroll-timeline: --scroll-timeline inline; 124 | animation-timeline: --scroll-timeline; 125 | 126 | overflow-x: auto; 127 | overflow-inline: auto; 128 | border: 1px solid var(--color-500); 129 | 130 | > * { 131 | mix-blend-mode: multiply; 132 | } 133 | 134 | &.table-outer { 135 | border-inline: 2px solid var(--color-500); 136 | 137 | th, td { 138 | &:first-child { 139 | border-inline-start: none; 140 | } 141 | 142 | &:last-child { 143 | border-inline-end: none; 144 | } 145 | } 146 | } 147 | } 148 | 149 | /* Negative spread to stop the shadow on the top and bottom of the element */ 150 | @keyframes scroll-shadow-inset { 151 | from { 152 | box-shadow: 153 | inset calc(var(--shadow-size) * -2) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color), 154 | inset calc(var(--shadow-size) * 0) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color); 155 | } 156 | 10%, 90% { 157 | box-shadow: 158 | inset calc(var(--shadow-size) * -1) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color), 159 | inset calc(var(--shadow-size) * 1) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color); 160 | } 161 | to { 162 | box-shadow: 163 | inset calc(var(--shadow-size) * 0) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color), 164 | inset calc(var(--shadow-size) * 2) 0 var(--shadow-size) var(--shadow-spread) var(--shadow-color); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/styles/site/components.css: -------------------------------------------------------------------------------- 1 | /* ----- Buttons ----- */ 2 | .btn { 3 | --padding-inline: var(--layout-space-sm); 4 | --padding-block: var(--layout-space-xs); 5 | 6 | --text-color: #fff; 7 | --bg-color: var(--color-500); 8 | --border-color: var(--color-500); 9 | --border-size: 0px; 10 | 11 | --text-color-hover: #fff; 12 | --bg-color-hover: var(--color-700); 13 | --border-color-hover: var(--color-700); 14 | 15 | display: inline-flex; 16 | gap: .25em; 17 | justify-content: center; 18 | align-items: center; 19 | padding: var(--padding-block) var(--padding-inline); 20 | border: var(--border-size) solid var(--border-color); 21 | background-color: var(--bg-color); 22 | transition: all .25s; 23 | 24 | color: var(--text-color); 25 | font-size: 1rem; 26 | line-height: 1.5; 27 | text-decoration: none; 28 | text-align: center; 29 | cursor: pointer; 30 | 31 | position: relative; 32 | overflow: hidden; 33 | 34 | > :is(span, .icon) { 35 | position: relative; 36 | z-index: 1; 37 | } 38 | 39 | > span { 40 | position: relative; 41 | z-index: 1; 42 | 43 | display: inline-flex; 44 | gap: .25em; 45 | justify-content: center; 46 | align-items: center; 47 | } 48 | 49 | &::after { 50 | content: ""; 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | width: 100%; 55 | height: 100%; 56 | z-index: 0; 57 | background-color: var(--bg-color-hover); 58 | scale: 0 1; 59 | transform-origin: 100% 50%; 60 | transition-property: scale; 61 | transition-duration: inherit; 62 | transition-timing-function: var(--ease-out-cubic); 63 | } 64 | 65 | &:is(:hover, :focus, .is-active), a:hover & { 66 | color: var(--text-color-hover); 67 | border-color: var(--border-color-hover); 68 | 69 | &::after { 70 | scale: 1; 71 | transform-origin: 0 50%; 72 | } 73 | } 74 | 75 | &:is(.disabled, :disabled) { 76 | pointer-events: none; 77 | opacity: .75; 78 | } 79 | 80 | & .icon, & svg { 81 | pointer-events: none; 82 | } 83 | 84 | &.btn--outline { 85 | --text-color: var(--color-500); 86 | } 87 | } 88 | 89 | 90 | .btn--white { 91 | --text-color: var(--color-500); 92 | --bg-color: var(--color-50); 93 | --border-color: #fff; 94 | 95 | --text-color-hover: var(--color-700); 96 | --bg-color-hover: #fff; 97 | --border-color-hover: #fff; 98 | 99 | &.btn--outline { 100 | --text-color: #fff; 101 | } 102 | } 103 | 104 | .btn--green { 105 | --bg-color: var(--color-accent-500); 106 | --border-color: var(--color-accent-500); 107 | 108 | --bg-color-hover: var(--color-accent-700); 109 | --border-color-hover: var(--color-accent-700); 110 | 111 | &.btn--outline { 112 | --text-color: var(--color-accent-700); 113 | } 114 | } 115 | 116 | 117 | .btn--outline, .btn--ghost { 118 | --bg-color: transparent; 119 | --border-size: 1px; 120 | } 121 | 122 | .btn--ghost { 123 | --text-color: #fff; 124 | --text-color-hover: var(--color-700); 125 | --bg-color-hover: #fff; 126 | --border-color-hover: #fff; 127 | } 128 | 129 | 130 | .btn--icon, .btn--round { 131 | --padding-inline: 1rem; 132 | --padding-block: 1rem; 133 | } 134 | 135 | .btn--round { 136 | align-items: center; 137 | justify-content: center; 138 | border-radius: 50%; 139 | aspect-ratio: 1 / 1; 140 | } 141 | 142 | .btn--icon-multi { 143 | &:not(.is-active) .icon use:last-child, &.is-active .icon use:first-child { 144 | opacity: 0; 145 | } 146 | &:not(.is-active) .icon:last-of-type, &.is-active .icon:first-of-type { 147 | display: none; 148 | } 149 | } 150 | 151 | 152 | .icon { 153 | display: inline-flex; 154 | justify-content: center; 155 | align-items: center; 156 | width: 1em; 157 | height: 1em; 158 | fill: currentColor; 159 | stroke: currentColor; 160 | transition: inherit; 161 | 162 | & use { 163 | transition: inherit; 164 | } 165 | } 166 | 167 | a, button { 168 | & .icon { 169 | pointer-events: none; 170 | } 171 | } 172 | 173 | 174 | 175 | /* ----- Header and footer ----- */ 176 | .header, .footer { 177 | .container { 178 | --layout-space: var(--layout-space-sm); 179 | } 180 | } 181 | 182 | .header { 183 | position: sticky; 184 | top: 0; 185 | z-index: 100; 186 | width: 100%; 187 | color: #fff; 188 | background-color: var(--color-500); 189 | border-bottom: 1px solid #fff; 190 | 191 | @media (--viewport-md-up) { 192 | min-height: 80px; 193 | } 194 | 195 | .container { 196 | display: flex; 197 | gap: var(--layout-space-sm); 198 | flex-wrap: wrap; 199 | align-items: center; 200 | } 201 | 202 | a:not(.btn) { 203 | color: #fff; 204 | text-decoration: none; 205 | 206 | &:is(:hover, :focus) { 207 | color: var(--color-100); 208 | } 209 | } 210 | } 211 | 212 | .logo-text { 213 | flex: 1 0 0%; 214 | margin: 0; 215 | font-family: var(--heading-font-family); 216 | color: #fff; 217 | 218 | font-size: 2.5rem; 219 | font-size: clamp(2rem, 1.5229rem + 2.0356vw, 2.5rem); 220 | } 221 | 222 | 223 | .header-nav { 224 | margin-inline-start: auto; 225 | 226 | &:not(:has(.btn)) { 227 | row-gap: 0; 228 | } 229 | 230 | @media (--viewport-sm-down) { 231 | width: 100%; 232 | } 233 | 234 | a:not(.btn) { 235 | position: relative; 236 | color: var(--color-100); 237 | text-transform: uppercase; 238 | 239 | &::after { 240 | content: ""; 241 | position: absolute; 242 | bottom: 0; 243 | left: 0; 244 | width: 100%; 245 | height: 2px; 246 | transition: transform .2s ease-in-out; 247 | 248 | z-index: -1; 249 | background-color: currentColor; 250 | transform: scaleX(0); 251 | transform-origin: 100% 50%; 252 | transition-timing-function: var(--ease-in-out-cubic); 253 | } 254 | 255 | &:is(:hover, :focus, .is-active) { 256 | color: #fff; 257 | 258 | &::after { 259 | transform: scaleX(1); 260 | transform-origin: 0 50%; 261 | } 262 | } 263 | 264 | } 265 | 266 | .btn { 267 | @media (--viewport-sm-down) { 268 | width: calc(50% - (var(--layout-space) / 2)); 269 | --padding-block: var(--layout-space-xxs); 270 | } 271 | } 272 | } 273 | 274 | 275 | .footer { 276 | text-align: center; 277 | 278 | .container > * { 279 | &:not(:first-child) { 280 | margin-block-start: var(--layout-space); 281 | } 282 | padding-block-start: var(--layout-space); 283 | border-block-start: 2px solid var(--color-500); 284 | } 285 | 286 | .group, .nav { 287 | align-items: center; 288 | justify-content: center; 289 | } 290 | 291 | .share-title { 292 | @media (--viewport-sm-down) { 293 | width: 100%; 294 | } 295 | } 296 | } 297 | 298 | .footer-nav { 299 | row-gap: var(--layout-space-xs); 300 | font-size: .875rem; 301 | } 302 | 303 | 304 | /* Page heading */ 305 | .page-intro { 306 | display: flex; 307 | gap: var(--layout-space-md); 308 | align-items: center; 309 | } 310 | 311 | .page-heading { 312 | flex-grow: 1; 313 | text-wrap: pretty; 314 | } 315 | 316 | .page-intro-img { 317 | width: 80px; 318 | height: auto; 319 | border-radius: 50%; 320 | } 321 | 322 | .columns-sm-2 { 323 | @media (--viewport-sm-up) { 324 | column-count: 2; 325 | column-gap: var(--layout-space-md); 326 | } 327 | 328 | > * { 329 | break-inside: avoid; 330 | text-wrap: pretty; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/styles/site/demo.css: -------------------------------------------------------------------------------- 1 | .demo--collapse { 2 | .toggle { 3 | width: 100%; 4 | } 5 | } 6 | 7 | 8 | .demo--accordion, .demo--tabs { 9 | .toggle-outer { 10 | border-bottom: var(--toggle-border-width) solid var(--toggle-border-color); 11 | } 12 | } 13 | 14 | .demo--accordion { 15 | .toggle { 16 | display: flex; 17 | align-items: center; 18 | gap: 8px; 19 | width: 100%; 20 | } 21 | 22 | .toggle-text { 23 | margin: -.5em 0; 24 | margin-inline-start: auto; 25 | font-size: 2rem; 26 | } 27 | } 28 | 29 | .demo--tabs { 30 | .toggle-outer { 31 | display: flex; 32 | } 33 | 34 | .toggle { 35 | border: var(--toggle-border-width) solid var(--toggle-border-color); 36 | border-bottom: none; 37 | } 38 | 39 | .toggle:not(:last-child) { 40 | border-right: none; 41 | } 42 | } 43 | 44 | 45 | .demo--menu, .demo--dropdown { 46 | .toggle { 47 | display: flex; 48 | align-items: center; 49 | gap: 8px; 50 | 51 | span { 52 | rotate: -90deg; 53 | scale: .5 1; 54 | margin: -.5em 0; 55 | margin-inline-start: auto; 56 | font-size: 1.5rem; 57 | font-weight: 600; 58 | transition: all .35s ease-out; 59 | } 60 | 61 | &.is-active { 62 | span { 63 | rotate: 90deg; 64 | } 65 | } 66 | } 67 | 68 | .toggle-panel-content { 69 | border: var(--toggle-border-width) solid var(--toggle-border-color); 70 | } 71 | } 72 | 73 | 74 | 75 | .demo--dropdown { 76 | .toggle-outer { 77 | display: inline-block; 78 | } 79 | 80 | .toggle-panel { 81 | width: 100%; 82 | } 83 | } 84 | 85 | 86 | .demo--menu { 87 | > .toggle-outer { 88 | display: flex; 89 | gap: var(--toggle-padding-sm); 90 | align-items: center; 91 | padding: var(--toggle-padding-sm); 92 | border: var(--toggle-border-width) solid var(--toggle-border-color); 93 | } 94 | 95 | .toggle:last-child { 96 | margin-left: auto; 97 | 98 | @media (min-width: 992px) { 99 | display: none; 100 | } 101 | } 102 | 103 | .menu { 104 | a { 105 | display: block; 106 | } 107 | 108 | @media (max-width: 991px) { 109 | position: absolute; 110 | top: 100%; 111 | left: 0; 112 | z-index: 1000; 113 | width: 100%; 114 | 115 | left: calc(var(--toggle-border-width) * -1); 116 | width: calc(100% + (var(--toggle-border-width) * 2)); 117 | border: var(--toggle-border-width) solid var(--toggle-border-color); 118 | border-top: none; 119 | } 120 | 121 | @media (min-width: 992px) { 122 | display: flex; 123 | gap: var(--toggle-padding-sm); 124 | height: auto; 125 | overflow: visible; 126 | } 127 | } 128 | 129 | .menu .toggle, .menu .toggle-panel { 130 | @media (max-width: 991px) { 131 | width: 100%; 132 | } 133 | } 134 | 135 | .menu .toggle-panel-content { 136 | @media (max-width: 991px) { 137 | border: none; 138 | } 139 | } 140 | 141 | .menu .toggle-panel { 142 | @media (min-width: 992px) { 143 | width: max-content; 144 | position: absolute; 145 | top: 100%; 146 | left: 0; 147 | z-index: 1000; 148 | } 149 | } 150 | } 151 | 152 | 153 | .demo--dialog, .demo--modal, .demo--popover { 154 | .toggle-dialog-content { 155 | padding: var(--toggle-padding-lg); 156 | border: var(--toggle-border-width) solid var(--toggle-border-color); 157 | box-shadow: 4px 4px 0px rgba(0 0 0 / 0.2); 158 | } 159 | 160 | .toggle-dialog > .toggle, .toggle-dialog-content > .toggle { 161 | position: absolute; 162 | top: 0; 163 | right: 0; 164 | width: 1.375em; 165 | padding: var(--toggle-padding-xs) var(--toggle-padding-sm); 166 | aspect-ratio: 1; 167 | font-size: 2rem; 168 | background: none; 169 | z-index: 100; 170 | } 171 | } 172 | 173 | 174 | .demo--tooltip { 175 | .toggle-tooltip { 176 | border: var(--toggle-border-width) solid var(--toggle-border-color); 177 | box-shadow: 2px 2px 0px rgba(0 0 0 / 0.2); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/styles/site/layout.css: -------------------------------------------------------------------------------- 1 | /* Spacing of components */ 2 | .space, .fullwidth { 3 | --layout-space: var(--layout-space-md); 4 | } 5 | 6 | 7 | .space--zero { 8 | --layout-space: 0; 9 | } 10 | .space--xxs { 11 | --layout-space: var(--layout-space-xxs); 12 | } 13 | .space--xs { 14 | --layout-space: var(--layout-space-xs); 15 | } 16 | .space--sm { 17 | --layout-space: var(--layout-space-sm); 18 | } 19 | .space--md { 20 | --layout-space: var(--layout-space-md); 21 | } 22 | .space--lg { 23 | --layout-space: var(--layout-space-lg); 24 | } 25 | .space--xl { 26 | --layout-space: var(--layout-space-xl); 27 | } 28 | .space--xxl { 29 | --layout-space: var(--layout-space-xxl); 30 | } 31 | 32 | .space { 33 | padding: var(--layout-space); 34 | } 35 | 36 | .space--block { 37 | padding-inline: 0; 38 | } 39 | 40 | .space--inline { 41 | padding-block: 0; 42 | } 43 | 44 | 45 | 46 | /* Fullwidth */ 47 | .fullwidth { 48 | 49 | } 50 | 51 | .main, .footer { 52 | @media (--viewport-sm-up) { 53 | padding-inline: var(--layout-gutter-inline); 54 | } 55 | } 56 | 57 | .container { 58 | max-width: var(--layout-breakpoint-md); 59 | padding-inline: var(--layout-gutter-inline); 60 | margin-inline: auto; 61 | } 62 | 63 | .fullwidth > .container { 64 | padding-block: var(--layout-space); 65 | } 66 | 67 | 68 | /* Grid */ 69 | .row { 70 | display: flex; 71 | flex-wrap: wrap; 72 | margin-inline: calc(-1 * var(--layout-gutter-inline)); 73 | 74 | & > * { 75 | flex-shrink: 0; 76 | width: 100%; 77 | max-width: 100%; 78 | padding-inline: var(--layout-gutter-inline); 79 | } 80 | } 81 | 82 | @media (--viewport-md-up) { 83 | .col-md-4 { 84 | flex: 0 0 auto; 85 | width: 33.33333333%; 86 | } 87 | 88 | .col-md-8 { 89 | flex: 0 0 auto; 90 | width: 66.66666667%; 91 | } 92 | } 93 | 94 | @media (--viewport-lg-up) { 95 | .col-lg-4 { 96 | flex: 0 0 auto; 97 | width: 33.33333333%; 98 | } 99 | 100 | .col-lg-8 { 101 | flex: 0 0 auto; 102 | width: 66.66666667%; 103 | } 104 | } 105 | 106 | 107 | /* Flow */ 108 | .flow > * + * { 109 | margin-block-start: var(--flow-space, 1.5rem); 110 | } 111 | 112 | .flow { 113 | /* Large gap before headings and after h1 */ 114 | h1, h2, h3, h4, h1 + *, :is(.heading:has(h1)) + * { 115 | --flow-space: var(--layout-space-lg); 116 | } 117 | 118 | /* Medium gap if a heading follows a heading */ 119 | h1 + h2, h2 + h3, h3 + h4, .heading + :is(h2, h3, h4) { 120 | --flow-space: var(--layout-space-md); 121 | } 122 | 123 | /* Small gap directly after heading and inside heading wrapper */ 124 | :is(h2, h3, h4, .heading) + *, &.heading > * + * { 125 | --flow-space: var(--layout-space-sm); 126 | } 127 | } 128 | 129 | 130 | 131 | /* Group */ 132 | .group, .nav { 133 | --layout-space: var(--layout-space-sm); 134 | 135 | display: flex; 136 | flex-wrap: wrap; 137 | gap: var(--layout-space); 138 | } 139 | 140 | .group--min { 141 | > * > *:first-child { 142 | &, & > *:first-child { 143 | min-width: 144px; 144 | text-align: center; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/styles/site/main.css: -------------------------------------------------------------------------------- 1 | @import "reset.css"; 2 | @import "variables.css"; 3 | @import "base.css"; 4 | @import "layout.css"; 5 | 6 | @import "components.css"; 7 | @import "demo.css"; 8 | 9 | /* Social image */ 10 | /* body { 11 | scale: 1.5; 12 | transform-origin: top center; 13 | } 14 | 15 | .header-nav { 16 | display: none; 17 | } 18 | 19 | .page-intro { 20 | padding-block: 40px 74px; 21 | } 22 | 23 | .page-intro-img { 24 | width: 159px; 25 | } */ 26 | 27 | /* Portfolio Image */ 28 | /* body { 29 | scale: 1.565; 30 | transform-origin: top center; 31 | } 32 | 33 | .header { 34 | display: none; 35 | } */ 36 | -------------------------------------------------------------------------------- /src/styles/site/reset.css: -------------------------------------------------------------------------------- 1 | /* https://www.joshwcomeau.com/css/custom-css-reset/, https://andy-bell.co.uk/a-more-modern-css-reset/ */ 2 | *, *::before, *::after { 3 | box-sizing: border-box; 4 | } 5 | * { 6 | margin: 0; 7 | } 8 | 9 | html { 10 | -moz-text-size-adjust: none; 11 | -webkit-text-size-adjust: none; 12 | text-size-adjust: none; 13 | } 14 | 15 | body { 16 | line-height: 1.5; 17 | -webkit-font-smoothing: antialiased; 18 | 19 | min-height: 100vh; 20 | } 21 | img, picture, video, canvas, svg { 22 | display: block; 23 | max-width: 100%; 24 | } 25 | input, button, textarea, select { 26 | font: inherit; 27 | } 28 | p, h1, h2, h3, h4, h5, h6 { 29 | overflow-wrap: break-word; 30 | } 31 | 32 | /* Opinionated */ 33 | h1, h2, h3, h4, 34 | button, input, label { 35 | line-height: 1.2; 36 | } 37 | 38 | h1, h2, h3, h4 { 39 | text-wrap: balance; 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/site/variables.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Source Code Pro'; 3 | src: url('../fonts/source-code-pro.woff2') format('woff2'); 4 | display: swap; 5 | } 6 | 7 | @font-face { 8 | font-family: 'Work Sans'; 9 | src: url('../fonts/work-sans.woff2') format('woff2'); 10 | display: swap; 11 | } 12 | 13 | 14 | :root { 15 | /* Colours */ 16 | /* https://oklch-palette.vercel.app/#53.67,0.257,262.51,100, https://oklch.com/#53.67,0.257,262.51,100 */ 17 | --color-50: oklch(97.89% 0.01 267.36); 18 | --color-100: oklch(93.51% 0.031 263.52); 19 | --color-200: oklch(79.69% 0.102 262.19); 20 | --color-300: oklch(71% 0.151 262.38); 21 | --color-400: oklch(62.26% 0.203 262.51); 22 | --color-500: oklch(53.67% 0.257 262.51); 23 | --color-600: oklch(47.18% 0.226 262.46); 24 | --color-700: oklch(38.31% 0.185 262.52); 25 | --color-800: oklch(29.19% 0.141 262.52); 26 | --color-900: oklch(20.92% 0.101 262.51); 27 | 28 | --color-accent-500: oklch(77.31% 0.136 177.29); 29 | --color-accent-700: oklch(50.9% 0.094 177.27); 30 | 31 | /* Type */ 32 | --sans-serif-font-family: system-ui, Arial, sans-serif; 33 | --serif-font-family: 'Times New Roman', Times, serif; 34 | /* --body-font-family: 'Work Sans', var(--sans-serif-font-family); 35 | --heading-font-family: 'Source Code Pro', var(--sans-serif-font-family); */ 36 | --body-font-family: 'Work Sans', sans-serif; 37 | --heading-font-family: 'Source Code Pro', monospace; 38 | 39 | --text-color: var(--color-700); 40 | --link-color: var(--color-500); 41 | --link-color-hover: var(--color-700); 42 | 43 | /* Layout */ 44 | --layout-breakpoint-xs: 0; 45 | --layout-breakpoint-sm: 576px; 46 | --layout-breakpoint-md: 768px; 47 | --layout-breakpoint-lg: 992px; 48 | --layout-breakpoint-xl: 1200px; 49 | --layout-breakpoint-xxl: 1400px; 50 | 51 | --layout-gutter-inline: 16px; 52 | --layout-gutter-block: 0px; 53 | 54 | --layout-space-xxs: 4px; 55 | --layout-space-xs: 8px; 56 | --layout-space-sm: 16px; 57 | --layout-space-md: 32px; 58 | --layout-space-lg: 48px; 59 | --layout-space-xl: 64px; 60 | --layout-space-xxl: 80px; 61 | 62 | --ease-out-cubic: cubic-bezier(.215, .610, .355, 1); 63 | --ease-in-out-cubic: cubic-bezier(.65, .05, .36, 1); 64 | 65 | --bg-grid-color: rgba(238, 238, 238, .75); 66 | --bg-grid-line: 2px; 67 | --bg-grid-box: 48px; 68 | 69 | @media (--viewport-md-up) { 70 | --layout-gutter-inline: 24px; 71 | } 72 | } 73 | 74 | @supports not (background-color: oklch(0%, 0, 0)) { 75 | :root { 76 | /* --color-50: #f0f5ff; */ 77 | --color-50: #f5f8ff; 78 | /* --color-100: #c5d9ff; */ 79 | --color-100: #dfeaff; 80 | --color-200: #99bdff; 81 | --color-300: #6d9fff; 82 | --color-400: #3f7eff; 83 | --color-500: #0157ff; 84 | --color-600: #0048d7; 85 | --color-700: #0034a3; 86 | --color-800: #002170; 87 | --color-900: #001145; 88 | 89 | --color-accent-500: #28d1b4; 90 | --color-accent-700: #007765; 91 | } 92 | } 93 | 94 | /* Custom media queries */ 95 | @custom-media --viewport-sm-up (min-width: 576px); 96 | @custom-media --viewport-sm-down (max-width: 575px); 97 | @custom-media --viewport-md-up (min-width: 768px); 98 | @custom-media --viewport-md-down (max-width: 767px); 99 | @custom-media --viewport-lg-up (min-width: 992px); 100 | @custom-media --viewport-lg-down (max-width: 991px); 101 | @custom-media --viewport-xl-up (min-width: 1200px); 102 | @custom-media --viewport-xl-down (max-width: 1199px); 103 | @custom-media --viewport-xxl-up (min-width: 1400px); 104 | @custom-media --viewport-xxl-down (max-width: 1399px); 105 | -------------------------------------------------------------------------------- /src/styles/tiny-ui-toggle.css: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | :root { 3 | --toggle-transition-duration: .35s; 4 | --toggle-transition-duration-sm: .15s; 5 | --toggle-transition-duration-close: var(--toggle-transition-duration); 6 | 7 | --toggle-padding-xs: .25rem; 8 | --toggle-padding-sm: .75rem; 9 | --toggle-padding-md: 1rem; 10 | --toggle-padding-lg: 2rem; 11 | 12 | --toggle-color: #0157ff; 13 | --toggle-background-color: #f5f8ff; 14 | --toggle-border-color: #0157ff; 15 | --toggle-border-width: 1px; 16 | 17 | --toggle-color-hover: #0034a3; 18 | --toggle-background-color-hover: #dfeaff; 19 | 20 | --toggle-color-active: #fff; 21 | --toggle-background-color-active: #0157ff; 22 | } 23 | /* Essential styles */ 24 | .toggle-outer { 25 | position: relative; 26 | } 27 | .toggle { 28 | color: #0157ff; 29 | color: var(--toggle-color); 30 | text-align: left; 31 | -webkit-text-decoration: none; 32 | text-decoration: none; 33 | background-color: #f5f8ff; 34 | background-color: var(--toggle-background-color); 35 | border: none; 36 | cursor: pointer; 37 | transition: all .35s ease-out; 38 | transition: all var(--toggle-transition-duration) ease-out; 39 | } 40 | .toggle:hover, .toggle:focus { 41 | color: #0034a3; 42 | color: var(--toggle-color-hover); 43 | background-color: #dfeaff; 44 | background-color: var(--toggle-background-color-hover); 45 | } 46 | .toggle:focus-visible { 47 | outline: 2px solid #0157ff; 48 | outline: 2px solid var(--toggle-background-color-active); 49 | } 50 | .toggle.is-active:not(.is-reset) { 51 | color: #fff; 52 | color: var(--toggle-color-active); 53 | background-color: #0157ff; 54 | background-color: var(--toggle-background-color-active); 55 | } 56 | .toggle:where(summary)::marker, .toggle:where(summary)::-webkit-details-marker { 57 | content: ''; 58 | display: none; 59 | } 60 | .toggle-panel { 61 | transition: all .35s ease-out; 62 | transition: all var(--toggle-transition-duration) ease-out; 63 | /* Emphasized decelerate */ 64 | transition-timing-function: cubic-bezier(.05, .7, .1, 1); 65 | /* transition-timing-function: cubic-bezier(0, 0, 0, 1); */ 66 | overflow: hidden; 67 | /* Standard decelerate */ 68 | } 69 | .toggle-panel:where(:not(details)) { 70 | height: 0; 71 | } 72 | /* Closing, Closed */ 73 | /* Other animations e.g. opacity and closing transitions can be added here */ 74 | .toggle-panel:where(:not(.is-active)) { 75 | transition-duration: .35s; 76 | transition-duration: var(--toggle-transition-duration-close); 77 | } 78 | /* Closed */ 79 | .toggle-panel:where(:not(.is-active):not(.is-anim):not(details)) { 80 | display: none; 81 | } 82 | /* Opening, Open, Closing */ 83 | .toggle-panel:where(.is-active, .is-anim) { 84 | } 85 | /* Opening */ 86 | .toggle-panel:where(.is-active.is-anim) { 87 | } 88 | /* Open */ 89 | .toggle-panel:where(.is-active:not(.is-anim)) { 90 | height: auto; 91 | overflow: visible; 92 | } 93 | /* Opening, Open */ 94 | .toggle-panel:where(.is-active) { 95 | } 96 | /* Closing */ 97 | .toggle-panel:where(.is-anim:not(.is-active)) { 98 | } 99 | /* Basic styling for toggle buttons and panels */ 100 | .toggle, .toggle-panel-content, .toggle-dialog-content, .toggle-tooltip-content { 101 | padding: .75rem 1rem; 102 | padding: var(--toggle-padding-sm) var(--toggle-padding-md); 103 | background-color: #f5f8ff; 104 | background-color: var(--toggle-background-color); 105 | } 106 | /* Animation variants */ 107 | .toggle-panel--partial { 108 | display: block; 109 | height: calc(2.5em + .75rem); 110 | } 111 | .toggle-panel--opacity:where(.is-anim:not(.is-active)) { 112 | opacity: 0; 113 | } 114 | .toggle-panel--none { 115 | height: auto; 116 | transition: none; 117 | } 118 | /* Dropdown */ 119 | .toggle-panel--dropdown { 120 | position: absolute; 121 | top: 100%; 122 | left: 0; 123 | z-index: 1000; 124 | transform-origin: 100% 0; 125 | will-change: scale; 126 | transition-timing-function: cubic-bezier(.19,1,.22,1); 127 | } 128 | .toggle-panel--dropdown:where(:not(.is-active)) { 129 | opacity: .25; 130 | scale: (.25); 131 | } 132 | /* Dialog / Modal */ 133 | /* Reset UA styles for dialog */ 134 | .toggle-dialog:where(dialog, [popover]) { 135 | margin: 0; 136 | padding: 0; 137 | color: inherit; 138 | background: none; 139 | border: none; 140 | } 141 | .toggle-dialog { 142 | --max-width: 480px; 143 | --max-height: 480px; 144 | 145 | position: fixed; 146 | z-index: 10000; 147 | top: 0; 148 | right: 0; 149 | bottom: 0; 150 | left: 0; 151 | margin: auto; 152 | width: -moz-fit-content; 153 | width: fit-content; 154 | height: -moz-fit-content; 155 | height: fit-content; 156 | transition: all .35s ease-out; 157 | transition: all var(--toggle-transition-duration) ease-out; 158 | 159 | /* Closed, Closing */ 160 | } 161 | .toggle-dialog:where(:not(.is-active)) { 162 | opacity: 0; 163 | transition-duration: .15s; 164 | transition-duration: var(--toggle-transition-duration-sm); 165 | } 166 | /* Closed */ 167 | .toggle-dialog:where(:not(.is-active):not(.is-anim)) { 168 | display: none; 169 | } 170 | /* Optional animation on dialog content */ 171 | .toggle-dialog > * { 172 | animation-name: slideInUp; 173 | animation-duration: .35s; 174 | animation-duration: var(--toggle-transition-duration); 175 | animation-fill-mode: forwards; 176 | } 177 | /* Closed, Closing */ 178 | .toggle-dialog:where(:not(.is-active)) > * { 179 | animation-name: slideOutUp; 180 | animation-duration: .15s; 181 | animation-duration: var(--toggle-transition-duration-sm); 182 | } 183 | .toggle-dialog-content { 184 | position: relative; 185 | overflow: scroll; 186 | overscroll-behavior: contain; 187 | max-width: var(--max-width); 188 | max-height: var(--max-height); 189 | } 190 | .toggle-dialog[aria-modal='true']::after, .toggle-modal::after { 191 | content: ''; 192 | position: fixed; 193 | top: 0; 194 | right: 0; 195 | bottom: 0; 196 | left: 0; 197 | z-index: -1; 198 | width: 100%; 199 | height: 100%; 200 | background: rgba(0, 0, 0, .5); 201 | } 202 | body.has-dialog { 203 | } 204 | body.has-modal { 205 | overflow: hidden; 206 | } 207 | .toggle-tooltip { 208 | position: absolute; 209 | bottom: 100%; 210 | left: 0; 211 | width: -moz-max-content; 212 | width: max-content; 213 | transition: all .15s ease-out; 214 | transition: all var(--toggle-transition-duration-sm) ease-out; 215 | } 216 | .toggle-tooltip:where(:not(.is-active)) { 217 | opacity: 0; 218 | } 219 | .toggle-tooltip:not(.is-active):not(.is-anim) { 220 | display: none; 221 | } 222 | @keyframes slideInUp { 223 | from { 224 | transform: translate3d(0, 32px, 0); 225 | } 226 | } 227 | @keyframes slideOutUp { 228 | to { 229 | transform: translate3d(0, -32px, 0); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/styles/tiny-ui-toggle/tiny-ui-toggle.css: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | :root { 3 | --toggle-transition-duration: .35s; 4 | --toggle-transition-duration-sm: .15s; 5 | --toggle-transition-duration-close: var(--toggle-transition-duration); 6 | 7 | --toggle-padding-xs: .25rem; 8 | --toggle-padding-sm: .75rem; 9 | --toggle-padding-md: 1rem; 10 | --toggle-padding-lg: 2rem; 11 | 12 | --toggle-color: #0157ff; 13 | --toggle-background-color: #f5f8ff; 14 | --toggle-border-color: #0157ff; 15 | --toggle-border-width: 1px; 16 | 17 | --toggle-color-hover: #0034a3; 18 | --toggle-background-color-hover: #dfeaff; 19 | 20 | --toggle-color-active: #fff; 21 | --toggle-background-color-active: #0157ff; 22 | } 23 | 24 | 25 | /* Essential styles */ 26 | .toggle-outer { 27 | position: relative; 28 | } 29 | 30 | 31 | 32 | .toggle { 33 | color: var(--toggle-color); 34 | text-align: left; 35 | text-decoration: none; 36 | background-color: var(--toggle-background-color); 37 | border: none; 38 | cursor: pointer; 39 | transition: all var(--toggle-transition-duration) ease-out; 40 | 41 | &:hover, &:focus { 42 | color: var(--toggle-color-hover); 43 | background-color: var(--toggle-background-color-hover); 44 | } 45 | 46 | &:focus-visible { 47 | outline: 2px solid var(--toggle-background-color-active); 48 | } 49 | 50 | &.is-active:not(.is-reset) { 51 | color: var(--toggle-color-active); 52 | background-color: var(--toggle-background-color-active); 53 | } 54 | 55 | &:where(summary)::marker, &:where(summary)::-webkit-details-marker { 56 | content: ''; 57 | display: none; 58 | } 59 | } 60 | 61 | 62 | 63 | .toggle-panel { 64 | transition: all var(--toggle-transition-duration) ease-out; 65 | /* Emphasized decelerate */ 66 | transition-timing-function: cubic-bezier(.05, .7, .1, 1); 67 | /* Standard decelerate */ 68 | /* transition-timing-function: cubic-bezier(0, 0, 0, 1); */ 69 | overflow: hidden; 70 | 71 | &:where(:not(details)) { 72 | height: 0; 73 | } 74 | 75 | /* Closing, Closed */ 76 | /* Other animations e.g. opacity and closing transitions can be added here */ 77 | &:where(:not(.is-active)) { 78 | transition-duration: var(--toggle-transition-duration-close); 79 | } 80 | 81 | /* Closed */ 82 | &:where(:not(.is-active):not(.is-anim):not(details)) { 83 | display: none; 84 | } 85 | 86 | /* Opening, Open, Closing */ 87 | &:where(.is-active, .is-anim) { 88 | } 89 | 90 | /* Opening */ 91 | &:where(.is-active.is-anim) { 92 | } 93 | 94 | /* Open */ 95 | &:where(.is-active:not(.is-anim)) { 96 | height: auto; 97 | overflow: visible; 98 | } 99 | 100 | /* Opening, Open */ 101 | &:where(.is-active) { 102 | } 103 | 104 | /* Closing */ 105 | &:where(.is-anim:not(.is-active)) { 106 | } 107 | } 108 | 109 | 110 | /* Basic styling for toggle buttons and panels */ 111 | .toggle, .toggle-panel-content, .toggle-dialog-content, .toggle-tooltip-content { 112 | padding: var(--toggle-padding-sm) var(--toggle-padding-md); 113 | background-color: var(--toggle-background-color); 114 | } 115 | 116 | 117 | /* Animation variants */ 118 | .toggle-panel--partial { 119 | display: block; 120 | height: calc(2.5em + .75rem); 121 | } 122 | 123 | .toggle-panel--opacity { 124 | &:where(.is-anim:not(.is-active)) { 125 | opacity: 0; 126 | } 127 | } 128 | 129 | .toggle-panel--none { 130 | height: auto; 131 | transition: none; 132 | } 133 | 134 | 135 | /* Dropdown */ 136 | .toggle-panel--dropdown { 137 | position: absolute; 138 | top: 100%; 139 | left: 0; 140 | z-index: 1000; 141 | transform-origin: 100% 0; 142 | will-change: scale; 143 | transition-timing-function: cubic-bezier(.19,1,.22,1); 144 | 145 | &:where(:not(.is-active)) { 146 | opacity: .25; 147 | scale: (.25); 148 | } 149 | } 150 | 151 | 152 | 153 | /* Dialog / Modal */ 154 | 155 | /* Reset UA styles for dialog */ 156 | .toggle-dialog { 157 | &:where(dialog, [popover]) { 158 | margin: 0; 159 | padding: 0; 160 | color: inherit; 161 | background: none; 162 | border: none; 163 | } 164 | } 165 | 166 | .toggle-dialog { 167 | --max-width: 480px; 168 | --max-height: 480px; 169 | 170 | position: fixed; 171 | z-index: 10000; 172 | inset: 0; 173 | margin: auto; 174 | width: fit-content; 175 | height: fit-content; 176 | transition: all var(--toggle-transition-duration) ease-out; 177 | 178 | /* Closed, Closing */ 179 | &:where(:not(.is-active)) { 180 | opacity: 0; 181 | transition-duration: var(--toggle-transition-duration-sm); 182 | } 183 | 184 | /* Closed */ 185 | &:where(:not(.is-active):not(.is-anim)) { 186 | display: none; 187 | } 188 | 189 | /* Optional animation on dialog content */ 190 | > * { 191 | animation-name: slideInUp; 192 | animation-duration: var(--toggle-transition-duration); 193 | animation-fill-mode: forwards; 194 | } 195 | 196 | /* Closed, Closing */ 197 | &:where(:not(.is-active)) { 198 | > * { 199 | animation-name: slideOutUp; 200 | animation-duration: var(--toggle-transition-duration-sm); 201 | } 202 | } 203 | } 204 | 205 | .toggle-dialog-content { 206 | position: relative; 207 | overflow: scroll; 208 | overscroll-behavior: contain; 209 | max-width: var(--max-width); 210 | max-height: var(--max-height); 211 | } 212 | 213 | .toggle-dialog[aria-modal='true']::after, .toggle-modal::after { 214 | content: ''; 215 | position: fixed; 216 | inset: 0; 217 | z-index: -1; 218 | width: 100%; 219 | height: 100%; 220 | background: rgba(0 0 0 / .5); 221 | } 222 | 223 | body.has-dialog { 224 | } 225 | 226 | body.has-modal { 227 | overflow: hidden; 228 | } 229 | 230 | 231 | .toggle-tooltip { 232 | position: absolute; 233 | bottom: 100%; 234 | left: 0; 235 | width: max-content; 236 | transition: all var(--toggle-transition-duration-sm) ease-out; 237 | 238 | &:where(:not(.is-active)) { 239 | opacity: 0; 240 | } 241 | 242 | &:is(:not(.is-active):not(.is-anim)) { 243 | display: none; 244 | } 245 | } 246 | 247 | 248 | @keyframes slideInUp { 249 | from { 250 | transform: translate3d(0, 32px, 0); 251 | } 252 | } 253 | 254 | @keyframes slideOutUp { 255 | to { 256 | transform: translate3d(0, -32px, 0); 257 | } 258 | } 259 | --------------------------------------------------------------------------------