diff --git a/.gitignore b/.gitignore index 2309cc8..096746c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,138 +1 @@ -# ---> Node -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - +/node_modules/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..52c09b3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "workbench.colorCustomizations": { + "titleBar.activeForeground": "#ffffff", + "titleBar.inactiveForeground": "#ffffff", + "titleBar.activeBackground": "#5920bc", + "titleBar.inactiveBackground": "#3f1883" + }, + + "editor.fontSize": 15, + "editor.tabSize": 2, + "terminal.integrated.fontSize": 15, + "window.zoomLevel": 1, + "[Log]": { + "editor.fontSize": 14 + }, + +"html-css-class-completion.includeGlobPattern": "public/css/**/*.{css,scss}", +"liveServer.settings.port": 5501 +} diff --git a/LICENSE b/LICENSE index 4d4a39d..99d100a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,73 +1,5 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright 2026 framex - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + Feel free to build anything you want! \ No newline at end of file diff --git a/README.md b/README.md index 9a4402e..57130f4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,384 @@ -# framexEngine-pro +# Framex Engine +> A lightweight, high-performance PHP engine for building static sites and modern web applications. + +[![PHP](https://img.shields.io/badge/PHP-8.0%2B-777BB4?logo=php&logoColor=white)](https://php.net) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-v4-06B6D4?logo=tailwindcss&logoColor=white)](https://tailwindcss.com) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) + +--- + +## Table of Contents + +- [What is Framex?](#what-is-framex) +- [Features](#features) +- [Quick Start](#quick-start) +- [Project Structure](#project-structure) +- [Creating Pages](#creating-pages) + - [PHP Pages](#php-pages) + - [Markdown Pages](#markdown-pages) +- [Templates & Partials](#templates--partials) +- [Styling](#styling) +- [Helper Functions](#helper-functions) +- [Contributing](#contributing) +- [License](#license) + +--- + +## What is Framex? + +**Framex Engine** is a minimal PHP framework designed for developers who want to ship fast static sites without the bloat of traditional CMS platforms or over-engineered frameworks. + +No database. No complex routing files. No plugin ecosystem to maintain. Just **PHP files**, **Markdown content**, and a directory structure that mirrors your URLs. + +Framex is perfect for: + +- Landing pages & marketing sites +- Documentation & blogs +- Portfolios & personal sites +- Prototypes & MVPs +- Any site where content changes faster than architecture + +--- + +## Features + +| Feature | Description | +|---------|-------------| +| **URL-Driven Routing** | Your folder structure *is* your routing. Zero configuration. | +| **Infinite Pages** | Create a file → get a page. No database required. | +| **Markdown Support** | Write content in `.md` files. Rendered automatically via Parsedown. | +| **Template System** | Switch layouts per page. Reusable partials for DRY templates. | +| **Tailwind CSS v4** | Pre-configured with Lightning CSS. Utility-first, blazing fast builds. | +| **Asset Pipeline** | Built-in cache busting, file hashing, and development helpers. | +| **Zero Dependencies** | Works on any shared host. No Composer required for core features. | +| **Dark Theme Ready** | Modern dark UI components included out of the box. | +| **Light / Dark Mode** | Automatic system detection with manual toggle. Persisted in localStorage. | + +--- + +## Quick Start + +### Requirements + +- PHP 8.0 or higher +- Node.js 18+ (for CSS build pipeline) + +### Installation + +```bash +# Clone the repository +git clone https://github.com/yourusername/framex-engine.git my-project +cd my-project + +# Install dependencies +npm install + +# Start the development watcher +npm run dev +``` + +Point your web server to the `public/` directory and visit `http://localhost`. + +> **Note:** If you use PHP's built-in server, run it from the project root: +> ```bash +> php -S localhost:8000 -t public/ +> ``` + +--- + +## Project Structure + +``` +framexEngine-Base/ +├── app/ +│ ├── views/ # Your pages live here +│ │ ├── index.php +│ │ ├── about.php +│ │ └── blog/ +│ │ └── hello-world.md +│ └── about/ +│ +├── core/ +│ ├── classes/ +│ │ ├── Engine.php # Router & request handler +│ │ ├── Framex.php # View renderer +│ │ └── Parsedown.php # Markdown parser +│ ├── config.php # Constants & configuration +│ ├── functions.php # Global helper functions +│ └── ignition.php # Application bootstrap +│ +├── templates/ +│ ├── main.php # Default layout (nav + footer) +│ ├── clean.php # Minimal layout +│ ├── admin.php # Dashboard layout +│ ├── error404.php # 404 error page +│ └── partials/ +│ ├── topmenu.php +│ └── footer.php +│ +├── public/ +│ ├── css/ # Compiled stylesheets +│ ├── js/ # Scripts +│ ├── images/ +│ └── index.php # Entry point +│ +├── public/css/src/ # CSS source files +│ ├── style.css # Main site styles +│ ├── admin.css # Admin panel styles +│ └── ... +│ +└── package.json # Build scripts & dependencies +``` + +--- + +## Creating Pages + +### PHP Pages + +Create a file in `app/views/` and it instantly becomes a URL: + +| File | URL | +|------|-----| +| `app/views/index.php` | `/` | +| `app/views/about.php` | `/about` | +| `app/views/blog/post.php` | `/blog/post` | +| `app/views/contact/index.php` | `/contact` | + +Example page (`app/views/about.php`): + +```php + + +

About Framex

+

This page was created by dropping a single PHP file in the views folder.

+``` + +### Markdown Pages + +Prefer writing in Markdown? Save your content as a `.md` file: + +| File | URL | +|------|-----| +| `app/views/changelog.md` | `/changelog` | +| `app/views/docs/installation.md` | `/docs/installation` | + +Framex automatically detects the extension, runs it through [Parsedown](https://parsedown.org/), and wraps it in your template. + +```markdown +# Installation + +1. Clone the repo +2. Run `npm install` +3. Start hacking +``` + +--- + +## Templates & Partials + +### Switching Templates + +By default, every page wraps inside `templates/main.php`. You can switch this per page: + +```php + +``` + +Built-in templates: + +| Template | Purpose | +|----------|---------| +| `main` | Default layout with navigation and footer | +| `clean` | Minimal layout, no wrappers | +| `admin` | Dashboard sidebar layout | + +### Partials + +Reusable chunks live in `templates/partials/`: + +```php + +``` + +Create your own: + +```bash +touch templates/partials/newsletter.php +``` + +```php + +
+ + +
+``` + +Include it anywhere: + +```php + +``` + +--- + +## Styling + +Framex ships with **Tailwind CSS v4** pre-configured. The build pipeline uses the Tailwind CLI with Lightning CSS for maximum performance. + +### Development + +```bash +# Watch for changes and rebuild automatically +npm run dev +``` + +### Production + +```bash +# Build minified CSS for production +npm run build +``` + +### CSS Source Files + +| File | Output | Purpose | +|------|--------|---------| +| `public/css/src/style.css` | `public/css/style.css` | Main site styles | +| `public/css/src/admin.css` | `public/css/admin.css` | Admin panel styles | +| `public/css/src/responsive.css` | `public/css/responsive.css` | Responsive overrides | +| `public/css/src/vendors.css` | `public/css/vendors.css` | Third-party styles | + +### Using Custom CSS + +You are not locked into Tailwind. The CSS pipeline is completely optional. You can: + +- Write vanilla CSS in `public/css/src/` +- Use Sass/SCSS (still installed) +- Link external CSS frameworks from a CDN +- Replace the entire build pipeline + +--- + +## Light & Dark Mode + +Framex includes a built-in light/dark mode toggle that respects the user's system preference and persists their choice in `localStorage`. + +- **Automatic detection** — Checks `prefers-color-scheme: dark` on first visit +- **Manual toggle** — Sun/moon button in the navbar +- **Instant switch** — Zero page reload, powered by Tailwind's `dark:` utilities +- **Persistent** — Choice is saved and restored on every page + +To style for dark mode in your templates, use Tailwind's `dark:` prefix: + +```html +
+ +
+``` + +--- + +## SEO & Meta Tags + +Every page can define custom meta descriptions and social sharing images through the `$data` array. Framex automatically generates the following tags in the ``: + +- `` — Search engine snippet +- `` — Open Graph (Facebook, LinkedIn, Discord) +- `` — Twitter/X Cards + +### Setting meta per page + +```php + +``` + +If you do not set a custom description or image, sensible defaults are used automatically. + +--- + +## Helper Functions + +Framex includes a set of global helpers to speed up development: + +| Function | Usage | Description | +|----------|-------|-------------| +| `asset()` | `asset('css/style.css')` | Versioned asset URLs with cache busting | +| `image()` | `image(800, 600)` | Random placeholder images via Picsum | +| `partial()` | `partial('footer')` | Include reusable template chunks | +| `e()` | `e($userInput)` | Escape HTML entities (XSS protection) | +| `pre()` | `pre($array, true)` | Debug print_r with optional die | +| `summarize()` | `summarize($text, 100)` | Truncate text with ellipsis | +| `urlSlug()` | `urlSlug($title)` | Convert strings to URL-friendly slugs | + +### Asset Helper Options + +```php +// Basic usage + + +// With file hash for cache busting + + +// With custom prefix + + +// Absolute URL + +``` + +--- + +## Configuration + +Edit `core/config.php` to customize your setup: + +```php +define('DEV_MODE', true); // Toggle development mode +define('SITE_URL', 'https://...'); // Your site URL +define('CSS', '/css/'); // CSS directory +define('JS', '/js/'); // JS directory +``` + +**Development Mode** (`DEV_MODE = true`): +- CSS URLs append a unique query string on every refresh +- Errors are more verbose + +**Production Mode** (`DEV_MODE = false`): +- Assets use file-hash based cache busting +- Cleaner error handling + +--- + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +--- + +## License + +Framex Engine is open-source software licensed under the [MIT License](LICENSE). + +--- + +

+ Built with ❤️ by Prodigital Framex +

diff --git a/app/views/about/index.md b/app/views/about/index.md new file mode 100644 index 0000000..4fa7fd7 --- /dev/null +++ b/app/views/about/index.md @@ -0,0 +1,68 @@ + + +# About Framex + +Hi, I am **Thanos**. I have been involved with computers, hardware, and practical IT work since 1995. Framex is a small PHP project I built and refined through real use, mostly for fast static pages, custom CSS experiments, presentations, API-driven pages, and lightweight content sites. + +![Thanos](../images/thanos.png) + +## Why this project exists + +Framex is built around one simple idea: a website should be easy to understand from its files. + +Routes map to files in `app/views`, templates live in `templates`, public assets live in `public`, and styling is handled through Tailwind CSS 4 or any CSS approach you prefer. There is no heavy framework layer to learn before you can build a page. + +## What it is good for + +- **Static websites** that need clean URLs and reusable templates. +- **Markdown documentation** with automatic typography and dark-mode support. +- **Small business websites** where speed and maintainability matter. +- **Custom landing pages** with modern Tailwind components. +- **API-powered pages** when you need PHP logic without a large framework. +- **Presentations and prototypes** that should stay portable and simple. + +## Design goals + +Framex tries to stay: + +- **Lightweight:** only the pieces needed to render pages clearly. +- **Fast:** no unnecessary runtime complexity. +- **Readable:** routes, views, templates, and assets are easy to find. +- **Flexible:** use PHP views, Markdown views, Tailwind CSS, or plain CSS. +- **Practical:** built for developers who want to ship pages without ceremony. + +## How pages work + +A URL such as `/about` resolves to a file like: + +```text +app/views/about/index.md +``` + +A URL such as `/demo/blog-post` resolves to: + +```text +app/views/demo/blog-post.php +``` + +Markdown pages are automatically parsed and styled. PHP pages can use custom markup, arrays, loops, metadata, and reusable helpers. + +## Usage + +You can use, extend, modify, reduce, or rebuild this project for personal and commercial work. Treat it as a starting point, not a locked system. + +Good next steps: + +- Read the [documentation](/docs). +- Explore the [demo pages](/demo). +- Create a new PHP view in `app/views`. +- Create a new Markdown page with `index.md`. +- Rebuild CSS with `npm run build:css` when you add new Tailwind classes. + +## Disclaimer + +This is a personal project shared for developers and people who understand how to work with PHP projects. I have used it for years without issues, but you are responsible for how you use it, modify it, deploy it, and secure it. + +Use it carefully, make backups, and review the code before using it in production. + +**Enjoy building.** diff --git a/app/views/blocks/accordion/accordion-1.php b/app/views/blocks/accordion/accordion-1.php new file mode 100644 index 0000000..28db820 --- /dev/null +++ b/app/views/blocks/accordion/accordion-1.php @@ -0,0 +1,53 @@ +
+
+
+
+ +

Lorem ipsum dolor sit amet consectetur adipisicing?

+ + + + +
+ +

+ Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ab hic veritatis molestias culpa + in, recusandae laboriosam neque aliquid libero nesciunt voluptate dicta quo officiis + explicabo consequuntur distinctio corporis earum similique! +

+
+ +
+ +

Lorem ipsum dolor sit amet consectetur adipisicing?

+ + + + +
+ +

+ Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ab hic veritatis molestias culpa + in, recusandae laboriosam neque aliquid libero nesciunt voluptate dicta quo officiis + explicabo consequuntur distinctio corporis earum similique! +

+
+ +
+ +

Lorem ipsum dolor sit amet consectetur adipisicing?

+ + + + +
+ +

+ Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ab hic veritatis molestias culpa + in, recusandae laboriosam neque aliquid libero nesciunt voluptate dicta quo officiis + explicabo consequuntur distinctio corporis earum similique! +

+
+
+
+
\ No newline at end of file diff --git a/app/views/blocks/accordion/index.php b/app/views/blocks/accordion/index.php new file mode 100644 index 0000000..0fed7b0 --- /dev/null +++ b/app/views/blocks/accordion/index.php @@ -0,0 +1,77 @@ +
+
+
+
+ + +
    + +
  • +
    + + + + + '> + +
    + +
  • + +
+
    + +
  • +
    + + + + + '> +
    +
  • + +
+
+
+
+
\ No newline at end of file diff --git a/app/views/blocks/blog-list/blog-list-1.php b/app/views/blocks/blog-list/blog-list-1.php new file mode 100644 index 0000000..a193683 --- /dev/null +++ b/app/views/blocks/blog-list/blog-list-1.php @@ -0,0 +1,40 @@ + +
+ +
+ +

+ The latest and greatest news +

+ + +
+
diff --git a/app/views/blocks/blog-list/blog-list-2.php b/app/views/blocks/blog-list/blog-list-2.php new file mode 100644 index 0000000..5801399 --- /dev/null +++ b/app/views/blocks/blog-list/blog-list-2.php @@ -0,0 +1,67 @@ + +
+ +
+ + +
+
diff --git a/app/views/blocks/blog-list/blog-list-3.php b/app/views/blocks/blog-list/blog-list-3.php new file mode 100644 index 0000000..55128eb --- /dev/null +++ b/app/views/blocks/blog-list/blog-list-3.php @@ -0,0 +1,96 @@ + +
+ +
+ +

+ The latest and greatest news +

+

+ Lorem ipsum dolor sit amet elit ut aliquam +

+ + +
+
diff --git a/app/views/blocks/blog-list/blog-list-4.php b/app/views/blocks/blog-list/blog-list-4.php new file mode 100644 index 0000000..bf43d73 --- /dev/null +++ b/app/views/blocks/blog-list/blog-list-4.php @@ -0,0 +1,155 @@ + +
+ +
+
+ +

+ The latest and greatest news +

+

+ Lorem ipsum dolor sit amet elit ut aliquam +

+ +
+ + + + +
+
+ +
+
+ +
+ + +
+
+

+ Here is the title for this News +

+

Lorem ipsum dolor sit amet elit ut aliquam

+
+ + +
+
+ + +
+ + +
+
+

+ Here is the title for this News +

+

Lorem ipsum dolor sit amet elit ut aliquam

+
+ + +
+
+
+ +
+ +
+ + +
+

+ Here is the title for this News +

+

+ We make every expression of Hero Spirits with precision and + passion +

+
+
+ +
+ + +
+

+ Here is the title for this News +

+

+ We make every expression of Hero Spirits with precision and + passion +

+
+
+ +
+ + +
+

+ Here is the title for this News +

+

+ We make every expression of Hero Spirits with precision and + passion +

+
+
+
+ + +
+ +
+
+
+
diff --git a/app/views/blocks/blog-list/index.php b/app/views/blocks/blog-list/index.php new file mode 100644 index 0000000..0fed7b0 --- /dev/null +++ b/app/views/blocks/blog-list/index.php @@ -0,0 +1,77 @@ +
+
+
+
+ + +
    + +
  • +
    + + + + + '> + +
    + +
  • + +
+
    + +
  • +
    + + + + + '> +
    +
  • + +
+
+
+
+
\ No newline at end of file diff --git a/app/views/blocks/heros/hero-1.php b/app/views/blocks/heros/hero-1.php new file mode 100644 index 0000000..e19ffad --- /dev/null +++ b/app/views/blocks/heros/hero-1.php @@ -0,0 +1,26 @@ +
+
+
+

+ Understand user flow and + increase + conversions +

+ +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eaque, nisi. Natus, provident + accusamus impedit minima harum corporis iusto. +

+ + +
+
+
\ No newline at end of file diff --git a/app/views/blocks/heros/hero-2.php b/app/views/blocks/heros/hero-2.php new file mode 100644 index 0000000..2299cde --- /dev/null +++ b/app/views/blocks/heros/hero-2.php @@ -0,0 +1,33 @@ + +
+ +
+ +

+ Meet Flowspark +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + varius enim in eros elementum tristique. Duis cursus, mi quis viverra + ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. +

+
+ +
+

Our Mission

+

+ Aliquet risus feugiat in ante metus. Arcu dui vivamus arcu felis + bibendum ut. Vestibulum lorem sed risus ultricies tristique nulla. + Vitae et leo duis ut diam quam. Bibendum arcu vitae elementum + curabitur vitae nunc. Dictumst vestibulum rhoncus est + pellentesque. Lectus proin nibh nisl condimentum id. Ullamcorper + dignissim cras tincidunt lobortis feugiat vivamus. +
+
+ Massa id neque aliquam vestibulum morbi blandit. Nulla + pellentesque dignissim enim sit amet venenatis. +

+
+
+
+
diff --git a/app/views/blocks/heros/index.php b/app/views/blocks/heros/index.php new file mode 100644 index 0000000..0fed7b0 --- /dev/null +++ b/app/views/blocks/heros/index.php @@ -0,0 +1,77 @@ +
+
+
+
+ + +
    + +
  • +
    + + + + + '> + +
    + +
  • + +
+
    + +
  • +
    + + + + + '> +
    +
  • + +
+
+
+
+
\ No newline at end of file diff --git a/app/views/blocks/index.php b/app/views/blocks/index.php new file mode 100644 index 0000000..0fed7b0 --- /dev/null +++ b/app/views/blocks/index.php @@ -0,0 +1,77 @@ +
+
+
+
+ + +
    + +
  • +
    + + + + + '> + +
    + +
  • + +
+
    + +
  • +
    + + + + + '> +
    +
  • + +
+
+
+
+
\ No newline at end of file diff --git a/app/views/blocks/sections/index.php b/app/views/blocks/sections/index.php new file mode 100644 index 0000000..0fed7b0 --- /dev/null +++ b/app/views/blocks/sections/index.php @@ -0,0 +1,77 @@ +
+
+
+
+ + +
    + +
  • +
    + + + + + '> + +
    + +
  • + +
+
    + +
  • +
    + + + + + '> +
    +
  • + +
+
+
+
+
\ No newline at end of file diff --git a/app/views/blocks/sections/section-1.php b/app/views/blocks/sections/section-1.php new file mode 100644 index 0000000..748b4cb --- /dev/null +++ b/app/views/blocks/sections/section-1.php @@ -0,0 +1,20 @@ +
+
+
+
+

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. +

+ + +

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Tenetur doloremque saepe + architecto maiores repudiandae amet perferendis repellendus, reprehenderit voluptas + sequi. +

+
+ + +
+
+
diff --git a/app/views/demo/blog-list.php b/app/views/demo/blog-list.php new file mode 100644 index 0000000..3ed9acc --- /dev/null +++ b/app/views/demo/blog-list.php @@ -0,0 +1,88 @@ + 'Designing fast content systems with plain PHP', + 'category' => 'Architecture', + 'date' => 'May 12, 2026', + 'read' => '7 min read', + 'description' => 'A practical look at keeping site structure simple while still giving editors polished pages.', + 'color' => 'bg-pre-blue', + ], + [ + 'title' => 'Markdown pages that look finished by default', + 'category' => 'Content', + 'date' => 'May 8, 2026', + 'read' => '5 min read', + 'description' => 'How automatic prose styling lets documentation and articles ship without custom templates.', + 'color' => 'bg-pre-emerald', + ], + [ + 'title' => 'A small Tailwind design system for real websites', + 'category' => 'Design', + 'date' => 'May 4, 2026', + 'read' => '6 min read', + 'description' => 'Reusable sections, cards, buttons, and preset colors keep page building consistent.', + 'color' => 'bg-pre-amber', + ], + [ + 'title' => 'Dark mode without making every page complicated', + 'category' => 'Frontend', + 'date' => 'April 28, 2026', + 'read' => '4 min read', + 'description' => 'Persisted preferences, system fallback, and theme-aware utility classes in one flow.', + 'color' => 'bg-pre-rose', + ], +]; +?> + +
+
+
+
+
+

Blog list demo

+

+ Editorial cards for a modern content index. +

+
+

+ This page is a PHP view. It uses an array of posts, loops through the data, and renders a responsive grid without needing a separate component system. +

+
+ +
+ All posts + Architecture + Content + Design +
+ +
+ $post): ?> +
+
+
+ + / + +
+

+ +

+
+
+

+
+ + Read post +
+
+
+ +
+
+
+
diff --git a/app/views/demo/blog-post.php b/app/views/demo/blog-post.php new file mode 100644 index 0000000..57a8dfa --- /dev/null +++ b/app/views/demo/blog-post.php @@ -0,0 +1,98 @@ + + +
+
+
+
+
+

Architecture / 7 min read

+

+ Designing fast content systems with plain PHP. +

+

+ A modern site does not need a heavy runtime to feel polished. Framex keeps page resolution, templates, assets, and Markdown close to the filesystem. +

+
+ Fx +
+

Framex Team

+

Published May 12, 2026

+
+
+
+
+
+ +
+
+
+
+

Routing

+

Files become URLs

+
+
+

Views

+

PHP and Markdown

+
+
+

Design

+

Tailwind CSS 4

+
+
+
+
+ +
+
+ + +
+

Simple routing

+

Framex resolves URLs by checking for matching PHP and Markdown files inside app/views. This keeps page creation predictable for new developers and small teams.

+ +

A route such as /demo/blog-post can be backed by app/views/demo/blog-post.php, while documentation can be written as Markdown in app/views/docs/index.md.

+ +

Shared layouts

+

Every page is wrapped by the main template, which includes the top menu, current view content, footer, compiled CSS, and JavaScript. PHP views can set metadata through the $data array.

+ +
+

Keep page-specific content in views, shared structure in templates, and reusable visual rules in Tailwind source CSS.

+
+ +

Content speed

+

Markdown pages are useful for documentation and simple publishing workflows. PHP views are better when a page needs custom data, cards, grids, forms, or conditional rendering.

+ +
// PHP view route
+app/views/demo/blog-post.php
+
+// Markdown view route
+app/views/demo/page.md
+
+
+
+
+ +
+
+
+
+

Next demo

+

Explore the Markdown page.

+
+ Open Markdown demo +
+
+
+
diff --git a/app/views/demo/index.php b/app/views/demo/index.php new file mode 100644 index 0000000..fc50566 --- /dev/null +++ b/app/views/demo/index.php @@ -0,0 +1,86 @@ + 'Blog List', + 'description' => 'A polished editorial index with featured content, filters, and responsive cards.', + 'href' => '/demo/blog-list', + 'label' => 'PHP view', + 'color' => 'bg-pre-blue', + ], + [ + 'title' => 'Blog Post', + 'description' => 'A long-form article layout with author metadata, hero content, and related cards.', + 'href' => '/demo/blog-post', + 'label' => 'PHP view', + 'color' => 'bg-pre-emerald', + ], + [ + 'title' => 'Markdown Page', + 'description' => 'A content page rendered from Markdown and automatically styled by `.prose`.', + 'href' => '/demo/page', + 'label' => 'MD view', + 'color' => 'bg-pre-amber', + ], +]; +?> + +
+
+
+
+

Demo library

+

+ Modern pages built with the Framex view system. +

+

+ These demos show how PHP views and Markdown views can share the same template, navigation, footer, light/dark mode, and reusable Tailwind classes. +

+
+ + +
+
+ +
+
+
+

4

+

Demo routes

+
+
+

2

+

View types

+
+
+

4

+

Preset colors

+
+
+

1

+

Shared layout

+
+
+
+
diff --git a/app/views/demo/page.md b/app/views/demo/page.md new file mode 100644 index 0000000..032fc60 --- /dev/null +++ b/app/views/demo/page.md @@ -0,0 +1,54 @@ +# Markdown Page Demo + +This page is rendered from `app/views/demo/page.md`. It uses the same main template, top menu, footer, CSS, JavaScript, and light/dark mode as PHP views. + +Markdown pages are ideal for documentation, policies, articles, guides, landing copy drafts, and simple content pages. + +## What gets styled automatically + +Framex wraps Markdown output in `.prose prose-shell`, so common content elements are styled without extra markup: + +- Headings +- Paragraphs +- Links +- Ordered and unordered lists +- Inline code +- Code blocks +- Blockquotes +- Tables +- Images +- Horizontal rules + +## Example content + +You can write ordinary Markdown: + +```md +## Feature section + +- Fast routes +- PHP views +- Markdown views +- Tailwind styling +``` + +And it becomes a fully styled content page. + +## Table example + +| View type | Best for | Route example | +| --- | --- | --- | +| PHP | Dynamic layouts, cards, forms | `/demo/blog-list` | +| Markdown | Docs, articles, simple pages | `/demo/page` | + +## Blockquote example + +> Markdown is the fastest way to add polished content when the page does not need custom PHP logic. + +## Next steps + +Open the other demo pages: + +- [Demo index](/demo) +- [Blog list](/demo/blog-list) +- [Blog post](/demo/blog-post) diff --git a/app/views/docs/index.md b/app/views/docs/index.md new file mode 100644 index 0000000..a7c3f8b --- /dev/null +++ b/app/views/docs/index.md @@ -0,0 +1,415 @@ +# Framex Engine Documentation + +Framex is a small PHP engine for building fast websites with plain PHP views, Markdown pages, reusable templates, and Tailwind CSS 4. It is designed to stay simple: URLs map to files, templates wrap views, and assets live in `public`. + +## Project Structure + +The important folders are: + +```text +app/views/ Page views for URLs +core/ Router, renderer, helpers, and configuration +templates/ Layout templates and partials +templates/partials/ Shared pieces such as top menu and footer +public/ Web root for CSS, JS, images, and index.php +public/css/src/ Tailwind source CSS +public/css/style.css Compiled production CSS +public/js/init.js Theme toggle and menu behavior +``` + +The web server should point to `public` as the document root. + +## URL Routing + +Framex resolves the browser URL to a file inside `app/views`. + +For a request like: + +```text +/about +``` + +Framex checks these files in order: + +```text +app/views/about.php +app/views/about/index.php +app/views/about.md +app/views/about/index.md +``` + +The first file that exists is rendered. If none exists, Framex falls back to `templates/error404.php`. + +Examples: + +```text +/ -> app/views/index.php +/docs -> app/views/docs/index.md +/about -> app/views/about/index.md +/pricing -> app/views/pricing.php +/blog/hello -> app/views/blog/hello.php or app/views/blog/hello/index.md +``` + +Trailing slashes are normalized, so `/docs/` and `/docs` resolve the same way. + +## PHP Views + +Use PHP views when a page needs custom markup, variables, conditionals, loops, forms, or reusable PHP logic. + +Create a file: + +```text +app/views/pricing.php +``` + +Then visit: + +```text +/pricing +``` + +A PHP view can set page metadata by assigning values to the `$data` array: + +```php + + +
+
+

Pricing

+

Choose the right plan.

+
+
+``` + +The rendered PHP view is injected into the main template through ``. + +## Markdown Views + +Use Markdown views for documentation, simple content pages, articles, and static text-heavy pages. + +Create a file: + +```text +app/views/guide/index.md +``` + +Then visit: + +```text +/guide +``` + +Markdown files are parsed by `Parsedown` and automatically wrapped in: + +```html +
+ ... +
+``` + +That means Markdown pages automatically get readable typography, spacing, links, lists, code blocks, tables, blockquotes, images, and light/dark colors. + +Example Markdown page: + +```md +# My Guide + +This page is rendered from Markdown. + +## Features + +- Clean URLs +- Automatic styling +- Light and dark mode + +```php +echo 'Code blocks are styled too'; +``` +``` + +## Templates + +The default layout is: + +```text +templates/main.php +``` + +It handles: + +- HTML document structure +- Meta tags +- CSS include +- Dark-mode bootstrap +- Top menu partial +- Current view output +- Footer partial +- JavaScript include + +The main content flow is: + +```php + + + +``` + +Partials live in: + +```text +templates/partials/ +``` + +For example: + +```text +templates/partials/topmenu.php +templates/partials/footer.php +``` + +Load a partial with: + +```php + +``` + +## Metadata + +PHP views can customize metadata with `$data`. + +Common keys: + +```php +$data['title'] = 'Page title'; +$data['metaDescription'] = 'Page description for search and sharing.'; +$data['metaImage'] = image(1200, 630); +$data['bodyClass'] = 'custom body classes'; +$data['template'] = 'main'; +``` + +If a key is not set, `templates/main.php` uses sensible defaults. + +Markdown views currently use the default metadata unless the renderer is extended to read front matter. + +## Assets + +Public files are served from `public`. + +Examples: + +```text +public/css/style.css -> /css/style.css +public/js/init.js -> /js/init.js +public/images/logo.png -> /images/logo.png +``` + +Use the `asset()` helper for cache-busted URLs: + +```php + + +``` + +Useful asset options: + +```php +asset('css/style.css'); +asset('js/init.js', ['hash' => true]); +asset('images/logo.png', ['absolute' => true]); +asset('images/missing.png', ['fallback' => '/images/default.png']); +``` + +## Tailwind CSS + +Framex uses Tailwind CSS 4 through the Tailwind CLI. + +Install dependencies: + +```bash +npm install +``` + +Build CSS: + +```bash +npm run build:css +``` + +Watch CSS during development: + +```bash +npm run watch:css +``` + +Source CSS: + +```text +public/css/src/style.css +``` + +Compiled CSS: + +```text +public/css/style.css +``` + +Tailwind scans these sources: + +```css +@source "../../../templates/**/*.php"; +@source "../../../app/**/*.php"; +@source "../../../app/**/*.md"; +``` + +When you add new Tailwind classes in PHP or Markdown files, rebuild the CSS. + +## Reusable CSS Classes + +The project includes reusable classes in `public/css/src/style.css`. + +Layout: + +```html +
+
...
+
+``` + +Buttons: + +```html +Primary +Secondary + +``` + +Cards: + +```html +
+

Card title

+

Card content.

+
+``` + +Preset backgrounds: + +```html +
Blue preset
+
Emerald preset
+
Amber preset
+
Rose preset
+``` + +These presets include both light and dark colors. + +## Light and Dark Mode + +Dark mode is controlled by the `dark` class on the `` element. + +The inline script in `templates/main.php` runs before the stylesheet loads. It checks: + +1. The saved `framex-theme` value in `localStorage`. +2. The browser system preference. +3. Light mode as the fallback. + +The toggle buttons in `templates/partials/topmenu.php` use: + +```html +data-theme-toggle +``` + +The behavior is implemented in: + +```text +public/js/init.js +``` + +Use Tailwind dark variants anywhere: + +```html +
+ Theme-aware content +
+``` + +## Running Locally + +Install dependencies and build CSS: + +```bash +npm install +npm run build:css +``` + +Start PHP's built-in server: + +```bash +php -S localhost:8000 -t public +``` + +Open: + +```text +http://localhost:8000/ +``` + +Useful URLs: + +```text +/ Landing page +/docs This documentation +/about Example Markdown page +``` + +## Adding a New Page + +For a PHP page: + +```text +app/views/services.php +``` + +Visit: + +```text +/services +``` + +For a Markdown page: + +```text +app/views/services/index.md +``` + +Visit: + +```text +/services +``` + +For a nested page: + +```text +app/views/blog/hello-world/index.md +``` + +Visit: + +```text +/blog/hello-world +``` + +## Recommended Workflow + +1. Add or edit a PHP or Markdown view in `app/views`. +2. Use `templates/partials` for shared UI. +3. Use `.section`, `.contain`, `.btn`, `.card`, and preset backgrounds for consistent design. +4. Run `npm run build:css` after changing Tailwind classes. +5. Test the URL in the browser. + +Framex works best when pages stay close to the filesystem, shared layout stays in templates, and reusable styling stays in the Tailwind source CSS. diff --git a/app/views/index.php b/app/views/index.php new file mode 100644 index 0000000..33839a3 --- /dev/null +++ b/app/views/index.php @@ -0,0 +1,178 @@ + + +
+
+
+
+

PHP engine plus Tailwind CSS 4

+

+ Build static sites that feel fast before you even optimize them. +

+

+ FramexEngine keeps routing, markdown, templates, and assets simple so you can ship lightweight PHP sites with a modern design system already in place. +

+ +
+ +
+
+
+ + + +
+
+
+
+

<?php

+

$data['title'] = 'Framex';

+

// templates, markdown, Tailwind

+

?>

+
+
+
+

CSS-first

+

Tailwind v4 build flow.

+
+
+

Markdown

+

Auto styled pages.

+
+
+
+
+
+
+ +
+
+
+

Features

+

A compact foundation for real projects.

+
+
+
+

Simple routing

+

PHP and markdown views resolve cleanly from the app views directory.

+
+
+

Reusable UI

+

Sections, buttons, cards, containers, and preset backgrounds are ready to use.

+
+
+

Dark mode

+

Theme preference follows the system first, then persists your manual choice.

+
+
+
+
+ +
+
+
+
+

FramexEngine ideas

+

Start small, then grow only where the project needs it.

+

+ Framex works well for pages that should be easy to edit, fast to ship, and simple to understand from the filesystem. +

+ +
+ +
+
+
01
+

Documentation hub

+

Use Markdown views for guides, API notes, changelogs, and internal knowledge bases.

+
+
+
02
+

Client landing pages

+

Build fast campaign pages with shared templates, reusable cards, and Tailwind utilities.

+
+
+
03
+

API dashboards

+

Use PHP views for simple data pages, status panels, metrics, and external API summaries.

+
+
+
04
+

Personal publishing

+

Create a lightweight blog, portfolio, or notes site without adding a database first.

+
+
+
+
+
+ +
+
+
+

Markdown pages

+

Drop in a `.md` file and get a designed page.

+

+ Framex already parses markdown views. The refreshed `.prose` layer now styles content, code, images, tables, and blockquotes automatically in both themes. +

+
+
+
+

Markdown preview

+

Write normal content and let the design system handle spacing, readable line length, and theme colors.

+

Markdown pages should look intentional without extra template work.

+
## Fast content
+- Create a view.md file
+- Publish clean HTML
+- Keep styling automatic
+
+
+
+
+ +
+
+
+

Preset colors

+

Four reusable light and dark background helpers.

+
+
+
+

Blue

+

Primary calls and product highlights.

+
+
+

Emerald

+

Success states and fresh sections.

+
+
+

Amber

+

Notes, warnings, and warm accents.

+
+
+

Rose

+

Promos and energetic content blocks.

+
+
+
+
+ +
+
+
+

Ready foundation

+

Start with structure, styling, and markdown already connected.

+ +
+
+
+
diff --git a/app/views/lab.php b/app/views/lab.php new file mode 100644 index 0000000..ff6460b --- /dev/null +++ b/app/views/lab.php @@ -0,0 +1,11 @@ +
+
+

Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui excepturi soluta quos nostrum, est ex, maxime repudiandae laborum molestias inventore debitis. Nihil quidem explicabo assumenda debitis quas, beatae molestiae officiis.

+
+
+ +
+
+

Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui excepturi soluta quos nostrum, est ex, maxime repudiandae laborum molestias inventore debitis. Nihil quidem explicabo assumenda debitis quas, beatae molestiae officiis.

+
+
\ No newline at end of file diff --git a/core/.htaccess b/core/.htaccess new file mode 100644 index 0000000..3a42882 --- /dev/null +++ b/core/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/core/classes/Engine.php b/core/classes/Engine.php new file mode 100644 index 0000000..fbe6fb6 --- /dev/null +++ b/core/classes/Engine.php @@ -0,0 +1,44 @@ +appPath = rtrim($appPath, '/') . '/'; + $this->templatesPath = rtrim($templatesPath, '/') . '/'; + + $this->viewPath = $this->determineViewPath($this->_urlParams()); + } + + protected function determineViewPath($urlParams): string + { + $path1 = $this->appPath . "app/views" . $urlParams . '.php'; + $path2 = $this->appPath . "app/views" . $urlParams . '/index.php'; + $path3 = $this->appPath . "app/views" . $urlParams . '.md'; + $path4 = $this->appPath . "app/views" . $urlParams . '/index.md'; + $path404 = $this->templatesPath . 'error404.php'; + + if (file_exists($path1)) { + return $path1; + } elseif (file_exists($path2)) { + return $path2; + } elseif (file_exists($path3)) { + return $path3; + } elseif (file_exists($path4)) { + return $path4; + } else { + return $path404; + } + } + + private function _urlParams(): string + { + $serverURL = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH); + // Normalize URL by removing trailing slashes + return rtrim($serverURL, "/"); + } +} diff --git a/core/classes/Framex.php b/core/classes/Framex.php new file mode 100644 index 0000000..bc20fd5 --- /dev/null +++ b/core/classes/Framex.php @@ -0,0 +1,49 @@ +viewsPath = rtrim($viewsPath, '/') . '/'; + } + + public function renderTemplate(string $file, array $data = []): void + { + $filePath = $this->appPath . "templates/" . $file . ".php"; + if (file_exists($filePath)) { + if (!empty($data)) { + extract($data); + } + require_once $filePath; + } else { + // Handle the error appropriately, e.g., log it or throw an exception + echo "Template file not found: " . htmlspecialchars($filePath); + } + } + + public function init(): void + { + $file = $this->viewPath; + $data = []; + + if (!file_exists($file)) { + $file = $this->viewsPath . 'templates/404.php'; + } + + if (strtolower(pathinfo($file, PATHINFO_EXTENSION)) === 'md') { + $markdown = file_get_contents($file); + $parsedown = new Parsedown(); + $data['view'] = '
' . $parsedown->text($markdown) . '
'; + } else { + ob_start(); + require_once $file; + $data['view'] = ob_get_clean(); + } + + $template = $data['template'] ?? 'main'; + $this->renderTemplate($template, $data); + } +} diff --git a/core/classes/Parsedown.php b/core/classes/Parsedown.php new file mode 100644 index 0000000..434b78e --- /dev/null +++ b/core/classes/Parsedown.php @@ -0,0 +1,1995 @@ +textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'tel:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) + { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') + { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) + { + $CurrentBlock = $Block; + } + else + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + protected function extractElement(array $Component) + { + if ( ! isset($Component['element'])) + { + if (isset($Component['markup'])) + { + $Component['element'] = array('rawHtml' => $Component['markup']); + } + elseif (isset($Component['hidden'])) + { + $Component['element'] = array(); + } + } + + return $Component['element']; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (strpos($Line['text'], '') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) + { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) + { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + $level = strspn($Line['text'], '#'); + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') + { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . $level, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + + # + # List + + protected function blockList($Line, ?array $CurrentBlock = null) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); + + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) + { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) + { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } + elseif ($contentIndent === 0) + { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), + 'element' => array( + 'name' => $name, + 'elements' => array(), + ), + ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') + { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') + { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) + { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) + { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) + { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['elements'] as &$li) + { + if (end($li['handler']['argument']) !== '') + { + $li['handler']['argument'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block['element']['handler']['argument'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['handler']['argument'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') + { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, ?array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed']) or isset($Block['interrupted'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ); + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'element' => array(), + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, ?array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') + { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) + { + return; + } + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] []= array( + 'name' => 'thead', + ); + + $Block['element']['elements'] []= array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] []= array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'elements' => $Elements, + ); + + $Block['element']['elements'][1]['elements'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + return array( + 'type' => 'Paragraph', + 'element' => array( + 'name' => 'p', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), + ), + ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!*_&[:<`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables = array()) + { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + $Elements = array(); + + $nonNestables = (empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) + { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) + { + if ( ! isset($Element['autobreak'])) + { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + # + # ~ + # + + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ){ + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = "mailto:$url"; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'element' => array('rawHtml' => $Excerpt['text'][1]), + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ), + 'autobreak' => true, + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), + 'nonNestables' => array('Url', 'Link'), + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['handler']['argument']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return array( + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Handlers + # + + protected function handle(array $Element) + { + if (isset($Element['handler'])) + { + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) + { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } + else + { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') + { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) + { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) + { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) + { + $markup .= $this->elements($Element['elements']); + } + elseif (isset($Element['element'])) + { + $markup .= $this->element($Element['element']); + } + else + { + if (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + } + + $markup .= $hasName ? '' : ''; + } + elseif ($hasName) + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) + { + if (empty($Element)) + { + continue; + } + + $autoBreakNext = (isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $Elements = $this->linesElements($lines); + + if ( ! in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) + { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) + { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; + } + + # + # Deprecated Methods + # + + /** + * @deprecated use text() instead + */ + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if ( ! isset($Element['name'])) + { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/core/config.php b/core/config.php new file mode 100644 index 0000000..d632991 --- /dev/null +++ b/core/config.php @@ -0,0 +1,33 @@ + 'ELLHNIKA', + ]; + return $values[$param] ?? ''; +} + diff --git a/core/functions.php b/core/functions.php new file mode 100644 index 0000000..6acdee4 --- /dev/null +++ b/core/functions.php @@ -0,0 +1,152 @@ + 'framex', 'hash' => true]) + * echo asset('js/app.js', ['absolute' => true, 'hash' => true]); + * echo asset('images/logo.png', ['fallback' => '/images/default-logo.png']); + */ +function asset($path, $options = []) { + // Default options + $defaults = [ + 'absolute' => false, + 'fallback' => null, // Fallback URL if file doesn't exist + 'prefix' => 'v', // Version parameter prefix + 'hash' => false // Use file hash instead of timestamp + ]; + + $options = array_merge($defaults, $options); + + // Remove leading slash + $path = ltrim($path, '/'); + + $documentRoot = $_SERVER['DOCUMENT_ROOT']; + $fullPath = $documentRoot . '/' . $path; + + // Generate version parameter + $version = ''; + if (file_exists($fullPath)) { + if ($options['hash']) { + // Use file hash (more reliable for cache busting) + $version = '?' . $options['prefix'] . '=' . substr(md5_file($fullPath), 0, 8); + } else { + // Use modification time + $version = '?' . $options['prefix'] . '=' . filemtime($fullPath); + } + } elseif ($options['fallback']) { + // Return fallback URL if provided + return $options['fallback']; + } else { + error_log("Asset not found: " . $fullPath); + } + + // Build URL + if ($options['absolute']) { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST']; + return $protocol . '://' . $host . '/' . $path . $version; + } else { + return '/' . $path . $version; + } +} + + + +/** + * Get Partials + * + * @param string $name The name of the partial to include. + * @throws Exception If the partial does not exist. + */ +function partial(string $name): void { + $path = TEMPLATES . '/partials/' . $name . '.php'; + if (file_exists($path)) { + include $path; + } else { + throw new Exception("Partial '{$name}' doesn't exist"); + } +} + + + +function codeBlock($code, $language = 'php') { + // 1. Convert special characters to HTML entities to prevent execution/bugs + $safe_code = htmlspecialchars($code, ENT_QUOTES, 'UTF-8'); + + // 2. Wrap in
 and  tags with the appropriate class
+    $output = '
';
+    $output .= $safe_code;
+    $output .= '
'; + + return $output; +} + + +/** + * Pre Helper + * + * @param mixed $array The data to print. + * @param bool|null $die Whether to stop execution after printing. + * @param bool $report Whether to echo the data directly. + */ +function pre($array, ?bool $die = null, bool $report = false): void { + if ($report) { + echo $array; + } + echo '
' . print_r($array, true) . '
'; + if ($die) { + die(); + } +} + +/** + * Get Random Image + * + * @param int $width The width of the image. + * @param int $height The height of the image. + * @return string The URL of the random image. + */ +function image(int $width = 960, int $height = 576): string { + return 'https://picsum.photos/' . $width . '/' . $height . '?v=' . rand(); +} + +/** + * Sanitize Input + * + * @param string $data The data to sanitize. + * @return string The sanitized data. + */ +function e(string $data): string { + return htmlspecialchars($data, ENT_QUOTES, 'UTF-8'); +} + +/** + * Summarize Text + * + * @param string $text The text to summarize. + * @param int $maxLength The maximum length of the summary. + * @return string The summarized text. + */ +function summarize(string $text, int $maxLength = 100): string { + if (strlen($text) <= $maxLength) { + return $text; + } + $summary = substr($text, 0, $maxLength); + return $summary . '...'; +} + +function urlSlug($value, $transliteration = true) +{ + if (extension_loaded('intl') && $transliteration == true) { + $transliterator = \Transliterator::create('Any-Latin; Latin-ASCII'); + $value = $transliterator->transliterate($value); + } + $slug = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); + $slug = preg_replace('~[^\pL\d]+~u', '-', $slug); + $slug = trim($slug, '-'); + $slug = strtolower($slug); + return $slug; +} + diff --git a/core/ignition.php b/core/ignition.php new file mode 100644 index 0000000..28a8800 --- /dev/null +++ b/core/ignition.php @@ -0,0 +1,17 @@ +=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.0.tgz", + "integrity": "sha512-X9kdlqyMopO9fewbgHsEeuy31YzMHbdZ9VsKt004tB+mxSg1CNbyhZYCzvhciN0AM4R4b5lvIprPjtNq7iQxpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "enhanced-resolve": "^5.21.0", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.3.0" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e039306 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "prodigital-framex", + "version": "1.0.0", + "description": "Framex Project", + "keywords": [ + "Framex" + ], + "author": "Prodigital Framex", + "license": "ISC", + "scripts": { + "build:css": "tailwindcss -i ./public/css/src/style.css -o ./public/css/style.css --minify", + "watch:css": "tailwindcss -i ./public/css/src/style.css -o ./public/css/style.css --watch" + }, + "devDependencies": { + "@tailwindcss/cli": "^4.3.0", + "tailwindcss": "^4.3.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions" + ] +} diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..bc530cb --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,8 @@ +Options +FollowSymLinks +RewriteEngine On + +# Ignore existing directories (!-d) and files (!-f) +RewriteCond %{SCRIPT_FILENAME} !-d +RewriteCond %{SCRIPT_FILENAME} !-f + +RewriteRule ^.*$ ./index.php diff --git a/public/css/responsive.css b/public/css/responsive.css new file mode 100644 index 0000000..65525ad --- /dev/null +++ b/public/css/responsive.css @@ -0,0 +1,2 @@ +/* Tailwind responsive utilities are included automatically via the main styles. + Add any extra responsive overrides here if needed. */ diff --git a/public/css/src/responsive.css b/public/css/src/responsive.css new file mode 100644 index 0000000..65525ad --- /dev/null +++ b/public/css/src/responsive.css @@ -0,0 +1,2 @@ +/* Tailwind responsive utilities are included automatically via the main styles. + Add any extra responsive overrides here if needed. */ diff --git a/public/css/src/style.css b/public/css/src/style.css new file mode 100644 index 0000000..00ea34d --- /dev/null +++ b/public/css/src/style.css @@ -0,0 +1,449 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap"); +@import "tailwindcss"; + +@custom-variant dark (&:where(.dark, .dark *)); + +@source "../../../templates/**/*.php"; +@source "../../../app/**/*.php"; +@source "../../../app/**/*.md"; + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +@layer base { + html { + color-scheme: light; + } + + html.dark { + color-scheme: dark; + } + + body { + @apply flex min-h-screen flex-col bg-slate-50 text-slate-950 antialiased transition-colors duration-300 dark:bg-slate-950 dark:text-slate-100; + font-feature-settings: + "cv02", + "cv03", + "cv04", + "cv11"; + } + + ::selection { + @apply bg-blue-600 text-white; + } + + a { + @apply transition-colors; + } + + :focus-visible { + @apply outline-2 outline-offset-2 outline-blue-500; + } + + .wrap { + @apply grow shrink-0 basis-auto; + } + + .contain, .band { + @apply mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8; + } + + .contain-narrow, .band-narrow { + @apply mx-auto w-full max-w-3xl px-4 sm:px-6 lg:px-8; + } + + .contain-wide, .band-wide { + @apply mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8; + } +} + +@layer components { + footer { + @apply grow-0; + } + + .wrp { + @apply mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8; + } + + .section, section { + @apply py-10 md:py-16; + } + + .section-tight { + @apply py-10 md:py-14; + } + + .text-gradient {@apply bg-linear-to-r from-indigo-400 to-pink-600 bg-clip-text text-transparent} + + .eyebrow { + @apply font-semibold tracking-widest text-blue-600 dark:text-blue-400; + } + + .btn { + @apply inline-flex min-h-11 items-center justify-center gap-2 rounded-lg px-5 py-2.5 text-sm font-semibold leading-none transition duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50; + } + + .btn-primary { + @apply bg-blue-600 text-white shadow-sm shadow-blue-600/25 hover:bg-blue-500 focus-visible:outline-blue-500 dark:bg-blue-500 dark:hover:bg-blue-400; + } + + .btn-secondary { + @apply border border-slate-200 bg-white text-slate-900 hover:border-slate-300 hover:bg-slate-100 focus-visible:outline-blue-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-100 dark:hover:border-slate-700 dark:hover:bg-slate-800; + } + + .btn-ghost { + @apply text-slate-700 hover:bg-slate-100 hover:text-slate-950 focus-visible:outline-blue-500 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-white; + } + + .card { + @apply rounded-lg border border-slate-200 bg-white p-6 shadow-sm shadow-slate-950/5 transition-colors dark:border-slate-800 dark:bg-slate-900/80 dark:shadow-black/20; + } + + .bg-pre-blue { + @apply bg-blue-50 text-blue-950 dark:bg-blue-950/35 dark:text-blue-50; + } + + .bg-pre-emerald { + @apply bg-emerald-50 text-emerald-950 dark:bg-emerald-950/35 dark:text-emerald-50; + } + + .bg-pre-amber { + @apply bg-amber-50 text-amber-950 dark:bg-amber-950/35 dark:text-amber-50; + } + + .bg-pre-rose { + @apply bg-rose-50 text-rose-950 dark:bg-rose-950/35 dark:text-rose-50; + } + + .nav-link { + @apply rounded-md px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-white; + } + + .prose-shell { + @apply mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8; + } + + .prose { + color: #475569; + line-height: 1.75; + } + + .dark .prose { + color: #94a3b8; + } + + .prose > * + * { + margin-top: 1.25em; + } + + .prose h1, + .prose h2, + .prose h3, + .prose h4, + .prose h5, + .prose h6 { + color: #0f172a; + font-weight: 700; + line-height: 1.25; + } + + .dark .prose h1, + .dark .prose h2, + .dark .prose h3, + .dark .prose h4, + .dark .prose h5, + .dark .prose h6 { + color: #f8fafc; + } + + .prose h1 { + font-size: 2.25rem; + margin-top: 0; + margin-bottom: 0.5em; + } + + .prose h2 { + font-size: 1.75rem; + margin-top: 2em; + margin-bottom: 0.75em; + padding-bottom: 0.4em; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + } + + .dark .prose h2 { + border-bottom-color: rgba(255, 255, 255, 0.06); + } + + .prose h3 { + font-size: 1.375rem; + margin-top: 1.75em; + margin-bottom: 0.6em; + } + + .prose h4 { + font-size: 1.125rem; + margin-top: 1.5em; + margin-bottom: 0.5em; + } + + .prose h5, + .prose h6 { + font-size: 1rem; + margin-top: 1.5em; + margin-bottom: 0.5em; + color: #334155; + } + + .dark .prose h5, + .dark .prose h6 { + color: #cbd5e1; + } + + .prose p { + margin-top: 1.25em; + margin-bottom: 1.25em; + } + + .prose a { + color: #2563eb; + text-decoration: none; + font-weight: 500; + transition: color 0.15s ease; + } + + .dark .prose a { + color: #60a5fa; + } + + .prose a:hover { + color: #1d4ed8; + text-decoration: underline; + } + + .dark .prose a:hover { + color: #93c5fd; + } + + .prose strong, + .prose b { + color: #0f172a; + font-weight: 600; + } + + .dark .prose strong, + .dark .prose b { + color: #f1f5f9; + } + + .prose em, + .prose i { + color: #334155; + } + + .dark .prose em, + .dark .prose i { + color: #cbd5e1; + } + + .prose ul, + .prose ol { + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-left: 1.625em; + } + + .prose ul { + list-style-type: disc; + } + + .prose ol { + list-style-type: decimal; + } + + .prose ul > li, + .prose ol > li { + margin-top: 0.5em; + margin-bottom: 0.5em; + padding-left: 0.375em; + } + + .prose ul > li::marker, + .prose ol > li::marker { + color: #2563eb; + } + + .dark .prose ul > li::marker, + .dark .prose ol > li::marker { + color: #3b82f6; + } + + .prose ul ul, + .prose ul ol, + .prose ol ul, + .prose ol ol { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + + .prose pre { + background-color: #f1f5f9; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 0.5rem; + padding: 1.25em; + margin-top: 1.5em; + margin-bottom: 1.5em; + overflow-x: auto; + font-size: 0.875em; + line-height: 1.7; + } + + .dark .prose pre { + background-color: #141f38; + border-color: rgba(255, 255, 255, 0.06); + } + + .prose pre code { + background: none; + border: none; + padding: 0; + font-size: inherit; + color: #334155; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + } + + .dark .prose pre code { + color: #e2e8f0; + } + + .prose code { + background-color: rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 0.375rem; + padding: 0.2em 0.4em; + font-size: 0.875em; + color: #1d4ed8; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + } + + .dark .prose code { + background-color: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.06); + color: #93c5fd; + } + + .prose blockquote { + margin-top: 1.5em; + margin-bottom: 1.5em; + padding-left: 1.25em; + border-left: 3px solid #2563eb; + font-style: italic; + color: #475569; + } + + .dark .prose blockquote { + border-left-color: #3b82f6; + color: #cbd5e1; + } + + .prose blockquote p:first-of-type { + margin-top: 0; + } + + .prose blockquote p:last-of-type { + margin-bottom: 0; + } + + .prose table { + width: 100%; + margin-top: 1.5em; + margin-bottom: 1.5em; + border-collapse: collapse; + font-size: 0.875em; + line-height: 1.5; + } + + .prose thead { + border-bottom: 1px solid rgba(0, 0, 0, 0.15); + } + + .dark .prose thead { + border-bottom-color: rgba(255, 255, 255, 0.1); + } + + .prose th { + padding: 0.75em 1em; + text-align: left; + font-weight: 600; + color: #0f172a; + } + + .dark .prose th { + color: #f1f5f9; + } + + .prose td { + padding: 0.625em 1em; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + vertical-align: top; + } + + .dark .prose td { + border-bottom-color: rgba(255, 255, 255, 0.05); + } + + .prose tbody tr:nth-child(even) { + background-color: rgba(0, 0, 0, 0.02); + } + + .dark .prose tbody tr:nth-child(even) { + background-color: rgba(255, 255, 255, 0.02); + } + + .prose tbody tr:hover { + background-color: rgba(0, 0, 0, 0.03); + } + + .dark .prose tbody tr:hover { + background-color: rgba(255, 255, 255, 0.04); + } + + .prose img { + border-radius: 0.5rem; + margin-top: 1.5em; + margin-bottom: 1.5em; + max-width: 100%; + height: auto; + display: block; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + } + + .prose hr { + margin-top: 2.5em; + margin-bottom: 2.5em; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); + } + + .dark .prose hr { + border-top-color: rgba(255, 255, 255, 0.08); + } + + .prose li > p, + .prose blockquote > p { + margin-top: 0.75em; + margin-bottom: 0.75em; + } +} + +@layer utilities { + +} diff --git a/public/css/src/vendors.css b/public/css/src/vendors.css new file mode 100644 index 0000000..d48b2cb --- /dev/null +++ b/public/css/src/vendors.css @@ -0,0 +1,2 @@ +/* Vendor styles — add third-party CSS here or @import them. + Tailwind is NOT imported here by default. */ diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..e715f81 --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,2787 @@ +/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ +@import url("https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap"); +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --color-amber-50: oklch(98.7% 0.022 95.277); + --color-amber-100: oklch(96.2% 0.059 95.617); + --color-amber-200: oklch(92.4% 0.12 95.746); + --color-amber-400: oklch(82.8% 0.189 84.429); + --color-amber-950: oklch(27.9% 0.077 45.635); + --color-emerald-50: oklch(97.9% 0.021 166.113); + --color-emerald-300: oklch(84.5% 0.143 164.978); + --color-emerald-400: oklch(76.5% 0.177 163.223); + --color-emerald-950: oklch(26.2% 0.051 172.552); + --color-blue-50: oklch(97% 0.014 254.604); + --color-blue-300: oklch(80.9% 0.105 251.813); + --color-blue-400: oklch(70.7% 0.165 254.624); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-950: oklch(28.2% 0.091 267.935); + --color-indigo-400: oklch(67.3% 0.182 276.935); + --color-indigo-600: oklch(51.1% 0.262 276.966); + --color-violet-600: oklch(54.1% 0.281 293.009); + --color-pink-600: oklch(59.2% 0.249 0.584); + --color-rose-50: oklch(96.9% 0.015 12.422); + --color-rose-300: oklch(81% 0.117 11.638); + --color-rose-400: oklch(71.2% 0.194 13.428); + --color-rose-950: oklch(27.1% 0.105 12.094); + --color-slate-50: oklch(98.4% 0.003 247.858); + --color-slate-100: oklch(96.8% 0.007 247.896); + --color-slate-200: oklch(92.9% 0.013 255.508); + --color-slate-300: oklch(86.9% 0.022 252.894); + --color-slate-400: oklch(70.4% 0.04 256.788); + --color-slate-500: oklch(55.4% 0.046 257.417); + --color-slate-600: oklch(44.6% 0.043 257.281); + --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); + --color-slate-900: oklch(20.8% 0.042 265.755); + --color-slate-950: oklch(12.9% 0.042 264.695); + --color-gray-50: oklch(98.5% 0.002 247.839); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-300: oklch(87.2% 0.01 258.338); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-900: oklch(21% 0.034 264.665); + --color-black: #000; + --color-white: #fff; + --spacing: 0.25rem; + --container-md: 28rem; + --container-lg: 32rem; + --container-xl: 36rem; + --container-2xl: 42rem; + --container-3xl: 48rem; + --container-4xl: 56rem; + --container-6xl: 72rem; + --container-7xl: 80rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); + --text-4xl: 2.25rem; + --text-4xl--line-height: calc(2.5 / 2.25); + --text-5xl: 3rem; + --text-5xl--line-height: 1; + --text-6xl: 3.75rem; + --text-6xl--line-height: 1; + --text-7xl: 4.5rem; + --text-7xl--line-height: 1; + --text-9xl: 8rem; + --text-9xl--line-height: 1; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --font-weight-extrabold: 800; + --font-weight-black: 900; + --tracking-tighter: -0.05em; + --tracking-tight: -0.025em; + --tracking-widest: 0.1em; + --leading-tight: 1.25; + --leading-relaxed: 1.625; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + --radius-2xl: 1rem; + --blur-xl: 24px; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .absolute { + position: absolute; + } + .relative { + position: relative; + } + .static { + position: static; + } + .sticky { + position: sticky; + } + .inset-0 { + inset: calc(var(--spacing) * 0); + } + .top-0 { + top: calc(var(--spacing) * 0); + } + .top-1 { + top: calc(var(--spacing) * 1); + } + .top-1\/2 { + top: calc(1 / 2 * 100%); + } + .top-1\/3 { + top: calc(1 / 3 * 100%); + } + .right-1 { + right: calc(var(--spacing) * 1); + } + .right-1\/4 { + right: calc(1 / 4 * 100%); + } + .right-5 { + right: calc(var(--spacing) * 5); + } + .bottom-5 { + bottom: calc(var(--spacing) * 5); + } + .left-1 { + left: calc(var(--spacing) * 1); + } + .left-1\/2 { + left: calc(1 / 2 * 100%); + } + .isolate { + isolation: isolate; + } + .-z-10 { + z-index: calc(10 * -1); + } + .z-50 { + z-index: 50; + } + .m-4 { + margin: calc(var(--spacing) * 4); + } + .mx-2 { + margin-inline: calc(var(--spacing) * 2); + } + .mx-auto { + margin-inline: auto; + } + .my-10 { + margin-block: calc(var(--spacing) * 10); + } + .ms-1 { + margin-inline-start: calc(var(--spacing) * 1); + } + .ms-2 { + margin-inline-start: calc(var(--spacing) * 2); + } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-3 { + margin-top: calc(var(--spacing) * 3); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .mt-5 { + margin-top: calc(var(--spacing) * 5); + } + .mt-6 { + margin-top: calc(var(--spacing) * 6); + } + .mt-8 { + margin-top: calc(var(--spacing) * 8); + } + .mt-10 { + margin-top: calc(var(--spacing) * 10); + } + .mt-12 { + margin-top: calc(var(--spacing) * 12); + } + .mr-2 { + margin-right: calc(var(--spacing) * 2); + } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-5 { + margin-bottom: calc(var(--spacing) * 5); + } + .mb-6 { + margin-bottom: calc(var(--spacing) * 6); + } + .mb-8 { + margin-bottom: calc(var(--spacing) * 8); + } + .mb-12 { + margin-bottom: calc(var(--spacing) * 12); + } + .mb-16 { + margin-bottom: calc(var(--spacing) * 16); + } + .block { + display: block; + } + .flex { + display: flex; + } + .grid { + display: grid; + } + .hidden { + display: none; + } + .inline { + display: inline; + } + .inline-block { + display: inline-block; + } + .inline-flex { + display: inline-flex; + } + .table { + display: table; + } + .size-3 { + width: calc(var(--spacing) * 3); + height: calc(var(--spacing) * 3); + } + .size-5 { + width: calc(var(--spacing) * 5); + height: calc(var(--spacing) * 5); + } + .size-6 { + width: calc(var(--spacing) * 6); + height: calc(var(--spacing) * 6); + } + .size-9 { + width: calc(var(--spacing) * 9); + height: calc(var(--spacing) * 9); + } + .size-11 { + width: calc(var(--spacing) * 11); + height: calc(var(--spacing) * 11); + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-14 { + height: calc(var(--spacing) * 14); + } + .h-60 { + height: calc(var(--spacing) * 60); + } + .h-72 { + height: calc(var(--spacing) * 72); + } + .h-80 { + height: calc(var(--spacing) * 80); + } + .h-\[300px\] { + height: 300px; + } + .h-\[600px\] { + height: 600px; + } + .h-full { + height: 100%; + } + .min-h-16 { + min-height: calc(var(--spacing) * 16); + } + .min-h-72 { + min-height: calc(var(--spacing) * 72); + } + .min-h-\[80vh\] { + min-height: 80vh; + } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-14 { + width: calc(var(--spacing) * 14); + } + .w-\[300px\] { + width: 300px; + } + .w-\[600px\] { + width: 600px; + } + .w-full { + width: 100%; + } + .w-screen { + width: 100vw; + } + .max-w-2xl { + max-width: var(--container-2xl); + } + .max-w-3xl { + max-width: var(--container-3xl); + } + .max-w-4xl { + max-width: var(--container-4xl); + } + .max-w-6xl { + max-width: var(--container-6xl); + } + .max-w-7xl { + max-width: var(--container-7xl); + } + .max-w-40 { + max-width: calc(var(--spacing) * 40); + } + .max-w-lg { + max-width: var(--container-lg); + } + .max-w-md { + max-width: var(--container-md); + } + .max-w-prose { + max-width: 65ch; + } + .max-w-xl { + max-width: var(--container-xl); + } + .flex-shrink { + flex-shrink: 1; + } + .shrink-0 { + flex-shrink: 0; + } + .flex-grow { + flex-grow: 1; + } + .grow { + flex-grow: 1; + } + .border-collapse { + border-collapse: collapse; + } + .-translate-x-1 { + --tw-translate-x: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-x-1\/2 { + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-y-1 { + --tw-translate-y: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-y-1\/2 { + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .cursor-pointer { + cursor: pointer; + } + .resize { + resize: both; + } + .grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + .flex-col { + flex-direction: column; + } + .flex-wrap { + flex-wrap: wrap; + } + .place-items-center { + place-items: center; + } + .items-center { + align-items: center; + } + .items-start { + align-items: flex-start; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .gap-1 { + gap: calc(var(--spacing) * 1); + } + .gap-1\.5 { + gap: calc(var(--spacing) * 1.5); + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-3 { + gap: calc(var(--spacing) * 3); + } + .gap-4 { + gap: calc(var(--spacing) * 4); + } + .gap-5 { + gap: calc(var(--spacing) * 5); + } + .gap-6 { + gap: calc(var(--spacing) * 6); + } + .gap-8 { + gap: calc(var(--spacing) * 8); + } + .gap-10 { + gap: calc(var(--spacing) * 10); + } + .gap-12 { + gap: calc(var(--spacing) * 12); + } + .space-y-4 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse))); + } + } + .overflow-hidden { + overflow: hidden; + } + .scroll-smooth { + scroll-behavior: smooth; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-2xl { + border-radius: var(--radius-2xl); + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .rounded-md { + border-radius: var(--radius-md); + } + .rounded-xl { + border-radius: var(--radius-xl); + } + .rounded-t-lg { + border-top-left-radius: var(--radius-lg); + border-top-right-radius: var(--radius-lg); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-y { + border-block-style: var(--tw-border-style); + border-block-width: 1px; + } + .border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + .border-solid { + --tw-border-style: solid; + border-style: solid; + } + .border-black { + border-color: var(--color-black); + } + .border-black\/5 { + border-color: color-mix(in srgb, #000 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-black) 5%, transparent); + } + } + .border-black\/10 { + border-color: color-mix(in srgb, #000 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-black) 10%, transparent); + } + } + .border-gray-100 { + border-color: var(--color-gray-100); + } + .border-gray-300 { + border-color: var(--color-gray-300); + } + .border-slate-200 { + border-color: var(--color-slate-200); + } + .border-slate-200\/80 { + border-color: color-mix(in srgb, oklch(92.9% 0.013 255.508) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-slate-200) 80%, transparent); + } + } + .bg-amber-100 { + background-color: var(--color-amber-100); + } + .bg-amber-400 { + background-color: var(--color-amber-400); + } + .bg-black { + background-color: var(--color-black); + } + .bg-black\/5 { + background-color: color-mix(in srgb, #000 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 5%, transparent); + } + } + .bg-black\/\[0\.02\] { + background-color: color-mix(in srgb, #000 2%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 2%, transparent); + } + } + .bg-blue-600 { + background-color: var(--color-blue-600); + } + .bg-emerald-400 { + background-color: var(--color-emerald-400); + } + .bg-gray-50 { + background-color: var(--color-gray-50); + } + .bg-gray-100 { + background-color: var(--color-gray-100); + } + .bg-gray-400 { + background-color: var(--color-gray-400); + } + .bg-indigo-600 { + background-color: var(--color-indigo-600); + } + .bg-indigo-600\/10 { + background-color: color-mix(in srgb, oklch(51.1% 0.262 276.966) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-indigo-600) 10%, transparent); + } + } + .bg-rose-400 { + background-color: var(--color-rose-400); + } + .bg-slate-50 { + background-color: var(--color-slate-50); + } + .bg-slate-950 { + background-color: var(--color-slate-950); + } + .bg-violet-600 { + background-color: var(--color-violet-600); + } + .bg-violet-600\/10 { + background-color: color-mix(in srgb, oklch(54.1% 0.281 293.009) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-violet-600) 10%, transparent); + } + } + .bg-white { + background-color: var(--color-white); + } + .bg-white\/70 { + background-color: color-mix(in srgb, #fff 70%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 70%, transparent); + } + } + .bg-white\/85 { + background-color: color-mix(in srgb, #fff 85%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 85%, transparent); + } + } + .bg-gradient-to-b { + --tw-gradient-position: to bottom in oklab; + background-image: linear-gradient(var(--tw-gradient-stops)); + } + .from-white { + --tw-gradient-from: var(--color-white); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-black { + --tw-gradient-to: var(--color-black); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-black\/20 { + --tw-gradient-to: color-mix(in srgb, #000 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-black) 20%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .bg-clip-text { + background-clip: text; + } + .object-cover { + object-fit: cover; + } + .p-0 { + padding: calc(var(--spacing) * 0); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-5 { + padding: calc(var(--spacing) * 5); + } + .p-6 { + padding: calc(var(--spacing) * 6); + } + .p-8 { + padding: calc(var(--spacing) * 8); + } + .p-10 { + padding: calc(var(--spacing) * 10); + } + .px-0 { + padding-inline: calc(var(--spacing) * 0); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-2\.5 { + padding-inline: calc(var(--spacing) * 2.5); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .px-5 { + padding-inline: calc(var(--spacing) * 5); + } + .px-6 { + padding-inline: calc(var(--spacing) * 6); + } + .px-8 { + padding-inline: calc(var(--spacing) * 8); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-1\.5 { + padding-block: calc(var(--spacing) * 1.5); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + .py-3\.5 { + padding-block: calc(var(--spacing) * 3.5); + } + .py-4 { + padding-block: calc(var(--spacing) * 4); + } + .py-5 { + padding-block: calc(var(--spacing) * 5); + } + .py-8 { + padding-block: calc(var(--spacing) * 8); + } + .py-10 { + padding-block: calc(var(--spacing) * 10); + } + .py-12 { + padding-block: calc(var(--spacing) * 12); + } + .py-16 { + padding-block: calc(var(--spacing) * 16); + } + .pt-4 { + padding-top: calc(var(--spacing) * 4); + } + .pb-4 { + padding-bottom: calc(var(--spacing) * 4); + } + .pb-5 { + padding-bottom: calc(var(--spacing) * 5); + } + .pb-8 { + padding-bottom: calc(var(--spacing) * 8); + } + .text-center { + text-align: center; + } + .text-left { + text-align: left; + } + .font-mono { + font-family: var(--font-mono); + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + .text-4xl { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + .text-5xl { + font-size: var(--text-5xl); + line-height: var(--tw-leading, var(--text-5xl--line-height)); + } + .text-7xl { + font-size: var(--text-7xl); + line-height: var(--tw-leading, var(--text-7xl--line-height)); + } + .text-9xl { + font-size: var(--text-9xl); + line-height: var(--tw-leading, var(--text-9xl--line-height)); + } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .leading-6 { + --tw-leading: calc(var(--spacing) * 6); + line-height: calc(var(--spacing) * 6); + } + .leading-7 { + --tw-leading: calc(var(--spacing) * 7); + line-height: calc(var(--spacing) * 7); + } + .leading-8 { + --tw-leading: calc(var(--spacing) * 8); + line-height: calc(var(--spacing) * 8); + } + .leading-tight { + --tw-leading: var(--leading-tight); + line-height: var(--leading-tight); + } + .font-black { + --tw-font-weight: var(--font-weight-black); + font-weight: var(--font-weight-black); + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-extrabold { + --tw-font-weight: var(--font-weight-extrabold); + font-weight: var(--font-weight-extrabold); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .tracking-tight { + --tw-tracking: var(--tracking-tight); + letter-spacing: var(--tracking-tight); + } + .tracking-tighter { + --tw-tracking: var(--tracking-tighter); + letter-spacing: var(--tracking-tighter); + } + .tracking-widest { + --tw-tracking: var(--tracking-widest); + letter-spacing: var(--tracking-widest); + } + .text-pretty { + text-wrap: pretty; + } + .text-wrap { + text-wrap: wrap; + } + .text-amber-200 { + color: var(--color-amber-200); + } + .text-black { + color: var(--color-black); + } + .text-black\/5 { + color: color-mix(in srgb, #000 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-black) 5%, transparent); + } + } + .text-blue-300 { + color: var(--color-blue-300); + } + .text-blue-600 { + color: var(--color-blue-600); + } + .text-current { + color: currentcolor; + } + .text-emerald-300 { + color: var(--color-emerald-300); + } + .text-gray-500 { + color: var(--color-gray-500); + } + .text-gray-700 { + color: var(--color-gray-700); + } + .text-gray-900 { + color: var(--color-gray-900); + } + .text-indigo-400 { + color: var(--color-indigo-400); + } + .text-indigo-600 { + color: var(--color-indigo-600); + } + .text-rose-300 { + color: var(--color-rose-300); + } + .text-slate-200 { + color: var(--color-slate-200); + } + .text-slate-500 { + color: var(--color-slate-500); + } + .text-slate-600 { + color: var(--color-slate-600); + } + .text-slate-700 { + color: var(--color-slate-700); + } + .text-slate-900 { + color: var(--color-slate-900); + } + .text-slate-950 { + color: var(--color-slate-950); + } + .text-transparent { + color: transparent; + } + .text-white { + color: var(--color-white); + } + .uppercase { + text-transform: uppercase; + } + .underline { + text-decoration-line: underline; + } + .antialiased { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + .opacity-60 { + opacity: 60%; + } + .opacity-70 { + opacity: 70%; + } + .opacity-80 { + opacity: 80%; + } + .shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-2xl { + --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-lg { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-sm { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-xl { + --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .ring-1 { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-black { + --tw-shadow-color: #000; + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, var(--color-black) var(--tw-shadow-alpha), transparent); + } + } + .shadow-black\/10 { + --tw-shadow-color: color-mix(in srgb, #000 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-black) 10%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .shadow-blue-600 { + --tw-shadow-color: oklch(54.6% 0.245 262.881); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, var(--color-blue-600) var(--tw-shadow-alpha), transparent); + } + } + .shadow-blue-600\/30 { + --tw-shadow-color: color-mix(in srgb, oklch(54.6% 0.245 262.881) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-blue-600) 30%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .shadow-slate-950 { + --tw-shadow-color: oklch(12.9% 0.042 264.695); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, var(--color-slate-950) var(--tw-shadow-alpha), transparent); + } + } + .shadow-slate-950\/10 { + --tw-shadow-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-slate-950) 10%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .shadow-slate-950\/20 { + --tw-shadow-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-slate-950) 20%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .ring-black { + --tw-ring-color: var(--color-black); + } + .ring-black\/5 { + --tw-ring-color: color-mix(in srgb, #000 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-black) 5%, transparent); + } + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + .blur-\[80px\] { + --tw-blur: blur(80px); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .blur-\[120px\] { + --tw-blur: blur(120px); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .backdrop-blur-xl { + --tw-backdrop-blur: blur(var(--blur-xl)); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .backdrop-filter { + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-transform { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } + .select-none { + -webkit-user-select: none; + user-select: none; + } + .\[grid-area\:1\/1\/4\/2\] { + grid-area: 1/1/4/2; + } + .group-open\:-rotate-180 { + &:is(:where(.group):is([open], :popover-open, :open) *) { + rotate: calc(180deg * -1); + } + } + .group-hover\:text-blue-500 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: var(--color-blue-500); + } + } + } + .hover\:bg-black\/10 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, #000 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 10%, transparent); + } + } + } + } + .hover\:bg-slate-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-100); + } + } + } + .hover\:bg-slate-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-200); + } + } + } + .hover\:text-blue-600 { + &:hover { + @media (hover: hover) { + color: var(--color-blue-600); + } + } + } + .sm\:mt-6 { + @media (width >= 40rem) { + margin-top: calc(var(--spacing) * 6); + } + } + .sm\:grid-cols-2 { + @media (width >= 40rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .sm\:flex-row { + @media (width >= 40rem) { + flex-direction: row; + } + } + .sm\:items-center { + @media (width >= 40rem) { + align-items: center; + } + } + .sm\:justify-between { + @media (width >= 40rem) { + justify-content: space-between; + } + } + .sm\:justify-items-stretch { + @media (width >= 40rem) { + justify-items: stretch; + } + } + .sm\:p-12 { + @media (width >= 40rem) { + padding: calc(var(--spacing) * 12); + } + } + .sm\:px-6 { + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + } + .sm\:py-24 { + @media (width >= 40rem) { + padding-block: calc(var(--spacing) * 24); + } + } + .sm\:text-left { + @media (width >= 40rem) { + text-align: left; + } + } + .sm\:text-3xl { + @media (width >= 40rem) { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + } + .sm\:text-4xl { + @media (width >= 40rem) { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + } + .sm\:text-5xl { + @media (width >= 40rem) { + font-size: var(--text-5xl); + line-height: var(--tw-leading, var(--text-5xl--line-height)); + } + } + .sm\:text-6xl { + @media (width >= 40rem) { + font-size: var(--text-6xl); + line-height: var(--tw-leading, var(--text-6xl--line-height)); + } + } + .sm\:text-9xl { + @media (width >= 40rem) { + font-size: var(--text-9xl); + line-height: var(--tw-leading, var(--text-9xl--line-height)); + } + } + .sm\:text-base { + @media (width >= 40rem) { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } + } + .sm\:text-lg\/relaxed { + @media (width >= 40rem) { + font-size: var(--text-lg); + line-height: var(--leading-relaxed); + } + } + .sm\:text-\[12rem\] { + @media (width >= 40rem) { + font-size: 12rem; + } + } + .md\:col-span-2 { + @media (width >= 48rem) { + grid-column: span 2 / span 2; + } + } + .md\:my-20 { + @media (width >= 48rem) { + margin-block: calc(var(--spacing) * 20); + } + } + .md\:mt-20 { + @media (width >= 48rem) { + margin-top: calc(var(--spacing) * 20); + } + } + .md\:mb-3 { + @media (width >= 48rem) { + margin-bottom: calc(var(--spacing) * 3); + } + } + .md\:mb-10 { + @media (width >= 48rem) { + margin-bottom: calc(var(--spacing) * 10); + } + } + .md\:mb-12 { + @media (width >= 48rem) { + margin-bottom: calc(var(--spacing) * 12); + } + } + .md\:mb-16 { + @media (width >= 48rem) { + margin-bottom: calc(var(--spacing) * 16); + } + } + .md\:block { + @media (width >= 48rem) { + display: block; + } + } + .md\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .md\:hidden { + @media (width >= 48rem) { + display: none; + } + } + .md\:h-32 { + @media (width >= 48rem) { + height: calc(var(--spacing) * 32); + } + } + .md\:w-32 { + @media (width >= 48rem) { + width: calc(var(--spacing) * 32); + } + } + .md\:grid-cols-2 { + @media (width >= 48rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .md\:grid-cols-3 { + @media (width >= 48rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + .md\:grid-cols-4 { + @media (width >= 48rem) { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + } + .md\:grid-cols-\[1\.4fr_1fr_1fr\] { + @media (width >= 48rem) { + grid-template-columns: 1.4fr 1fr 1fr; + } + } + .md\:flex-col { + @media (width >= 48rem) { + flex-direction: column; + } + } + .md\:flex-row { + @media (width >= 48rem) { + flex-direction: row; + } + } + .md\:items-center { + @media (width >= 48rem) { + align-items: center; + } + } + .md\:justify-between { + @media (width >= 48rem) { + justify-content: space-between; + } + } + .md\:space-y-8 { + @media (width >= 48rem) { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); + } + } + } + .md\:border-b { + @media (width >= 48rem) { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + } + .md\:border-gray-300 { + @media (width >= 48rem) { + border-color: var(--color-gray-300); + } + } + .md\:p-0 { + @media (width >= 48rem) { + padding: calc(var(--spacing) * 0); + } + } + .md\:p-12 { + @media (width >= 48rem) { + padding: calc(var(--spacing) * 12); + } + } + .md\:px-8 { + @media (width >= 48rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .md\:px-10 { + @media (width >= 48rem) { + padding-inline: calc(var(--spacing) * 10); + } + } + .md\:py-20 { + @media (width >= 48rem) { + padding-block: calc(var(--spacing) * 20); + } + } + .md\:pr-8 { + @media (width >= 48rem) { + padding-right: calc(var(--spacing) * 8); + } + } + .md\:text-left { + @media (width >= 48rem) { + text-align: left; + } + } + .md\:text-2xl { + @media (width >= 48rem) { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + } + .md\:text-5xl { + @media (width >= 48rem) { + font-size: var(--text-5xl); + line-height: var(--tw-leading, var(--text-5xl--line-height)); + } + } + .lg\:sticky { + @media (width >= 64rem) { + position: sticky; + } + } + .lg\:top-24 { + @media (width >= 64rem) { + top: calc(var(--spacing) * 24); + } + } + .lg\:mb-8 { + @media (width >= 64rem) { + margin-bottom: calc(var(--spacing) * 8); + } + } + .lg\:mb-16 { + @media (width >= 64rem) { + margin-bottom: calc(var(--spacing) * 16); + } + } + .lg\:block { + @media (width >= 64rem) { + display: block; + } + } + .lg\:grid-cols-2 { + @media (width >= 64rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .lg\:grid-cols-\[0\.8fr_1\.2fr\] { + @media (width >= 64rem) { + grid-template-columns: 0.8fr 1.2fr; + } + } + .lg\:grid-cols-\[0\.9fr_1\.1fr\] { + @media (width >= 64rem) { + grid-template-columns: 0.9fr 1.1fr; + } + } + .lg\:grid-cols-\[0\.75fr_1\.25fr\] { + @media (width >= 64rem) { + grid-template-columns: 0.75fr 1.25fr; + } + } + .lg\:grid-cols-\[0\.85fr_1\.15fr\] { + @media (width >= 64rem) { + grid-template-columns: 0.85fr 1.15fr; + } + } + .lg\:grid-cols-\[1\.05fr_0\.95fr\] { + @media (width >= 64rem) { + grid-template-columns: 1.05fr 0.95fr; + } + } + .lg\:flex-row { + @media (width >= 64rem) { + flex-direction: row; + } + } + .lg\:items-center { + @media (width >= 64rem) { + align-items: center; + } + } + .lg\:items-end { + @media (width >= 64rem) { + align-items: flex-end; + } + } + .lg\:items-start { + @media (width >= 64rem) { + align-items: flex-start; + } + } + .lg\:gap-6 { + @media (width >= 64rem) { + gap: calc(var(--spacing) * 6); + } + } + .lg\:gap-10 { + @media (width >= 64rem) { + gap: calc(var(--spacing) * 10); + } + } + .lg\:self-start { + @media (width >= 64rem) { + align-self: flex-start; + } + } + .lg\:px-8 { + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .lg\:py-32 { + @media (width >= 64rem) { + padding-block: calc(var(--spacing) * 32); + } + } + .lg\:text-7xl { + @media (width >= 64rem) { + font-size: var(--text-7xl); + line-height: var(--tw-leading, var(--text-7xl--line-height)); + } + } + .dark\:block { + &:where(.dark, .dark *) { + display: block; + } + } + .dark\:hidden { + &:where(.dark, .dark *) { + display: none; + } + } + .dark\:border-slate-700 { + &:where(.dark, .dark *) { + border-color: var(--color-slate-700); + } + } + .dark\:border-slate-800 { + &:where(.dark, .dark *) { + border-color: var(--color-slate-800); + } + } + .dark\:border-slate-800\/80 { + &:where(.dark, .dark *) { + border-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-slate-800) 80%, transparent); + } + } + } + .dark\:border-white\/5 { + &:where(.dark, .dark *) { + border-color: color-mix(in srgb, #fff 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-white) 5%, transparent); + } + } + } + .dark\:border-white\/10 { + &:where(.dark, .dark *) { + border-color: color-mix(in srgb, #fff 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-white) 10%, transparent); + } + } + } + .dark\:bg-black { + &:where(.dark, .dark *) { + background-color: var(--color-black); + } + } + .dark\:bg-black\/20 { + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, #000 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 20%, transparent); + } + } + } + .dark\:bg-slate-900 { + &:where(.dark, .dark *) { + background-color: var(--color-slate-900); + } + } + .dark\:bg-slate-900\/40 { + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 40%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-900) 40%, transparent); + } + } + } + .dark\:bg-slate-950 { + &:where(.dark, .dark *) { + background-color: var(--color-slate-950); + } + } + .dark\:bg-slate-950\/85 { + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 85%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-950) 85%, transparent); + } + } + } + .dark\:bg-white { + &:where(.dark, .dark *) { + background-color: var(--color-white); + } + } + .dark\:bg-white\/5 { + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, #fff 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 5%, transparent); + } + } + } + .dark\:bg-white\/\[0\.02\] { + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, #fff 2%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 2%, transparent); + } + } + } + .dark\:to-white\/20 { + &:where(.dark, .dark *) { + --tw-gradient-to: color-mix(in srgb, #fff 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-white) 20%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + } + .dark\:text-blue-400 { + &:where(.dark, .dark *) { + color: var(--color-blue-400); + } + } + .dark\:text-blue-600 { + &:where(.dark, .dark *) { + color: var(--color-blue-600); + } + } + .dark\:text-slate-100 { + &:where(.dark, .dark *) { + color: var(--color-slate-100); + } + } + .dark\:text-slate-300 { + &:where(.dark, .dark *) { + color: var(--color-slate-300); + } + } + .dark\:text-slate-400 { + &:where(.dark, .dark *) { + color: var(--color-slate-400); + } + } + .dark\:text-slate-500 { + &:where(.dark, .dark *) { + color: var(--color-slate-500); + } + } + .dark\:text-slate-950 { + &:where(.dark, .dark *) { + color: var(--color-slate-950); + } + } + .dark\:text-white { + &:where(.dark, .dark *) { + color: var(--color-white); + } + } + .dark\:text-white\/5 { + &:where(.dark, .dark *) { + color: color-mix(in srgb, #fff 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-white) 5%, transparent); + } + } + } + .dark\:text-white\/60 { + &:where(.dark, .dark *) { + color: color-mix(in srgb, #fff 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-white) 60%, transparent); + } + } + } + .dark\:text-white\/80 { + &:where(.dark, .dark *) { + color: color-mix(in srgb, #fff 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-white) 80%, transparent); + } + } + } + .dark\:text-white\/90 { + &:where(.dark, .dark *) { + color: color-mix(in srgb, #fff 90%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-white) 90%, transparent); + } + } + } + .dark\:shadow-white\/10 { + &:where(.dark, .dark *) { + --tw-shadow-color: color-mix(in srgb, #fff 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-white) 10%, transparent) var(--tw-shadow-alpha), transparent); + } + } + } + .dark\:ring-white\/10 { + &:where(.dark, .dark *) { + --tw-ring-color: color-mix(in srgb, #fff 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-white) 10%, transparent); + } + } + } + .dark\:hover\:bg-slate-900 { + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-900); + } + } + } + } + .dark\:hover\:bg-white\/10 { + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, #fff 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 10%, transparent); + } + } + } + } + } + .dark\:hover\:text-blue-400 { + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + color: var(--color-blue-400); + } + } + } + } +} +@layer base { + html { + color-scheme: light; + } + html.dark { + color-scheme: dark; + } + body { + display: flex; + min-height: 100vh; + flex-direction: column; + background-color: var(--color-slate-50); + color: var(--color-slate-950); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 300ms; + transition-duration: 300ms; + &:where(.dark, .dark *) { + background-color: var(--color-slate-950); + } + &:where(.dark, .dark *) { + color: var(--color-slate-100); + } + font-feature-settings: "cv02", "cv03", "cv04", "cv11"; + } + ::selection { + background-color: var(--color-blue-600); + color: var(--color-white); + } + a { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + :focus-visible { + outline-style: var(--tw-outline-style); + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--color-blue-500); + } + .wrap { + flex-shrink: 0; + flex-grow: 1; + flex-basis: auto; + } + .contain, .band { + margin-inline: auto; + width: 100%; + max-width: var(--container-6xl); + padding-inline: calc(var(--spacing) * 4); + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .contain-narrow, .band-narrow { + margin-inline: auto; + width: 100%; + max-width: var(--container-3xl); + padding-inline: calc(var(--spacing) * 4); + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .contain-wide, .band-wide { + margin-inline: auto; + width: 100%; + max-width: var(--container-7xl); + padding-inline: calc(var(--spacing) * 4); + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } +} +@layer components { + footer { + flex-grow: 0; + } + .wrp { + margin-inline: auto; + width: 100%; + max-width: var(--container-6xl); + padding-inline: calc(var(--spacing) * 4); + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .section, section { + padding-block: calc(var(--spacing) * 10); + @media (width >= 48rem) { + padding-block: calc(var(--spacing) * 16); + } + } + .section-tight { + padding-block: calc(var(--spacing) * 10); + @media (width >= 48rem) { + padding-block: calc(var(--spacing) * 14); + } + } + .text-gradient { + --tw-gradient-position: to right; + @supports (background-image: linear-gradient(in lab, red, red)) { + --tw-gradient-position: to right in oklab; + } + background-image: linear-gradient(var(--tw-gradient-stops)); + --tw-gradient-from: var(--color-indigo-400); + --tw-gradient-to: var(--color-pink-600); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + background-clip: text; + color: transparent; + } + .eyebrow { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + --tw-tracking: var(--tracking-widest); + letter-spacing: var(--tracking-widest); + color: var(--color-blue-600); + &:where(.dark, .dark *) { + color: var(--color-blue-400); + } + } + .btn { + display: inline-flex; + min-height: calc(var(--spacing) * 11); + align-items: center; + justify-content: center; + gap: calc(var(--spacing) * 2); + border-radius: var(--radius-lg); + padding-inline: calc(var(--spacing) * 5); + padding-block: calc(var(--spacing) * 2.5); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + --tw-leading: 1; + line-height: 1; + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 200ms; + transition-duration: 200ms; + &:focus-visible { + outline-style: var(--tw-outline-style); + outline-width: 2px; + } + &:focus-visible { + outline-offset: 2px; + } + &:disabled { + pointer-events: none; + } + &:disabled { + opacity: 50%; + } + } + .btn-primary { + background-color: var(--color-blue-600); + color: var(--color-white); + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + --tw-shadow-color: color-mix(in srgb, oklch(54.6% 0.245 262.881) 25%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-blue-600) 25%, transparent) var(--tw-shadow-alpha), transparent); + } + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-500); + } + } + &:focus-visible { + outline-color: var(--color-blue-500); + } + &:where(.dark, .dark *) { + background-color: var(--color-blue-500); + } + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-400); + } + } + } + } + .btn-secondary { + border-style: var(--tw-border-style); + border-width: 1px; + border-color: var(--color-slate-200); + background-color: var(--color-white); + color: var(--color-slate-900); + &:hover { + @media (hover: hover) { + border-color: var(--color-slate-300); + } + } + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-100); + } + } + &:focus-visible { + outline-color: var(--color-blue-500); + } + &:where(.dark, .dark *) { + border-color: var(--color-slate-800); + } + &:where(.dark, .dark *) { + background-color: var(--color-slate-900); + } + &:where(.dark, .dark *) { + color: var(--color-slate-100); + } + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + border-color: var(--color-slate-700); + } + } + } + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-800); + } + } + } + } + .btn-ghost { + color: var(--color-slate-700); + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-100); + } + } + &:hover { + @media (hover: hover) { + color: var(--color-slate-950); + } + } + &:focus-visible { + outline-color: var(--color-blue-500); + } + &:where(.dark, .dark *) { + color: var(--color-slate-300); + } + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-900); + } + } + } + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + color: var(--color-white); + } + } + } + } + .card { + border-radius: var(--radius-lg); + border-style: var(--tw-border-style); + border-width: 1px; + border-color: var(--color-slate-200); + background-color: var(--color-white); + padding: calc(var(--spacing) * 6); + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + --tw-shadow-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-slate-950) 5%, transparent) var(--tw-shadow-alpha), transparent); + } + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + &:where(.dark, .dark *) { + border-color: var(--color-slate-800); + } + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-900) 80%, transparent); + } + } + &:where(.dark, .dark *) { + --tw-shadow-color: color-mix(in srgb, #000 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-black) 20%, transparent) var(--tw-shadow-alpha), transparent); + } + } + } + .bg-pre-blue { + background-color: var(--color-blue-50); + color: var(--color-blue-950); + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, oklch(28.2% 0.091 267.935) 35%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-blue-950) 35%, transparent); + } + } + &:where(.dark, .dark *) { + color: var(--color-blue-50); + } + } + .bg-pre-emerald { + background-color: var(--color-emerald-50); + color: var(--color-emerald-950); + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, oklch(26.2% 0.051 172.552) 35%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-emerald-950) 35%, transparent); + } + } + &:where(.dark, .dark *) { + color: var(--color-emerald-50); + } + } + .bg-pre-amber { + background-color: var(--color-amber-50); + color: var(--color-amber-950); + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, oklch(27.9% 0.077 45.635) 35%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-amber-950) 35%, transparent); + } + } + &:where(.dark, .dark *) { + color: var(--color-amber-50); + } + } + .bg-pre-rose { + background-color: var(--color-rose-50); + color: var(--color-rose-950); + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, oklch(27.1% 0.105 12.094) 35%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-rose-950) 35%, transparent); + } + } + &:where(.dark, .dark *) { + color: var(--color-rose-50); + } + } + .nav-link { + border-radius: var(--radius-md); + padding-inline: calc(var(--spacing) * 3); + padding-block: calc(var(--spacing) * 2); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + color: var(--color-slate-700); + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-100); + } + } + &:hover { + @media (hover: hover) { + color: var(--color-slate-950); + } + } + &:where(.dark, .dark *) { + color: var(--color-slate-300); + } + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-900); + } + } + } + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + color: var(--color-white); + } + } + } + } + .prose-shell { + margin-inline: auto; + max-width: var(--container-4xl); + padding-inline: calc(var(--spacing) * 4); + padding-block: calc(var(--spacing) * 16); + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .prose { + color: #475569; + line-height: 1.75; + } + .dark .prose { + color: #94a3b8; + } + .prose > * + * { + margin-top: 1.25em; + } + .prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 { + color: #0f172a; + font-weight: 700; + line-height: 1.25; + } + .dark .prose h1, .dark .prose h2, .dark .prose h3, .dark .prose h4, .dark .prose h5, .dark .prose h6 { + color: #f8fafc; + } + .prose h1 { + font-size: 2.25rem; + margin-top: 0; + margin-bottom: 0.5em; + } + .prose h2 { + font-size: 1.75rem; + margin-top: 2em; + margin-bottom: 0.75em; + padding-bottom: 0.4em; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + } + .dark .prose h2 { + border-bottom-color: rgba(255, 255, 255, 0.06); + } + .prose h3 { + font-size: 1.375rem; + margin-top: 1.75em; + margin-bottom: 0.6em; + } + .prose h4 { + font-size: 1.125rem; + margin-top: 1.5em; + margin-bottom: 0.5em; + } + .prose h5, .prose h6 { + font-size: 1rem; + margin-top: 1.5em; + margin-bottom: 0.5em; + color: #334155; + } + .dark .prose h5, .dark .prose h6 { + color: #cbd5e1; + } + .prose p { + margin-top: 1.25em; + margin-bottom: 1.25em; + } + .prose a { + color: #2563eb; + text-decoration: none; + font-weight: 500; + transition: color 0.15s ease; + } + .dark .prose a { + color: #60a5fa; + } + .prose a:hover { + color: #1d4ed8; + text-decoration: underline; + } + .dark .prose a:hover { + color: #93c5fd; + } + .prose strong, .prose b { + color: #0f172a; + font-weight: 600; + } + .dark .prose strong, .dark .prose b { + color: #f1f5f9; + } + .prose em, .prose i { + color: #334155; + } + .dark .prose em, .dark .prose i { + color: #cbd5e1; + } + .prose ul, .prose ol { + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-left: 1.625em; + } + .prose ul { + list-style-type: disc; + } + .prose ol { + list-style-type: decimal; + } + .prose ul > li, .prose ol > li { + margin-top: 0.5em; + margin-bottom: 0.5em; + padding-left: 0.375em; + } + .prose ul > li::marker, .prose ol > li::marker { + color: #2563eb; + } + .dark .prose ul > li::marker, .dark .prose ol > li::marker { + color: #3b82f6; + } + .prose ul ul, .prose ul ol, .prose ol ul, .prose ol ol { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + .prose pre { + background-color: #f1f5f9; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 0.5rem; + padding: 1.25em; + margin-top: 1.5em; + margin-bottom: 1.5em; + overflow-x: auto; + font-size: 0.875em; + line-height: 1.7; + } + .dark .prose pre { + background-color: #141f38; + border-color: rgba(255, 255, 255, 0.06); + } + .prose pre code { + background: none; + border: none; + padding: 0; + font-size: inherit; + color: #334155; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + } + .dark .prose pre code { + color: #e2e8f0; + } + .prose code { + background-color: rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 0.375rem; + padding: 0.2em 0.4em; + font-size: 0.875em; + color: #1d4ed8; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + } + .dark .prose code { + background-color: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.06); + color: #93c5fd; + } + .prose blockquote { + margin-top: 1.5em; + margin-bottom: 1.5em; + padding-left: 1.25em; + border-left: 3px solid #2563eb; + font-style: italic; + color: #475569; + } + .dark .prose blockquote { + border-left-color: #3b82f6; + color: #cbd5e1; + } + .prose blockquote p:first-of-type { + margin-top: 0; + } + .prose blockquote p:last-of-type { + margin-bottom: 0; + } + .prose table { + width: 100%; + margin-top: 1.5em; + margin-bottom: 1.5em; + border-collapse: collapse; + font-size: 0.875em; + line-height: 1.5; + } + .prose thead { + border-bottom: 1px solid rgba(0, 0, 0, 0.15); + } + .dark .prose thead { + border-bottom-color: rgba(255, 255, 255, 0.1); + } + .prose th { + padding: 0.75em 1em; + text-align: left; + font-weight: 600; + color: #0f172a; + } + .dark .prose th { + color: #f1f5f9; + } + .prose td { + padding: 0.625em 1em; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + vertical-align: top; + } + .dark .prose td { + border-bottom-color: rgba(255, 255, 255, 0.05); + } + .prose tbody tr:nth-child(even) { + background-color: rgba(0, 0, 0, 0.02); + } + .dark .prose tbody tr:nth-child(even) { + background-color: rgba(255, 255, 255, 0.02); + } + .prose tbody tr:hover { + background-color: rgba(0, 0, 0, 0.03); + } + .dark .prose tbody tr:hover { + background-color: rgba(255, 255, 255, 0.04); + } + .prose img { + border-radius: 0.5rem; + margin-top: 1.5em; + margin-bottom: 1.5em; + max-width: 100%; + height: auto; + display: block; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + } + .prose hr { + margin-top: 2.5em; + margin-bottom: 2.5em; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); + } + .dark .prose hr { + border-top-color: rgba(255, 255, 255, 0.08); + } + .prose li > p, .prose blockquote > p { + margin-top: 0.75em; + margin-bottom: 0.75em; + } +} +@layer utilities; +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-gradient-position { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-via { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-to { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-via-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from-position { + syntax: ""; + inherits: false; + initial-value: 0%; +} +@property --tw-gradient-via-position { + syntax: ""; + inherits: false; + initial-value: 50%; +} +@property --tw-gradient-to-position { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-leading { + syntax: "*"; + inherits: false; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-tracking { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-blur { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-invert { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-space-y-reverse: 0; + --tw-border-style: solid; + --tw-gradient-position: initial; + --tw-gradient-from: #0000; + --tw-gradient-via: #0000; + --tw-gradient-to: #0000; + --tw-gradient-stops: initial; + --tw-gradient-via-stops: initial; + --tw-gradient-from-position: 0%; + --tw-gradient-via-position: 50%; + --tw-gradient-to-position: 100%; + --tw-leading: initial; + --tw-font-weight: initial; + --tw-tracking: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-backdrop-blur: initial; + --tw-backdrop-brightness: initial; + --tw-backdrop-contrast: initial; + --tw-backdrop-grayscale: initial; + --tw-backdrop-hue-rotate: initial; + --tw-backdrop-invert: initial; + --tw-backdrop-opacity: initial; + --tw-backdrop-saturate: initial; + --tw-backdrop-sepia: initial; + --tw-duration: initial; + } + } +} diff --git a/public/css/vendors.css b/public/css/vendors.css new file mode 100644 index 0000000..d48b2cb --- /dev/null +++ b/public/css/vendors.css @@ -0,0 +1,2 @@ +/* Vendor styles — add third-party CSS here or @import them. + Tailwind is NOT imported here by default. */ diff --git a/public/images/thanos.png b/public/images/thanos.png new file mode 100644 index 0000000..52acfb4 Binary files /dev/null and b/public/images/thanos.png differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..7d69cca --- /dev/null +++ b/public/index.php @@ -0,0 +1,7 @@ +init(); diff --git a/public/js/init.js b/public/js/init.js new file mode 100644 index 0000000..f8cf22b --- /dev/null +++ b/public/js/init.js @@ -0,0 +1,58 @@ +"use strict"; + +(function () { + const framexEngine = { + init() { + this.bindThemeToggle(); + this.bindMobileMenu(); + }, + + bindThemeToggle() { + const buttons = document.querySelectorAll("[data-theme-toggle]"); + if (!buttons.length) { + return; + } + + const applyTheme = (theme) => { + const isDark = theme === "dark"; + document.documentElement.classList.toggle("dark", isDark); + try { + localStorage.setItem("framex-theme", theme); + } catch (error) {} + buttons.forEach((button) => { + button.setAttribute("aria-pressed", String(isDark)); + }); + }; + + buttons.forEach((button) => { + button.addEventListener("click", () => { + const nextTheme = document.documentElement.classList.contains("dark") + ? "light" + : "dark"; + applyTheme(nextTheme); + }); + }); + }, + + bindMobileMenu() { + const button = document.querySelector("[data-mobile-menu-toggle]"); + const menu = document.querySelector("[data-mobile-menu]"); + if (!button || !menu) { + return; + } + + button.addEventListener("click", () => { + const isOpen = button.getAttribute("aria-expanded") === "true"; + button.setAttribute("aria-expanded", String(!isOpen)); + menu.classList.toggle("hidden", isOpen); + }); + }, + }; + + // Initialize on DOM ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => framexEngine.init()); + } else { + framexEngine.init(); + } +})(); diff --git a/templates/clean.php b/templates/clean.php new file mode 100644 index 0000000..6ddf564 --- /dev/null +++ b/templates/clean.php @@ -0,0 +1,31 @@ + + + + + + + <?= (isset($title) ? $title : 'Framex Engine') ?> + + + + + + + +
+ +
+ + + + + + diff --git a/templates/error404.php b/templates/error404.php new file mode 100644 index 0000000..6eefa58 --- /dev/null +++ b/templates/error404.php @@ -0,0 +1,57 @@ + + +
+ +
+
+
+
+
+ +
+ +
+

+ 404 +

+
+ + 404 + +
+
+ + +

+ Page not found +

+

+ The page you are looking for does not exist. It might have been moved, renamed, or maybe it never existed in the first place. +

+ + + + + +
+

Did you mean to create this file?

+ + app/views.php + +
+
+
diff --git a/templates/main.php b/templates/main.php new file mode 100644 index 0000000..f766cf0 --- /dev/null +++ b/templates/main.php @@ -0,0 +1,57 @@ + + + + + + + + <?= $metaTitle ?> + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + diff --git a/templates/partials/footer.php b/templates/partials/footer.php new file mode 100644 index 0000000..3126cb5 --- /dev/null +++ b/templates/partials/footer.php @@ -0,0 +1,39 @@ + + diff --git a/templates/partials/topmenu.php b/templates/partials/topmenu.php new file mode 100644 index 0000000..07f460b --- /dev/null +++ b/templates/partials/topmenu.php @@ -0,0 +1,57 @@ + +
+ + + +