diff --git a/README.md b/README.md index 8da7443..d0dd43b 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,21 @@ - Generates HTML from Markdown using pandoc, commonmark, or markdown.pl (configurable) - Supports post metadata (title, date, tags) -- Supports `lastmod` timestamp in frontmatter for tracking content updates (used in sitemap, RSS feed, and optionally displayed on posts). +- Supports `lastmod` timestamp in frontmatter for tracking content updates (used in sitemap, RSS feed, and optionally displayed on posts) - Full date and time support with timezone awareness - Post descriptions/summaries for previews, OpenGraph, and RSS - Admin interface for managing posts and scheduling publications (planned for future release) - Standalone post editor with modern Ghost-like interface for visual content creation -- Creates tag index pages +- Creates tag index pages with optional tag RSS feeds +- Related Posts: automatically suggests related posts based on shared tags at the end of each post +- Author index pages with conditional navigation menu and optional author RSS feeds - Archives by year and month for chronological browsing - Dynamic menu generation based on available pages - Support for primary and secondary pages with automatic menu organization -- Generates sitemap.xml and RSS feed with timezone support +- Generates `sitemap.xml` and RSS feeds with timezone support +- Two build modes: `normal` (incremental, cache-backed) and `ram` (memory-first) +- RAM mode stage timing summary printed at the end of each RAM build +- Asset pre-compression with incremental and parallel gzip processing (`.html`, `.css`, `.xml`, `.js`) - Clean design - No JavaScript required (except for admin interface) - Works well without images @@ -49,11 +54,9 @@ - Supports static files (images, CSS, JS, etc.) - Configurable clean output directory option - Draft posts support -- Post scheduling system - Backup and restore functionality -- Incremental builds with file caching for improved performance -- Smart metadata caching system -- Parallel processing support using GNU parallel (if available) +- Incremental builds with file and metadata caching for improved performance +- Parallel processing with GNU parallel (if available) plus shell-worker fallbacks - File locking for safe concurrent operations - Automatic handling of different operating systems (Linux/macOS/BSDs) - Custom URL slugs with SEO-friendly permalinks @@ -204,20 +207,25 @@ BSSG/ ├── scripts/ # Supporting scripts │ ├── build/ # Modular build scripts │ │ ├── main.sh # Main build orchestrator -│ │ ├── utils.sh # Utility functions (colors, formatting, etc.) -│ │ ├── cli.sh # Command-line argument parsing -│ │ ├── config_loader.sh # Loads default and user configuration -│ │ ├── deps.sh # Dependency checking -│ │ ├── cache.sh # Cache management functions -│ │ ├── content_discovery.sh # Finds posts, pages, drafts -│ │ ├── markdown_processor.sh # Markdown conversion logic -│ │ ├── process_posts.sh # Processes individual posts -│ │ ├── process_pages.sh # Processes individual pages -│ │ ├── generate_indexes.sh # Creates index, tag, and archive pages -│ │ ├── generate_feeds.sh # Creates RSS feed and sitemap +│ │ ├── config_loader.sh # Loads defaults and local overrides +│ │ ├── deps.sh # Dependency checks +│ │ ├── cache.sh # Cache/config hash helpers +│ │ ├── content.sh # Metadata/excerpt/markdown helpers +│ │ ├── indexing.sh # File/tags/authors/archive index builders +│ │ ├── templates.sh # Template preload/menu generation +│ │ ├── generate_posts.sh # Post rendering +│ │ ├── generate_pages.sh # Static page rendering +│ │ ├── generate_index.sh # Homepage/pagination generation +│ │ ├── generate_tags.sh # Tag pages (+ optional tag RSS) +│ │ ├── generate_authors.sh # Author pages (+ optional author RSS) +│ │ ├── generate_archives.sh # Archive pages (year/month) +│ │ ├── generate_feeds.sh # Main RSS + sitemap │ │ ├── generate_secondary_pages.sh # Creates pages.html index -│ │ ├── copy_static.sh # Copies static files and theme assets -│ │ └── theme_utils.sh # Theme-related utilities +│ │ ├── related_posts.sh # Related-post indexing/render helpers +│ │ ├── post_process.sh # URL rewrite + permissions fixes +│ │ ├── assets.sh # Static copy + CSS/theme handling +│ │ ├── ram_mode.sh # RAM-mode preload/in-memory datasets +│ │ └── utils.sh # Shared helpers (time, URLs, parallel) │ ├── post.sh # Handles post creation │ ├── page.sh # Handles page creation │ ├── edit.sh # Handles post/page editing (updates lastmod) @@ -225,6 +233,8 @@ BSSG/ │ ├── list.sh # Lists posts, pages, drafts, tags │ ├── backup.sh # Backup functionality │ ├── restore.sh # Restore functionality +│ ├── benchmark.sh # Build benchmarking helper +│ ├── server.sh # Local development server implementation │ ├── theme.sh # Theme management and processing (legacy helper) │ ├── template.sh # Template processing utilities (legacy helper) │ └── css.sh # CSS generation utilities (legacy helper) @@ -258,58 +268,33 @@ BSSG/ ```bash cd BSSG -./bssg.sh [command] [options] +./bssg.sh [--config ] [command] [options] ``` ### Available Commands ``` -Usage: ./bssg.sh command [options] +Usage: ./bssg.sh [--config ] command [options] Commands: - post [-html] [draft_file] # Interactive: Create/edit post/draft, prompt for title, open editor. - # Rebuilds site afterwards if REBUILD_AFTER_POST=true in config. - # Use -html for HTML format. + post [-html] [draft_file] + Interactive: create/edit post or continue a draft. post -t [-T <tags>] [-s <slug>] [--html] [-d] {-c <content> | -f <file> | --stdin} [--build] - # Command-line: Create post non-interactively. - # -t: Title (required) - # -T: Tags (comma-sep) - # -s: Slug (optional) - # --html: HTML format (default: MD) - # -d: Save as draft - # -c: Content string - # -f: Content file - # --stdin: Content from stdin - # --build: Force rebuild (overrides REBUILD_AFTER_POST=false) - page [-html] [-s] [draft_file] Create a new page (in $PAGES_DIR or $DRAFTS_DIR/pages) - or continue editing a draft (in $DRAFTS_DIR/pages) - Use -html to edit in HTML instead of Markdown - Use -s to mark page as secondary (for menu) - edit [-n] <file> Edit an existing post/page/draft (updates lastmod) - File path should point to $SRC_DIR, $PAGES_DIR, $DRAFTS_DIR etc. - Use -n to rename based on title (posts/drafts only currently) - delete [-f] <file> Delete a post/page/draft - File path should point to $SRC_DIR, $PAGES_DIR, $DRAFTS_DIR etc. - Use -f to skip confirmation - list {posts|pages|drafts|tags [-n]} - List posts ($SRC_DIR), pages ($PAGES_DIR), - drafts ($DRAFTS_DIR and $DRAFTS_DIR/pages), or tags. - For tags, use -n to sort by count. - backup Create a backup of all posts, pages, drafts, and config - restore [backup_file|ID] Restore from a backup (all content by default) - Options: --no-content, --no-config - backups List all available backups - build [opts] Build the site using the modular build system in scripts/build/ - Options: -c|--clean-output, -f|--force-rebuild, - --config FILE, --theme NAME, - --site-url URL, --output DIR - init <target_directory> Initialize a new, empty site structure in the specified directory. - This is useful for separating your site content from the BSSG core scripts. - The script will preserve the path format you provide (relative, absolute, or tilde-prefixed) - in the generated site 'config.sh.local' for portability. - Note: If using '~' for your home directory, quote the path (e.g., '~/mysite' or "~/mysite") - to ensure the tilde is preserved in the generated config. - help Show this help message + Command-line: create post non-interactively. + page [-html] [-s] [draft_file] + Create a page or continue a page draft. + edit [-n] <file> Edit an existing post/page/draft (updates lastmod). + delete [-f] <file> Delete a post/page/draft. + list List all posts. + tags [-n] List all tags. Use -n to sort by post count. + drafts List all draft posts. + backup Create a backup of posts, pages, drafts, and config. + restore [backup_file|ID] Restore from a backup (options: --no-content, --no-config). + backups List all available backups. + build [options] Build the site (run './bssg.sh build --help' for full options). + server [options] Build and run local server (run './bssg.sh server --help'). + init <target_directory> Initialize a new site in the specified directory. + help Show help. ``` ### Creating Posts and Pages @@ -466,23 +451,39 @@ You can use these options with restore to selectively restore content: Usage: ./bssg.sh build [options] Options: - -c, --clean-output Empty the output directory before building + --src DIR Override source directory (from config: SRC_DIR) + --pages DIR Override pages directory (from config: PAGES_DIR) + --drafts DIR Override drafts directory (from config: DRAFTS_DIR) + --output DIR Override output directory (from config: OUTPUT_DIR) + --templates DIR Override templates directory (from config: TEMPLATES_DIR) + --themes-dir DIR Override themes directory (from config: THEMES_DIR) + --theme NAME Override theme for this build + --static DIR Override static directory (from config: STATIC_DIR) + --clean-output [bool] Clean output directory before build (default from config) -f, --force-rebuild Ignore cache and rebuild all files - --config FILE Use a specific configuration file (e.g., my_config.sh) - instead of the default config.sh - --src DIR Override the SRC_DIR specified in the config file - --pages DIR Override the PAGES_DIR specified in the config file - --drafts DIR Override the DRAFTS_DIR specified in the config file - --output DIR Build the site to a specific output directory - --templates DIR Override the TEMPLATES_DIR specified in the config file - --themes-dir DIR Override the THEMES_DIR specified in the config file - --theme NAME Override the theme specified in the config file for this build - --static DIR Override the STATIC_DIR specified in the config file - --site-url URL Override the SITE_URL specified in the config file for this build + --build-mode MODE Build mode: normal or ram + --site-title TITLE Override site title + --site-url URL Override site URL + --site-description DESC Override site description + --author-name NAME Override author name + --author-email EMAIL Override author email + --posts-per-page NUM Override pagination size --deploy Force deployment after successful build (overrides config) - --no-deploy Prevent deployment after build (overrides config) + --no-deploy Skip deployment after build (overrides config) + --help Show build help ``` +`--config <path>` is a global option and can be passed with any command (including `build`) to load a specific configuration file. + +Examples: + +```bash +./bssg.sh --config /path/to/site/config.sh.local build --build-mode ram +./bssg.sh build --output ./public --clean-output true +``` + +The option list above reflects the current `build --help` output. + ### Internationalization (i18n) BSSG supports generating the site in different languages. @@ -538,6 +539,9 @@ slug: custom-slug image: /path/to/image.jpg image_caption: Optional caption for the image description: A brief summary of your post that will appear in listings, social media shares, and RSS feeds. +author_name: John Doe # Optional: Override default site author +author_email: john@example.com # Optional: Override default site author email +fediverse_creator: @john@example.social # Optional: Override the fediverse:creator meta tag for this post --- Content goes here... @@ -573,6 +577,163 @@ When you specify an image, it will appear: - In the RSS feed - In OpenGraph and Twitter metadata for better social media sharing +### Multi-Author Support + +BSSG supports multiple authors through optional frontmatter fields that can override the default site author configuration on a per-post basis. + +#### Author Fields + +- `author_name`: The name of the post author (optional) +- `author_email`: The email address of the post author (optional) +- `fediverse_creator`: Explicit override for the post's `<meta name="fediverse:creator">` tag (optional) + +#### Fallback Behavior + +BSSG uses intelligent fallback logic for author information: + +1. **Custom Author**: If both `author_name` and `author_email` are specified, they will be used for that post +2. **Name Only**: If only `author_name` is specified, the name will be used but no email will be included in metadata +3. **Default Fallback**: If author fields are empty or missing, the default `AUTHOR_NAME` and `AUTHOR_EMAIL` from your site configuration will be used + +#### Author Index Pages + +When multiple authors are detected in your posts, BSSG automatically generates: + +- **Main Authors Index**: A page at `/authors/` listing all authors with their post counts +- **Individual Author Pages**: Pages at `/authors/author-slug/` showing all posts by a specific author +- **Conditional Navigation**: An "Authors" menu item that only appears when you have multiple authors (configurable threshold) + +The author pages reuse the same styling as tag pages for visual consistency and include: +- Post listings sorted by date (newest first) +- Post counts and metadata +- Schema.org structured data for SEO +- Responsive design that works on all devices + +#### Configuration Options + +You can control author page behavior in your `config.sh.local`: + +```bash +# Enable/disable author pages (default: false) +ENABLE_AUTHOR_PAGES=false + +# Minimum number of authors to show the Authors menu (default: 2) +SHOW_AUTHORS_MENU_THRESHOLD=2 + +# Enable author-specific RSS feeds (default: false) +ENABLE_AUTHOR_RSS=false +``` + +#### Where Author Information Appears + +Author information is displayed and used in: + +- **Post Pages**: Copyright notices in the footer +- **Index Pages**: "by Author Name" in post listings +- **Author Pages**: Dedicated pages listing posts by each author +- **Navigation Menu**: "Authors" link (when multiple authors exist) +- **RSS Feeds**: Dublin Core `dc:creator` elements with proper author attribution +- **Schema.org Metadata**: JSON-LD structured data for search engines +- **Archive Pages**: Author information in post listings + +### Fediverse Creator Tag + +BSSG can emit Mastodon's `fediverse:creator` metadata across generated pages so link previews can show and follow the author more easily. + +#### Fallback Order + +BSSG resolves the creator tag in this order: + +1. `fediverse_creator` in the post frontmatter +2. `AUTHOR_FEDIVERSE_CREATORS["Author Name"]` from config, matched against `author_name` +3. `FEDIVERSE_CREATOR` from config + +If none of those are set, no `fediverse:creator` meta tag is emitted. + +For non-post pages such as the homepage, tags, archives, authors, and static pages, BSSG uses the resolved site-level/default creator. Individual posts can still override that value with `fediverse_creator` in frontmatter. + +#### Configuration + +Add a site-wide default in `config.sh.local`: + +```bash +FEDIVERSE_CREATOR="@you@example.social" +``` + +For multi-author sites, you can optionally add exact-match per-author overrides: + +```bash +declare -A AUTHOR_FEDIVERSE_CREATORS=( + ["Jane Smith"]="@jane@example.social" + ["John Doe"]="@john@example.com" +) +``` + +If you customize `templates/header.html`, keep `{{fediverse_creator_meta}}` inside `<head>`. The bundled template already includes it, and BSSG also falls back to injecting the tag before `</head>` for older custom headers. + +#### Examples + +**Post with custom author:** +```markdown +--- +title: Guest Post Example +author_name: Jane Smith +author_email: jane@example.com +--- +``` + +**Post with name only (no email):** +```markdown +--- +title: Anonymous Contributor Post +author_name: Anonymous Contributor +author_email: # Leave empty - no email will be included +--- +``` + +**Post using default site author:** +```markdown +--- +title: Regular Post +# No author fields - will use AUTHOR_NAME and AUTHOR_EMAIL from config +--- +``` + +This feature is particularly useful for: +- Guest posts from different authors +- Multi-author blogs or publications +- Posts where you want to credit a specific contributor +- Maintaining author attribution when migrating content from other platforms +- Creating author-focused content organization alongside tags and archives + +### Fediverse Profile Verification + +BSSG can also emit one or more site-wide `<link rel="me">` tags in the document `<head>`, which is useful for Mastodon and compatible fediverse profile verification. + +Add this to `config.sh.local` for a single profile: + +```bash +REL_ME_URL="https://mastodon.example.com/@john" +``` + +Or use multiple links: + +```bash +REL_ME_URLS=( + "https://mastodon.example.com/@john" + "https://another-fedi.example/@john" +) +``` + +The default header.html now includes a `{{rel_me_link}}` placeholder, which expands to one or more tags such as: + +```html +<link rel="me" href="https://mastodon.example.com/@john"> +<link rel="me" href="https://another-fedi.example/@john"> +``` + +If both `REL_ME_URL` and `REL_ME_URLS` are set, BSSG emits all unique URLs from both. If neither is set, BSSG omits the tags. + ## Customization To customize the appearance of your site, you can edit: @@ -594,40 +755,99 @@ The `config.sh` file contains the default configuration settings for the site ge ```bash # Directory configuration -SRC_DIR="src" # Source directory for posts -PAGES_DIR="pages" # Source directory for pages -DRAFTS_DIR="drafts" # Source directory for drafts (posts and pages) -OUTPUT_DIR="output" # Where the generated site is placed +SRC_DIR="src" +PAGES_DIR="pages" # Directory for static pages +OUTPUT_DIR="output" TEMPLATES_DIR="templates" THEMES_DIR="themes" STATIC_DIR="static" +DRAFTS_DIR="drafts" # Directory for drafts THEME="default" +CACHE_DIR=".bssg_cache" # Default cache directory location (relative to BSSG root) # Build configuration -CLEAN_OUTPUT=false +CLEAN_OUTPUT=false # If true, BSSG will always perform a full rebuild +REBUILD_AFTER_POST=true # Build site automatically after creating a new post (scripts/post.sh) +REBUILD_AFTER_EDIT=true # Build site automatically after editing a post (scripts/edit.sh) +PRECOMPRESS_ASSETS="false" # Options: "true", "false". If true, compress text assets (HTML, CSS, XML, JS) with gzip during build. +BUILD_MODE="normal" # Options: "normal", "ram". RAM mode preloads inputs and keeps build indexes/data in memory. + +# Optional performance tunables (not required): +# RAM_MODE_MAX_JOBS=6 # Cap parallel workers in RAM mode (defaults to 6) +# RAM_MODE_VERBOSE=false # Extra RAM-mode debug/timing logs +# PRECOMPRESS_GZIP_LEVEL=9 # gzip level for precompression (1-9) +# PRECOMPRESS_MAX_JOBS=0 # 0=auto based on CPU/RAM mode cap +# PRECOMPRESS_VERBOSE=false # Verbose logs for precompression +# RAM_RSS_PREFILL_MIN_HITS=2 # RAM tag-RSS cache prefill threshold +# RAM_RSS_PREFILL_MAX_POSTS=24 # RAM tag-RSS prefill upper bound + +# Customization +CUSTOM_CSS="" # Optional: Path to custom CSS file relative to output root (e.g., "/css/custom.css"). File should be placed in STATIC_DIR. # Site information -SITE_TITLE="My Journal" -SITE_DESCRIPTION="A personal journal and introspective newspaper" -SITE_URL="http://localhost" +SITE_TITLE="My new BSSG site" +SITE_DESCRIPTION="A complete SSG - written in bash" +SITE_URL="http://localhost:8000" AUTHOR_NAME="Anonymous" AUTHOR_EMAIL="anonymous@example.com" +REL_ME_URL="" # Optional fediverse profile URL for <link rel="me"> verification +# REL_ME_URLS=( +# "https://mastodon.example.com/@john" +# "https://another-fedi.example/@john" +# ) +FEDIVERSE_CREATOR="" # Optional default fediverse:creator value for posts # Content configuration DATE_FORMAT="%Y-%m-%d %H:%M:%S %z" -TIMEZONE="local" # Options: "local", "GMT", or a specific timezone - # Affects how dates are displayed in the generated site based on system interpretation. -SHOW_TIMEZONE="false" # Options: "true", "false". Determines if the timezone offset (e.g., +0200) is shown in displayed dates. +TIMEZONE="local" # Options: "local", "GMT", or a specific timezone like "America/New_York" +SHOW_TIMEZONE="false" # Options: "true", "false". Whether to display the timezone in rendered dates. POSTS_PER_PAGE=10 -ENABLE_ARCHIVES=true # Enable or disable archives by year/month -URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. -RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be included in the RSS feed description instead of the excerpt. Useful for readers that consume entire posts via RSS. -ENABLE_TAG_RSS=true # Options: "true", "false". If set to "true" (default), an additional RSS feed will be generated for each tag at `output/tags/<tag-slug>/rss.xml`. +RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". Include full post content in RSS feed. +RSS_FILENAME="rss.xml" # The filename for the main RSS feed (e.g., feed.xml, rss.xml) +INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". Show full post content on homepage instead of just description/excerpt. +ENABLE_ARCHIVES=true # Enable or disable archive pages +ENABLE_AUTHOR_PAGES=false # Enable or disable author pages (default: false) +ENABLE_AUTHOR_RSS=false # Enable or disable author-specific RSS feeds (default: false) +SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum authors to show menu (default: 2) +URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs. Available: Year, Month, Day, slug +ENABLE_TAG_RSS=true # Enable or disable tag-specific RSS feed generation (default: true) + +# Optional exact-match per-author fediverse overrides +# declare -A AUTHOR_FEDIVERSE_CREATORS=( +# ["Jane Smith"]="@jane@example.social" +# ) + +# Archive Page Configuration +ARCHIVES_LIST_ALL_POSTS="false" # Options: "true", "false". If true, list all posts on the main archive page. + +# Page configuration +PAGE_URL_FORMAT="slug" # Format for page URLs. Available: slug, filename (without ext) + +# Markdown processing configuration +MARKDOWN_PROCESSOR="commonmark" # Options: "pandoc", "commonmark", or "markdown.pl" + +# Language Configuration +SITE_LANG="en" # Default language code (e.g., en, es, fr). See locales/ directory. + +# Related Posts Configuration +ENABLE_RELATED_POSTS=true # Enable or disable related posts feature +RELATED_POSTS_COUNT=3 # Number of related posts to show (default: 3) + +# Server Configuration (for 'bssg.sh server' command) +# These are the defaults used by 'bssg.sh server' if not overridden by command-line options. +BSSG_SERVER_PORT_DEFAULT="8000" # Default port for the local development server +BSSG_SERVER_HOST_DEFAULT="localhost" # Default host for the local development server # Deployment configuration DEPLOY_AFTER_BUILD="false" # Options: "true", "false". Automatically deploy after a successful build. DEPLOY_SCRIPT="" # Path to the deployment script to execute if DEPLOY_AFTER_BUILD is true. + +# Terminal colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color ``` #### Date Format Examples @@ -750,6 +970,7 @@ BSSG includes a variety of themes to customize the look of your site. Themes are - `dark` - Dark mode theme - `flat` - Microsoft Metro/Modern UI inspired flat design - `glassmorphism` - Modern frosted glass effect with blue/teal gradient +- `liquid-glass` - Fluid translucent surfaces with refractive highlights and layered depth - `material` - Material Design inspired theme - `art-deco` - Inspired by 1920s-30s Art Deco style with geometric patterns, elegant fonts, and gold/black/silver/jewel color palettes - `bauhaus` - Inspired by the Bauhaus school, focusing on functionality, primary geometric shapes, primary colors plus black and white, and clean sans-serif typography @@ -774,9 +995,12 @@ BSSG includes a variety of themes to customize the look of your site. Themes are #### Operating System Themes - `beos` - BeOS inspired theme +- `freebsd` - FreeBSD-inspired theme with the iconic red/black visual language and orb motif - `macclassic` - Classic Mac OS inspired theme - `macos9` - Mac OS 9 inspired theme +- `netbsd` - NetBSD-inspired theme with a navy/orange flag aesthetic and engineering-focused layout - `nextstep` - NeXTSTEP inspired theme +- `openbsd` - OpenBSD-inspired yellow/black Puffy-style theme with bold security-flavored styling - `osx` - macOS inspired theme - `win311` - Windows 3.11 inspired theme - `win95` - Windows 95 inspired theme @@ -794,12 +1018,17 @@ BSSG includes a variety of themes to customize the look of your site. Themes are - `docs` - A clean, structured theme ideal for technical documentation with excellent code formatting and clear navigation - `longform` - Optimized for reading long articles with highly readable typography, contained text width, and minimal distractions - `reader-mode` - Simulates browser reader mode with almost total emphasis on text, sepia background, very readable serif font, and minimal graphic elements +- `mynotes` - A warm, intimate, text-first journal theme designed for meditative long-form reading +- `museum-label` - Museum catalog style with refined serif typography, restrained metadata, and clean archival cards +- `field-journal` - Warm paper-inspired writing theme with natural tones and notebook-style presentation +- `thoughtful` - A warm, accessible, and performant theme for personal reflection blogs and thoughtful writing - `text-only` - A step beyond minimalism using browser defaults with clean base typography for readability and lightning-fast loading #### Special Themes - `brutalist` - Raw, minimalist concrete-inspired design - `newspaper` - Classic newspaper layout - `diary` - Personal diary/journal style +- `microfiche` - Monochrome archival projection aesthetic with scanline and microfilm-inspired styling - `random` - Selects a random theme (from the available themes) for each build To use a theme, specify it in your config file: @@ -832,11 +1061,22 @@ You can also specify a custom SITE_URL for the previews: ./generate_theme_previews.sh --site-url "https://example.com/blog" ``` -The script will use the SITE_URL from the following sources in order of precedence: -1. Command line argument (--site-url) -2. Local config file (config.sh.local) -3. Main config file (config.sh) -4. Default value (http://localhost) +You can also point the preview generator at a site-specific config file, just like `bssg.sh build`: + +```bash +./generate_theme_previews.sh --config /path/to/site/config.sh.local +``` + +BSSG configuration is resolved in this order: +1. Command line argument (`--config`) +2. `BSSG_LCONF` environment variable +3. Local config file (`config.sh.local`) +4. Main config file (`config.sh`) + +The preview `SITE_URL` is then chosen from: +1. Command line argument (`--site-url`) +2. The selected BSSG configuration +3. Default value (`http://localhost`) Each theme preview will be accessible at `SITE_URL/theme` (e.g., `https://example.com/blog/dark`). @@ -993,13 +1233,25 @@ The system maintains a cache of extracted metadata from markdown files to reduce - File index information is stored in `.bssg_cache/file_index.txt` - Tags index information is stored in `.bssg_cache/tags_index.txt` +### RAM Build Mode + +BSSG supports a RAM-first build mode for faster full rebuilds and lower disk churn: + +- Set `BUILD_MODE="ram"` in `config.sh.local`, or run `./bssg.sh build --build-mode ram` +- Source/posts/pages/templates/locales are preloaded in memory +- Build indexes (file/tags/authors/archive, plus page lists) are kept in memory +- RAM mode intentionally skips cache persistence and always behaves like an in-memory full rebuild +- A stage timing summary is printed at the end of RAM-mode builds +- On low-end disk-bound hosts, RAM mode can significantly reduce build time by avoiding repeated disk reads + ### Parallel Processing -If GNU parallel is installed on your system, BSSG can process multiple files simultaneously: +BSSG uses multiple execution strategies to process files in parallel: - Automatically detects GNU parallel and enables it for builds with many files -- Uses 80% of available CPU cores for optimal performance -- Falls back to sequential processing if parallel is not available +- Falls back to internal shell workers when GNU parallel is unavailable or unsuitable for a stage +- Auto-detects CPU core count for worker sizing +- In RAM mode, worker count is capped by `RAM_MODE_MAX_JOBS` (default: `6`) to reduce memory pressure To take advantage of parallel processing, install GNU parallel: @@ -1014,6 +1266,10 @@ brew install parallel pkg install parallel ``` +### Real-World Result + +On a single-core OpenBSD server with spinning disks, the maintainer observed build time dropping to about one third of the previous release when building with `BUILD_MODE="ram"`. + ## Site Configuration Key configuration options: @@ -1031,11 +1287,34 @@ DATE_FORMAT="%Y-%m-%d %H:%M:%S %z" TIMEZONE="local" # Options: "local", "GMT", or a specific timezone SHOW_TIMEZONE="false" # Options: "true", "false". Determines if the timezone offset (e.g., +0200) is shown in displayed dates. POSTS_PER_PAGE=10 +BUILD_MODE="normal" # "normal" (incremental cache-backed) or "ram" (memory-first) ENABLE_ARCHIVES=true # Enable or disable archives by year/month URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be included in the RSS feed description instead of the excerpt. Useful for readers that consume entire posts via RSS. +INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be displayed on the homepage and paginated index pages instead of just the description/excerpt. ENABLE_TAG_RSS=true # Options: "true", "false". If set to "true" (default), an additional RSS feed will be generated for each tag at `output/tags/<tag-slug>/rss.xml`. + +# Precompression options +PRECOMPRESS_ASSETS="false" # Generate .gz siblings for changed text assets +# PRECOMPRESS_GZIP_LEVEL=9 +# PRECOMPRESS_MAX_JOBS=0 +# PRECOMPRESS_VERBOSE=false + +# RAM-mode tuning (optional) +# RAM_MODE_MAX_JOBS=6 +# RAM_MODE_VERBOSE=false +# RAM_RSS_PREFILL_MIN_HITS=2 +# RAM_RSS_PREFILL_MAX_POSTS=24 + +# Related Posts configuration +ENABLE_RELATED_POSTS=true # Options: "true", "false". If set to "true" (default), related posts based on shared tags will be shown at the end of each post. +RELATED_POSTS_COUNT=3 # Number of related posts to display (default: 3, recommended maximum: 5). + +# Multi-author configuration +ENABLE_AUTHOR_PAGES=false # Options: "true", "false". If set to "true", author index pages will be generated. +ENABLE_AUTHOR_RSS=false # Options: "true", "false". If set to "true", RSS feeds will be generated for each author. +SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum number of authors required to show the "Authors" menu item. ``` The `URL_SLUG_FORMAT` setting determines how your post URLs are structured. By default, it uses `Year/Month/Day/slug` which creates URLs like `http://yoursite.com/2023/01/15/my-post-title/`. @@ -1137,4 +1416,3 @@ This project is licensed under the BSD 3-Clause License - see the LICENSE file f - **Themes**: Explore the available themes in the `themes` directory. - **Backup & Restore**: Use `./bssg.sh backup` and `./bssg.sh restore` to manage content backups. - **Development Blog**: Stay up-to-date with the latest release notes, development progress, and announcements on the official BSSG Dev Blog: [https://blog.bssg.dragas.net](https://blog.bssg.dragas.net) - diff --git a/bssg-editor.html b/bssg-editor.html index ba5719c..cfb871b 100644 --- a/bssg-editor.html +++ b/bssg-editor.html @@ -48,6 +48,7 @@ color: var(--text-primary); line-height: 1.6; overflow-x: hidden; + -webkit-text-size-adjust: 100%; } /* Layout */ @@ -55,17 +56,20 @@ display: grid; grid-template-rows: auto 1fr auto; height: 100vh; + height: 100dvh; /* Dynamic viewport height for mobile */ } /* Header */ .header { background: var(--surface); border-bottom: 1px solid var(--border); - padding: 1rem 2rem; + padding: 1rem 1.5rem; display: flex; justify-content: space-between; align-items: center; box-shadow: var(--shadow); + position: relative; + z-index: 100; } .header h1 { @@ -76,8 +80,20 @@ .header-actions { display: flex; - gap: 0.75rem; + gap: 0.5rem; align-items: center; + flex-wrap: wrap; + } + + /* Mobile header toggle */ + .mobile-menu-toggle { + display: none; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--text-primary); + padding: 0.25rem; } /* Buttons */ @@ -93,6 +109,10 @@ align-items: center; gap: 0.5rem; text-decoration: none; + white-space: nowrap; + min-height: 44px; /* Better touch target */ + min-width: 44px; + justify-content: center; } .btn-primary { @@ -138,6 +158,7 @@ border-right: 1px solid var(--border); padding: 1.5rem; overflow-y: auto; + transition: transform 0.3s ease; } .sidebar h2 { @@ -169,6 +190,7 @@ color: var(--text-primary); font-size: 0.875rem; transition: border-color 0.2s; + min-height: 44px; /* Better touch target */ } .form-input:focus { @@ -198,22 +220,32 @@ .editor-toolbar { background: var(--surface); border-bottom: 1px solid var(--border); - padding: 0.75rem 1.5rem; + padding: 0.75rem 1rem; display: flex; - gap: 0.5rem; + gap: 0.25rem; align-items: center; + overflow-x: auto; + scrollbar-width: none; + -ms-overflow-style: none; + position: sticky; + top: 0; + z-index: 10; } - .editor-content { - display: grid; - grid-template-columns: 1fr; - height: 100%; - overflow: hidden; - } + .editor-toolbar::-webkit-scrollbar { + display: none; + } - .editor-content.split-view { - grid-template-columns: 1fr 1fr; - } + .editor-content { + display: grid; + grid-template-columns: 1fr; + height: 100%; + overflow: hidden; + } + + .editor-content.split-view { + grid-template-columns: 1fr 1fr; + } .editor-pane { display: flex; @@ -228,6 +260,27 @@ font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); + display: flex; + justify-content: space-between; + align-items: center; + } + + .settings-toggle { + background: var(--primary-color); + color: white; + border: none; + border-radius: 0.375rem; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + cursor: pointer; + display: none; /* Hidden by default on desktop */ + align-items: center; + gap: 0.5rem; + transition: all 0.2s; + } + + .settings-toggle:hover { + background: var(--primary-hover); } .editor-textarea { @@ -243,19 +296,24 @@ outline: none; } - .editor-pane.preview-pane { - flex: 1; - padding: 1.5rem; - overflow-y: auto; - background: var(--background); - border-left: 1px solid var(--border); - display: none; - } + .editor-pane.preview-pane { + flex: 1; + padding: 0; + overflow-y: auto; + background: var(--background); + border-left: 1px solid var(--border); + display: none; + } - .split-view .editor-pane.preview-pane { - display: flex; - flex-direction: column; - } + .split-view .editor-pane.preview-pane { + display: flex; + flex-direction: column; + } + + .preview-content { + padding: 1.5rem; + flex: 1; + } /* Preview content styling */ .preview-content h1, @@ -324,19 +382,21 @@ border-top: 1px solid var(--border); padding: 0.5rem 1.5rem; display: flex; - justify-content: between; + justify-content: space-between; align-items: center; font-size: 0.75rem; color: var(--text-muted); + gap: 1rem; } .status-left { display: flex; gap: 1rem; + flex-wrap: wrap; } .status-right { - margin-left: auto; + white-space: nowrap; } /* Modal styles */ @@ -351,6 +411,7 @@ align-items: center; justify-content: center; z-index: 1000; + padding: 1rem; } .modal.active { @@ -361,8 +422,9 @@ background: var(--background); border-radius: 0.5rem; box-shadow: var(--shadow-lg); - max-width: 90vw; - max-height: 90vh; + max-width: calc(100vw - 2rem); + max-height: calc(100vh - 2rem); + width: 100%; overflow: hidden; display: flex; flex-direction: column; @@ -384,6 +446,7 @@ .modal-body { padding: 1.5rem; overflow-y: auto; + flex: 1; } .modal-footer { @@ -392,6 +455,7 @@ display: flex; gap: 0.75rem; justify-content: flex-end; + flex-wrap: wrap; } /* Unsplash image grid */ @@ -431,37 +495,121 @@ font-size: 0.75rem; } - /* Responsive design */ - @media (max-width: 768px) { - .main-content { - grid-template-columns: 1fr; - grid-template-rows: auto 1fr; - } + /* Tag input styling */ + .tag-input-container { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + padding: 0.5rem; + border: 1px solid var(--border); + border-radius: 0.375rem; + background: var(--background); + min-height: 44px; + align-items: center; + } - .sidebar { - border-right: none; - border-bottom: 1px solid var(--border); - max-height: 40vh; - } + .tag-input-container:focus-within { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1); + } - .editor-content.split-view { - grid-template-columns: 1fr; - grid-template-rows: 1fr auto; - } + .tag { + background: var(--primary-color); + color: white; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + display: flex; + align-items: center; + gap: 0.25rem; + } - .split-view .editor-pane.preview-pane { - border-left: none; - border-top: 1px solid var(--border); - max-height: 40vh; - } + .tag-remove { + background: none; + border: none; + color: white; + cursor: pointer; + padding: 0; + width: 1rem; + height: 1rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + } - .header { - padding: 1rem; - } + .tag-remove:hover { + background: rgba(255, 255, 255, 0.2); + } - .header h1 { - font-size: 1.25rem; - } + .tag-input { + border: none; + outline: none; + background: transparent; + flex: 1; + min-width: 100px; + color: var(--text-primary); + } + + /* Article list styles */ + .article-item { + padding: 1rem; + border: 1px solid var(--border); + border-radius: 0.5rem; + margin-bottom: 0.75rem; + cursor: pointer; + transition: all 0.2s; + background: var(--background); + min-height: 44px; + } + + .article-item:hover { + background: var(--surface-hover); + border-color: var(--primary-color); + } + + .article-item.selected { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); + } + + .article-title { + font-weight: 600; + font-size: 1rem; + margin-bottom: 0.25rem; + } + + .article-meta { + font-size: 0.875rem; + color: var(--text-muted); + margin-bottom: 0.5rem; + } + + .article-item.selected .article-meta { + color: rgba(255, 255, 255, 0.8); + } + + .article-description { + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.4; + } + + .article-item.selected .article-description { + color: rgba(255, 255, 255, 0.9); + } + + .no-articles { + text-align: center; + padding: 2rem; + color: var(--text-muted); + font-style: italic; + } + + /* Preview mobile toggle */ + .preview-mobile-toggle { + display: none; } /* Focus mode */ @@ -562,116 +710,321 @@ 100% { transform: rotate(360deg); } } - /* Tag input styling */ - .tag-input-container { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; - padding: 0.5rem; - border: 1px solid var(--border); - border-radius: 0.375rem; - background: var(--background); - min-height: 2.5rem; - align-items: center; + /* Mobile and tablet responsive design */ + @media (max-width: 1024px) { + .header-actions { + gap: 0.25rem; + } + + .btn { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; + } } - .tag-input-container:focus-within { - border-color: var(--primary-color); - box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1); + @media (max-width: 768px) { + .header { + padding: 0.75rem 1rem; + flex-wrap: wrap; + gap: 0.5rem; + } + + .header h1 { + font-size: 1.25rem; + flex: 1; + } + + .mobile-menu-toggle { + display: block; + } + + .header-actions { + display: none; + width: 100%; + background: var(--surface); + padding: 0.75rem; + border-top: 1px solid var(--border); + margin: 0 -1rem -0.75rem; + gap: 0.5rem; + flex-wrap: wrap; + } + + .header-actions.active { + display: flex; + } + + .btn { + padding: 0.6rem 0.8rem; + font-size: 0.8rem; + flex: 1; + min-width: auto; + } + + .main-content { + grid-template-columns: 1fr; + grid-template-rows: 1fr; + position: relative; + } + + .sidebar { + border-right: none; + border-bottom: 1px solid var(--border); + max-height: 80vh; + transform: translateX(-100%); + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 320px; + max-width: 85vw; + z-index: 200; + box-shadow: var(--shadow-lg); + transition: transform 0.3s ease; + } + + .sidebar.mobile-open { + transform: translateX(0); + } + + .sidebar::before { + content: ''; + position: fixed; + top: 0; + left: 100%; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + z-index: -1; + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + } + + .sidebar.mobile-open::before { + opacity: 1; + pointer-events: all; + } + + #sidebarClose { + display: block !important; + } + + .settings-toggle { + display: flex !important; + background: var(--primary-color); + color: white; + font-size: 0.8rem; + padding: 0.4rem 0.8rem; + } + + .editor-content { + height: calc(100vh - 120px); + height: calc(100dvh - 120px); + } + + .editor-content.split-view { + grid-template-columns: 1fr; + grid-template-rows: 1fr auto; + } + + .split-view .editor-pane.preview-pane { + display: flex; + border-left: none; + border-top: 1px solid var(--border); + height: 50vh; + max-height: none; + } + + .editor-toolbar { + padding: 0.4rem 0.5rem; + gap: 0.3rem; + position: sticky; + top: 0; + z-index: 15; + background: var(--surface); + border-bottom: 1px solid var(--border); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: visible; + flex-wrap: wrap; + justify-content: space-between; + } + + .toolbar-group { + display: flex; + gap: 0.2rem; + align-items: center; + flex-wrap: nowrap; + pointer-events: auto; + } + + .toolbar-group .btn { + pointer-events: auto; + cursor: pointer; + position: relative; + z-index: 1; + } + + .editor-toolbar .btn { + padding: 0.4rem; + min-width: 34px; + font-size: 0.75rem; + flex-shrink: 0; + height: 34px; + border-radius: 0.25rem; + pointer-events: auto; + cursor: pointer; + } + + .editor-toolbar > div { + display: flex; + gap: 0.2rem; + align-items: center; + flex-wrap: nowrap; + } + + .editor-textarea { + padding: 1rem; + font-size: 0.9rem; + padding-top: 0.5rem; /* Reduced since toolbar is sticky */ + } + + .preview-mobile-toggle { + display: block; + color: var(--text-secondary); + cursor: pointer; + padding: 0.25rem; + } + + .status-bar { + padding: 0.5rem 1rem; + font-size: 0.7rem; + flex-wrap: wrap; + position: sticky; + bottom: 0; + background: var(--surface); + border-top: 1px solid var(--border); + z-index: 10; + } + + .status-left { + gap: 0.5rem; + } + + /* Modal adjustments */ + .modal { + padding: 0.5rem; + } + + .modal-content { + max-width: 100vw; + max-height: 100vh; + border-radius: 0.25rem; + } + + .modal-header, + .modal-body, + .modal-footer { + padding: 1rem; + } + + .image-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 0.75rem; + } + + .form-input { + font-size: 16px; /* Prevents zoom on iOS */ + } + + /* Focus mode adjustments */ + .focus-mode .editor-textarea { + padding: 1.5rem 1rem; + font-size: 0.95rem; + } + + .focus-mode .editor-toolbar { + display: none; + } + + .focus-mode .status-bar { + display: none; + } + + /* Improved hint for first-time users */ + .editor-pane-header::after { + content: '👈 Tap for post metadata'; + position: absolute; + right: 10px; + top: -30px; + background: var(--primary-color); + color: white; + padding: 0.4rem 0.6rem; + border-radius: 0.375rem; + font-size: 0.7rem; + opacity: 0; + animation: fadeInOut 4s ease-in-out; + pointer-events: none; + white-space: nowrap; + box-shadow: var(--shadow); + } + + @keyframes fadeInOut { + 0%, 100% { opacity: 0; transform: translateY(10px); } + 25%, 75% { opacity: 1; transform: translateY(0); } + } } - .tag { - background: var(--primary-color); - color: white; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - font-size: 0.75rem; - display: flex; - align-items: center; - gap: 0.25rem; + @media (max-width: 480px) { + .header { + padding: 0.5rem 0.75rem; + } + + .sidebar { + padding: 1rem; + } + + .editor-textarea { + padding: 0.75rem; + } + + .preview-content { + padding: 1rem; + } + + .status-bar { + padding: 0.4rem 0.75rem; + } + + .btn { + padding: 0.5rem 0.6rem; + font-size: 0.75rem; + } + + .image-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + } } - .tag-remove { - background: none; - border: none; - color: white; - cursor: pointer; - padding: 0; - width: 1rem; - height: 1rem; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; + /* Landscape phone adjustments */ + @media (max-width: 768px) and (orientation: landscape) { + .sidebar.mobile-open { + max-height: 60vh; + overflow-y: auto; + } + + .split-view .editor-pane.preview-pane { + height: 40vh; + } } - .tag-remove:hover { - background: rgba(255, 255, 255, 0.2); + /* High DPI displays */ + @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + .editor-textarea, + .preview-content { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } } - - .tag-input { - border: none; - outline: none; - background: transparent; - flex: 1; - min-width: 100px; - color: var(--text-primary); - } - - /* Article list styles */ - .article-item { - padding: 1rem; - border: 1px solid var(--border); - border-radius: 0.5rem; - margin-bottom: 0.75rem; - cursor: pointer; - transition: all 0.2s; - background: var(--background); - } - - .article-item:hover { - background: var(--surface-hover); - border-color: var(--primary-color); - } - - .article-item.selected { - background: var(--primary-color); - color: white; - border-color: var(--primary-color); - } - - .article-title { - font-weight: 600; - font-size: 1rem; - margin-bottom: 0.25rem; - } - - .article-meta { - font-size: 0.875rem; - color: var(--text-muted); - margin-bottom: 0.5rem; - } - - .article-item.selected .article-meta { - color: rgba(255, 255, 255, 0.8); - } - - .article-description { - font-size: 0.875rem; - color: var(--text-secondary); - line-height: 1.4; - } - - .article-item.selected .article-description { - color: rgba(255, 255, 255, 0.9); - } - - .no-articles { - text-align: center; - padding: 2rem; - color: var(--text-muted); - font-style: italic; - } </style> </head> <body> @@ -679,36 +1032,44 @@ <!-- Header --> <header class="header"> <h1>BSSG Post Editor</h1> - <div class="header-actions"> + <button class="mobile-menu-toggle" id="mobileMenuToggle" aria-label="Toggle menu"> + ☰ + </button> + <div class="header-actions" id="headerActions"> <button class="btn btn-ghost" id="themeToggle" title="Toggle theme"> 🌙 </button> - <button class="btn btn-secondary" id="newBtn" title="New Article (Ctrl+N)"> - 📄 New - </button> - <button class="btn btn-secondary" id="saveBtn" title="Save Article (Ctrl+S)"> - 💾 Save - </button> - <button class="btn btn-secondary" id="loadBtn" title="Load Article (Ctrl+O)"> - 📂 Load - </button> - <button class="btn btn-secondary" id="importBtn"> - 📁 Import - </button> - <button class="btn btn-secondary" id="copyBtn"> - 📋 Copy - </button> - <button class="btn btn-primary" id="exportBtn"> - 💾 Export - </button> + <button class="btn btn-secondary" id="newBtn" title="New Article (Ctrl+N)"> + 📄 New + </button> + <button class="btn btn-secondary" id="saveBtn" title="Save Article (Ctrl+S)"> + 💾 Save + </button> + <button class="btn btn-secondary" id="loadBtn" title="Load Article (Ctrl+O)"> + 📂 Load + </button> + <button class="btn btn-secondary" id="importBtn"> + 📁 Import + </button> + <button class="btn btn-secondary" id="copyBtn"> + 📋 Copy + </button> + <button class="btn btn-primary" id="exportBtn"> + 💾 Export + </button> </div> </header> <!-- Main content --> <main class="main-content"> <!-- Sidebar with frontmatter --> - <aside class="sidebar"> - <h2>Post Settings</h2> + <aside class="sidebar" id="sidebar"> + <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> + <h2 style="margin: 0;">Post Settings</h2> + <button class="btn btn-ghost" id="sidebarClose" title="Close Settings" style="display: none; padding: 0.5rem;"> + ✕ + </button> + </div> <div class="form-group"> <label class="form-label" for="title">Title *</label> @@ -757,90 +1118,114 @@ <div class="form-help">URL or path to featured image</div> </div> - <div class="form-group"> - <label class="form-label" for="imageCaption">Image Caption</label> - <input type="text" id="imageCaption" class="form-input" placeholder="Optional caption"> - <div class="form-help">Caption for the featured image</div> - </div> + <div class="form-group"> + <label class="form-label" for="imageCaption">Image Caption</label> + <input type="text" id="imageCaption" class="form-input" placeholder="Optional caption"> + <div class="form-help">Caption for the featured image</div> + </div> - <hr style="margin: 1.5rem 0; border: none; border-top: 1px solid var(--border);"> - - <h3 style="font-size: 1rem; margin-bottom: 1rem; color: var(--text-secondary);">Settings</h3> - - <div class="form-group"> - <label class="form-label" for="unsplashKey">Unsplash Access Key</label> - <input type="password" id="unsplashKey" class="form-input" placeholder="Optional - for real Unsplash images"> - <div class="form-help">Get your free API key from <a href="https://unsplash.com/developers" target="_blank">Unsplash Developers</a></div> - </div> + <div class="form-group"> + <label class="form-label" for="authorName">Author Name</label> + <input type="text" id="authorName" class="form-input" placeholder="Leave empty for default"> + <div class="form-help">Author name for this post (optional)</div> + </div> + + <div class="form-group"> + <label class="form-label" for="authorEmail">Author Email</label> + <input type="email" id="authorEmail" class="form-input" placeholder="Leave empty for default"> + <div class="form-help">Author email for this post (optional)</div> + </div> + + <div class="form-group"> + <label class="form-label" for="fediverseCreator">Fediverse Creator</label> + <input type="text" id="fediverseCreator" class="form-input" placeholder="@you@example.social"> + <div class="form-help">Optional override for the fediverse:creator meta tag</div> + </div> + + <hr style="margin: 1.5rem 0; border: none; border-top: 1px solid var(--border);"> + + <h3 style="font-size: 1rem; margin-bottom: 1rem; color: var(--text-secondary);">Settings</h3> + + <div class="form-group"> + <label class="form-label" for="unsplashKey">Unsplash Access Key</label> + <input type="password" id="unsplashKey" class="form-input" placeholder="Optional - for real Unsplash images"> + <div class="form-help">Get your free API key from <a href="https://unsplash.com/developers" target="_blank">Unsplash Developers</a></div> + </div> </aside> <!-- Editor area --> <section class="editor-area"> - <!-- Toolbar --> - <div class="editor-toolbar"> - <button class="btn btn-ghost" id="boldBtn" title="Bold (Ctrl+B)"> - <strong>B</strong> - </button> - <button class="btn btn-ghost" id="italicBtn" title="Italic (Ctrl+I)"> - <em>I</em> - </button> - <button class="btn btn-ghost" id="linkBtn" title="Link (Ctrl+K)"> - 🔗 - </button> - <button class="btn btn-ghost" id="imageBtn" title="Image"> - 🖼️ - </button> - <button class="btn btn-ghost" id="codeBtn" title="Inline Code (Ctrl+`)"> - </> - </button> - <button class="btn btn-ghost" id="codeBlockBtn" title="Code Block"> - 📄 - </button> - <button class="btn btn-ghost" id="quoteBtn" title="Quote"> - 💬 - </button> - <button class="btn btn-ghost" id="listBtn" title="Unordered List"> - • - </button> - <button class="btn btn-ghost" id="orderedListBtn" title="Ordered List"> - 1. - </button> - <button class="btn btn-ghost" id="h1Btn" title="Heading 1"> - H1 - </button> - <button class="btn btn-ghost" id="h2Btn" title="Heading 2"> - H2 - </button> - <button class="btn btn-ghost" id="h3Btn" title="Heading 3"> - H3 - </button> - <button class="btn btn-ghost" id="hrBtn" title="Horizontal Rule"> - ― - </button> - <div style="margin-left: auto;"> - <button class="btn btn-ghost" id="previewBtn" title="Toggle Preview (Ctrl+P)"> - 👁️ - </button> - <button class="btn btn-ghost" id="focusBtn" title="Focus mode"> - 🎯 - </button> - </div> - </div> + <!-- Toolbar --> + <div class="editor-toolbar"> + <button class="btn btn-ghost" id="boldBtn" title="Bold (Ctrl+B)"> + <strong>B</strong> + </button> + <button class="btn btn-ghost" id="italicBtn" title="Italic (Ctrl+I)"> + <em>I</em> + </button> + <button class="btn btn-ghost" id="linkBtn" title="Link (Ctrl+K)"> + 🔗 + </button> + <button class="btn btn-ghost" id="imageBtn" title="Image"> + 🖼️ + </button> + <button class="btn btn-ghost" id="codeBtn" title="Inline Code (Ctrl+`)"> + </> + </button> + <button class="btn btn-ghost" id="codeBlockBtn" title="Code Block"> + 📄 + </button> + <button class="btn btn-ghost" id="quoteBtn" title="Quote"> + 💬 + </button> + <button class="btn btn-ghost" id="listBtn" title="Unordered List"> + • + </button> + <button class="btn btn-ghost" id="orderedListBtn" title="Ordered List"> + 1. + </button> + <button class="btn btn-ghost" id="h1Btn" title="Heading 1"> + H1 + </button> + <button class="btn btn-ghost" id="h2Btn" title="Heading 2"> + H2 + </button> + <button class="btn btn-ghost" id="h3Btn" title="Heading 3"> + H3 + </button> + <button class="btn btn-ghost" id="hrBtn" title="Horizontal Rule"> + ― + </button> + <div style="margin-left: auto; display: flex; gap: 0.25rem;"> + <button class="btn btn-ghost" id="previewBtn" title="Toggle Preview (Ctrl+P)"> + 👁️ + </button> + <button class="btn btn-ghost" id="focusBtn" title="Focus mode"> + 🎯 + </button> + </div> + </div> <!-- Editor content --> - <div class="editor-content"> + <div class="editor-content" id="editorContent"> <div class="editor-pane"> - <div class="editor-pane-header">Markdown Editor</div> + <div class="editor-pane-header"> + Markdown Editor + <button class="settings-toggle" id="sidebarToggle" title="Post Frontmatter"> + 📝 Frontmatter + </button> + </div> <textarea id="markdownEditor" class="editor-textarea" placeholder="Start writing your post..."></textarea> </div> - <div class="editor-pane preview-pane"> - <div class="editor-pane-header">Live Preview</div> - <div style="flex: 1; padding: 1.5rem; overflow-y: auto;"> - <div id="previewContent" class="preview-content"> - <p style="color: var(--text-muted); font-style: italic;">Preview will appear here as you type...</p> - </div> - </div> - </div> + <div class="editor-pane preview-pane"> + <div class="editor-pane-header"> + Live Preview + <span class="preview-mobile-toggle" id="previewMobileToggle">✕</span> + </div> + <div class="preview-content" id="previewContent"> + <p style="color: var(--text-muted); font-style: italic;">Preview will appear here as you type...</p> + </div> + </div> </div> </section> </main> @@ -865,18 +1250,18 @@ <h3 class="modal-title">Choose Image from Unsplash</h3> <button class="btn btn-ghost" id="closeUnsplashModal">✕</button> </div> - <div class="modal-body"> - <div class="form-group"> - <input type="text" id="unsplashSearch" class="form-input" placeholder="Search for images..."> - </div> - <div id="unsplashStatus" style="margin-bottom: 1rem; padding: 0.5rem; border-radius: 0.25rem; font-size: 0.875rem; display: none;"> - </div> - <div id="unsplashResults" class="image-grid"> - <p style="color: var(--text-muted); text-align: center; grid-column: 1 / -1;"> - Enter a search term to find images - </p> - </div> - </div> + <div class="modal-body"> + <div class="form-group"> + <input type="text" id="unsplashSearch" class="form-input" placeholder="Search for images..."> + </div> + <div id="unsplashStatus" style="margin-bottom: 1rem; padding: 0.5rem; border-radius: 0.25rem; font-size: 0.875rem; display: none;"> + </div> + <div id="unsplashResults" class="image-grid"> + <p style="color: var(--text-muted); text-align: center; grid-column: 1 / -1;"> + Enter a search term to find images + </p> + </div> + </div> <div class="modal-footer"> <button class="btn btn-secondary" id="cancelUnsplash">Cancel</button> </div> @@ -947,18 +1332,23 @@ description: '', image: '', imageCaption: '', + authorName: '', + authorEmail: '', + fediverseCreator: '', content: '' }, - isDirty: false, - lastSaved: null, - theme: localStorage.getItem('bssg-editor-theme') || 'light', - focusMode: false, - previewMode: false, - currentArticleId: null, - selectedArticleId: null, - lastWordCount: 0, - autoSaveTimeout: null, - lastActivity: null + isDirty: false, + lastSaved: null, + theme: localStorage.getItem('bssg-editor-theme') || 'light', + focusMode: false, + previewMode: false, + currentArticleId: null, + selectedArticleId: null, + lastWordCount: 0, + autoSaveTimeout: null, + lastActivity: null, + isMobile: window.innerWidth <= 768, + sidebarOpen: false }; // DOM elements @@ -972,69 +1362,105 @@ description: document.getElementById('description'), image: document.getElementById('image'), imageCaption: document.getElementById('imageCaption'), + authorName: document.getElementById('authorName'), + authorEmail: document.getElementById('authorEmail'), + fediverseCreator: document.getElementById('fediverseCreator'), markdownEditor: document.getElementById('markdownEditor'), previewContent: document.getElementById('previewContent'), wordCount: document.getElementById('wordCount'), charCount: document.getElementById('charCount'), saveStatus: document.getElementById('saveStatus'), lastSaved: document.getElementById('lastSaved'), - themeToggle: document.getElementById('themeToggle'), - newBtn: document.getElementById('newBtn'), - saveBtn: document.getElementById('saveBtn'), - loadBtn: document.getElementById('loadBtn'), - importBtn: document.getElementById('importBtn'), - copyBtn: document.getElementById('copyBtn'), - exportBtn: document.getElementById('exportBtn'), + themeToggle: document.getElementById('themeToggle'), + newBtn: document.getElementById('newBtn'), + saveBtn: document.getElementById('saveBtn'), + loadBtn: document.getElementById('loadBtn'), + importBtn: document.getElementById('importBtn'), + copyBtn: document.getElementById('copyBtn'), + exportBtn: document.getElementById('exportBtn'), fileInput: document.getElementById('fileInput'), unsplashBtn: document.getElementById('unsplashBtn'), - unsplashModal: document.getElementById('unsplashModal'), - unsplashSearch: document.getElementById('unsplashSearch'), - unsplashResults: document.getElementById('unsplashResults'), - unsplashStatus: document.getElementById('unsplashStatus'), - closeUnsplashModal: document.getElementById('closeUnsplashModal'), - cancelUnsplash: document.getElementById('cancelUnsplash'), - boldBtn: document.getElementById('boldBtn'), - italicBtn: document.getElementById('italicBtn'), - linkBtn: document.getElementById('linkBtn'), - imageBtn: document.getElementById('imageBtn'), - codeBtn: document.getElementById('codeBtn'), - codeBlockBtn: document.getElementById('codeBlockBtn'), - quoteBtn: document.getElementById('quoteBtn'), - listBtn: document.getElementById('listBtn'), - orderedListBtn: document.getElementById('orderedListBtn'), - h1Btn: document.getElementById('h1Btn'), - h2Btn: document.getElementById('h2Btn'), - h3Btn: document.getElementById('h3Btn'), - hrBtn: document.getElementById('hrBtn'), - previewBtn: document.getElementById('previewBtn'), - focusBtn: document.getElementById('focusBtn'), - focusExit: document.getElementById('focusExit'), - unsplashKey: document.getElementById('unsplashKey'), - saveModal: document.getElementById('saveModal'), - saveTitle: document.getElementById('saveTitle'), - saveDescription: document.getElementById('saveDescription'), - closeSaveModal: document.getElementById('closeSaveModal'), - cancelSave: document.getElementById('cancelSave'), - confirmSave: document.getElementById('confirmSave'), - loadModal: document.getElementById('loadModal'), - searchArticles: document.getElementById('searchArticles'), - articlesList: document.getElementById('articlesList'), - closeLoadModal: document.getElementById('closeLoadModal'), - cancelLoad: document.getElementById('cancelLoad'), - deleteSelected: document.getElementById('deleteSelected') + unsplashModal: document.getElementById('unsplashModal'), + unsplashSearch: document.getElementById('unsplashSearch'), + unsplashResults: document.getElementById('unsplashResults'), + unsplashStatus: document.getElementById('unsplashStatus'), + closeUnsplashModal: document.getElementById('closeUnsplashModal'), + cancelUnsplash: document.getElementById('cancelUnsplash'), + boldBtn: document.getElementById('boldBtn'), + italicBtn: document.getElementById('italicBtn'), + linkBtn: document.getElementById('linkBtn'), + imageBtn: document.getElementById('imageBtn'), + codeBtn: document.getElementById('codeBtn'), + codeBlockBtn: document.getElementById('codeBlockBtn'), + quoteBtn: document.getElementById('quoteBtn'), + listBtn: document.getElementById('listBtn'), + orderedListBtn: document.getElementById('orderedListBtn'), + h1Btn: document.getElementById('h1Btn'), + h2Btn: document.getElementById('h2Btn'), + h3Btn: document.getElementById('h3Btn'), + hrBtn: document.getElementById('hrBtn'), + previewBtn: document.getElementById('previewBtn'), + focusBtn: document.getElementById('focusBtn'), + focusExit: document.getElementById('focusExit'), + unsplashKey: document.getElementById('unsplashKey'), + saveModal: document.getElementById('saveModal'), + saveTitle: document.getElementById('saveTitle'), + saveDescription: document.getElementById('saveDescription'), + closeSaveModal: document.getElementById('closeSaveModal'), + cancelSave: document.getElementById('cancelSave'), + confirmSave: document.getElementById('confirmSave'), + loadModal: document.getElementById('loadModal'), + searchArticles: document.getElementById('searchArticles'), + articlesList: document.getElementById('articlesList'), + closeLoadModal: document.getElementById('closeLoadModal'), + cancelLoad: document.getElementById('cancelLoad'), + deleteSelected: document.getElementById('deleteSelected'), + mobileMenuToggle: document.getElementById('mobileMenuToggle'), + headerActions: document.getElementById('headerActions'), + sidebar: document.getElementById('sidebar'), + sidebarToggle: document.getElementById('sidebarToggle'), + sidebarClose: document.getElementById('sidebarClose'), + editorContent: document.getElementById('editorContent'), + previewMobileToggle: document.getElementById('previewMobileToggle') }; - // Utility functions - function generateSlug(title) { - if (!title) return ''; - return title - .toLowerCase() - .trim() - .replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/--+/g, '-') // Replace multiple hyphens with single hyphen - .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens - } + // Utility functions + function generateSlug(title) { + if (!title) return ''; + + let slug = title.toLowerCase().trim(); + + // Transliterate common Unicode characters to ASCII equivalents + const transliterationMap = { + 'à': 'a', 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', + 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', + 'î': 'i', 'ï': 'i', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', + 'ö': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u', 'ý': 'y', + 'ÿ': 'y', 'ß': 'ss', 'đ': 'd', 'ł': 'l', 'ń': 'n', 'ś': 's', 'ź': 'z', + 'ż': 'z', 'č': 'c', 'ř': 'r', 'š': 's', 'ž': 'z', 'ą': 'a', 'ę': 'e', + 'ć': 'c', 'ł': 'l', 'ń': 'n', 'ó': 'o', 'ś': 's', 'ź': 'z', 'ż': 'z' + }; + + // Apply transliteration + for (const [unicode, ascii] of Object.entries(transliterationMap)) { + slug = slug.replace(new RegExp(unicode, 'g'), ascii); + } + + // Remove all non-alphanumeric characters except spaces and hyphens + slug = slug.replace(/[^a-z0-9\s-]/g, ''); + + // Replace spaces with hyphens + slug = slug.replace(/\s+/g, '-'); + + // Replace multiple hyphens with single hyphen + slug = slug.replace(/--+/g, '-'); + + // Remove leading/trailing hyphens + slug = slug.replace(/^-+|-+$/g, ''); + + // If slug is empty, return 'untitled' + return slug || 'untitled'; + } function formatDate(date) { const d = new Date(date); @@ -1094,137 +1520,152 @@ frontmatter += `image_caption: ${post.imageCaption}\n`; } + if (post.authorName) { + frontmatter += `author_name: ${post.authorName}\n`; + } + + if (post.authorEmail) { + frontmatter += `author_email: ${post.authorEmail}\n`; + } + + if (post.fediverseCreator) { + frontmatter += `fediverse_creator: ${post.fediverseCreator}\n`; + } + frontmatter += '---\n\n'; return frontmatter; } function generateMarkdown() { - return generateFrontmatter() + (state.currentPost.content || ''); + const content = state.currentPost.content || ''; + // Ensure content ends with a newline for proper BSSG processing + const contentWithNewline = content.endsWith('\n') ? content : content + '\n'; + return generateFrontmatter() + contentWithNewline; } - // Enhanced markdown to HTML converter - function markdownToHtml(markdown) { - if (!markdown.trim()) return ''; - - let html = markdown; - - // Code blocks (must be processed before inline code) - html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>'); - - // Headers - html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>'); - html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>'); - html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>'); - - // Horizontal rules - html = html.replace(/^---$/gm, '<hr>'); - html = html.replace(/^\*\*\*$/gm, '<hr>'); - - // Bold (must be before italic) - html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); - html = html.replace(/__(.*?)__/g, '<strong>$1</strong>'); - - // Italic - html = html.replace(/\*(.*?)\*/g, '<em>$1</em>'); - html = html.replace(/_(.*?)_/g, '<em>$1</em>'); - - // Inline code - html = html.replace(/`(.*?)`/g, '<code>$1</code>'); - - // Images - html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">'); - - // Links - html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>'); - - // Lists - process line by line to maintain context - const listLines = html.split('\n'); - let inList = false; - let listType = ''; - - for (let i = 0; i < listLines.length; i++) { - const line = listLines[i]; - const isUnorderedItem = /^[\*\-] (.+)$/.test(line); - const isOrderedItem = /^(\d+)\. (.+)$/.test(line); - - if (isUnorderedItem || isOrderedItem) { - const content = line.replace(/^[\*\-\d\.] /, ''); - const currentListType = isOrderedItem ? 'ol' : 'ul'; - - if (!inList) { - listLines[i] = `<${currentListType}><li>${content}</li>`; - inList = true; - listType = currentListType; - } else if (listType === currentListType) { - listLines[i] = `<li>${content}</li>`; - } else { - listLines[i] = `</${listType}><${currentListType}><li>${content}</li>`; - listType = currentListType; - } - } else if (inList && line.trim() === '') { - // Empty line continues the list - continue; - } else if (inList) { - // Close the list - listLines[i-1] += `</${listType}>`; - inList = false; - listType = ''; - } - } - - // Close list if we end with one - if (inList) { - listLines[listLines.length - 1] += `</${listType}>`; - } - - html = listLines.join('\n'); - - // Blockquotes - html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>'); - - // Line breaks and paragraphs - html = html.replace(/\n\n/g, '</p><p>'); - html = html.replace(/\n/g, '<br>'); - - // Wrap in paragraphs (but not headers, lists, blockquotes, etc.) - const paragraphLines = html.split('<br>'); - const processedLines = paragraphLines.map(line => { - const trimmed = line.trim(); - if (!trimmed || - trimmed.startsWith('<h') || - trimmed.startsWith('<ul') || - trimmed.startsWith('<ol') || - trimmed.startsWith('<li') || - trimmed.startsWith('<blockquote') || - trimmed.startsWith('<pre') || - trimmed.startsWith('<hr') || - trimmed.includes('</p><p>')) { - return line; - } - return `<p>${line}</p>`; - }); - html = processedLines.join('<br>'); - - // Clean up - html = html.replace(/<p><\/p>/g, ''); - html = html.replace(/<p><br><\/p>/g, ''); - html = html.replace(/<br><p>/g, '<p>'); - html = html.replace(/<\/p><br>/g, '</p>'); - html = html.replace(/(<\/(?:ul|ol|blockquote|pre|h[1-6]|hr)>)<br>/g, '$1'); - html = html.replace(/<br>(<(?:ul|ol|blockquote|pre|h[1-6]|hr))/g, '$1'); - - return html; - } + // Enhanced markdown to HTML converter + function markdownToHtml(markdown) { + if (!markdown.trim()) return ''; + + let html = markdown; + + // Code blocks (must be processed before inline code) + html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>'); + + // Headers + html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>'); + html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>'); + html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>'); + + // Horizontal rules + html = html.replace(/^---$/gm, '<hr>'); + html = html.replace(/^\*\*\*$/gm, '<hr>'); + + // Bold (must be before italic) + html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); + html = html.replace(/__(.*?)__/g, '<strong>$1</strong>'); + + // Italic + html = html.replace(/\*(.*?)\*/g, '<em>$1</em>'); + html = html.replace(/_(.*?)_/g, '<em>$1</em>'); + + // Inline code + html = html.replace(/`(.*?)`/g, '<code>$1</code>'); + + // Images + html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">'); + + // Links + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>'); + + // Lists - process line by line to maintain context + const listLines = html.split('\n'); + let inList = false; + let listType = ''; + + for (let i = 0; i < listLines.length; i++) { + const line = listLines[i]; + const isUnorderedItem = /^[\*\-] (.+)$/.test(line); + const isOrderedItem = /^(\d+)\. (.+)$/.test(line); + + if (isUnorderedItem || isOrderedItem) { + const content = line.replace(/^[\*\-\d\.] /, ''); + const currentListType = isOrderedItem ? 'ol' : 'ul'; + + if (!inList) { + listLines[i] = `<${currentListType}><li>${content}</li>`; + inList = true; + listType = currentListType; + } else if (listType === currentListType) { + listLines[i] = `<li>${content}</li>`; + } else { + listLines[i] = `</${listType}><${currentListType}><li>${content}</li>`; + listType = currentListType; + } + } else if (inList && line.trim() === '') { + // Empty line continues the list + continue; + } else if (inList) { + // Close the list + listLines[i-1] += `</${listType}>`; + inList = false; + listType = ''; + } + } + + // Close list if we end with one + if (inList) { + listLines[listLines.length - 1] += `</${listType}>`; + } + + html = listLines.join('\n'); + + // Blockquotes + html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>'); + + // Line breaks and paragraphs + html = html.replace(/\n\n/g, '</p><p>'); + html = html.replace(/\n/g, '<br>'); + + // Wrap in paragraphs (but not headers, lists, blockquotes, etc.) + const paragraphLines = html.split('<br>'); + const processedLines = paragraphLines.map(line => { + const trimmed = line.trim(); + if (!trimmed || + trimmed.startsWith('<h') || + trimmed.startsWith('<ul') || + trimmed.startsWith('<ol') || + trimmed.startsWith('<li') || + trimmed.startsWith('<blockquote') || + trimmed.startsWith('<pre') || + trimmed.startsWith('<hr') || + trimmed.includes('</p><p>')) { + return line; + } + return `<p>${line}</p>`; + }); + html = processedLines.join('<br>'); + + // Clean up + html = html.replace(/<p><\/p>/g, ''); + html = html.replace(/<p><br><\/p>/g, ''); + html = html.replace(/<br><p>/g, '<p>'); + html = html.replace(/<\/p><br>/g, '</p>'); + html = html.replace(/(<\/(?:ul|ol|blockquote|pre|h[1-6]|hr)>)<br>/g, '$1'); + html = html.replace(/<br>(<(?:ul|ol|blockquote|pre|h[1-6]|hr))/g, '$1'); + + return html; + } - function updatePreview() { - // Only update preview if it's visible - if (state.previewMode) { - const content = elements.markdownEditor.value; - const html = markdownToHtml(content); - elements.previewContent.innerHTML = html || '<p style="color: var(--text-muted); font-style: italic;">Preview will appear here as you type...</p>'; - } - } + function updatePreview() { + // Only update preview if it's visible + if (state.previewMode) { + const content = elements.markdownEditor.value; + const html = markdownToHtml(content); + elements.previewContent.innerHTML = html || '<p style="color: var(--text-muted); font-style: italic;">Preview will appear here as you type...</p>'; + } + } function updateWordCount() { const content = elements.markdownEditor.value; @@ -1313,258 +1754,266 @@ } } - function saveToLocalStorage() { - try { - localStorage.setItem('bssg-editor-draft', JSON.stringify(state.currentPost)); - markClean(); - } catch (error) { - console.error('Failed to save to localStorage:', error); - elements.saveStatus.textContent = 'Save failed'; - elements.saveStatus.style.color = 'var(--error)'; - } - } + function saveToLocalStorage() { + try { + localStorage.setItem('bssg-editor-draft', JSON.stringify(state.currentPost)); + markClean(); + } catch (error) { + console.error('Failed to save to localStorage:', error); + elements.saveStatus.textContent = 'Save failed'; + elements.saveStatus.style.color = 'var(--error)'; + } + } - // Article management functions - function generateArticleId() { - return 'article_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); - } + // Article management functions + function generateArticleId() { + return 'article_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + } - function getStoredArticles() { - try { - const articles = localStorage.getItem('bssg-editor-articles'); - return articles ? JSON.parse(articles) : {}; - } catch (error) { - console.error('Failed to load articles:', error); - return {}; - } - } + function getStoredArticles() { + try { + const articles = localStorage.getItem('bssg-editor-articles'); + return articles ? JSON.parse(articles) : {}; + } catch (error) { + console.error('Failed to load articles:', error); + return {}; + } + } - function saveArticle(id, articleData) { - try { - const articles = getStoredArticles(); - articles[id] = { - ...articleData, - id: id, - lastModified: new Date().toISOString(), - created: articles[id]?.created || new Date().toISOString() - }; - localStorage.setItem('bssg-editor-articles', JSON.stringify(articles)); - return true; - } catch (error) { - console.error('Failed to save article:', error); - return false; - } - } + function saveArticle(id, articleData) { + try { + const articles = getStoredArticles(); + articles[id] = { + ...articleData, + id: id, + lastModified: new Date().toISOString(), + created: articles[id]?.created || new Date().toISOString() + }; + localStorage.setItem('bssg-editor-articles', JSON.stringify(articles)); + return true; + } catch (error) { + console.error('Failed to save article:', error); + return false; + } + } - function deleteArticle(id) { - try { - const articles = getStoredArticles(); - delete articles[id]; - localStorage.setItem('bssg-editor-articles', JSON.stringify(articles)); - return true; - } catch (error) { - console.error('Failed to delete article:', error); - return false; - } - } + function deleteArticle(id) { + try { + const articles = getStoredArticles(); + delete articles[id]; + localStorage.setItem('bssg-editor-articles', JSON.stringify(articles)); + return true; + } catch (error) { + console.error('Failed to delete article:', error); + return false; + } + } - function newArticle() { - if (state.isDirty) { - const currentTitle = state.currentPost.title || 'Untitled'; - const message = `You have unsaved changes to "${currentTitle}". Creating a new article will discard these changes.\n\nDo you want to continue?`; - if (!confirm(message)) { - return; - } - } + function newArticle() { + if (state.isDirty) { + const currentTitle = state.currentPost.title || 'Untitled'; + const message = `You have unsaved changes to "${currentTitle}". Creating a new article will discard these changes.\n\nDo you want to continue?`; + if (!confirm(message)) { + return; + } + } - // Reset to default state - state.currentPost = { - title: '', - slug: '', - date: new Date().toISOString().slice(0, 16), - lastmod: '', - tags: [], - description: '', - image: '', - imageCaption: '', - content: '' - }; - state.currentArticleId = null; - state.isDirty = false; - state.lastWordCount = 0; - - // Clear any pending auto-save - if (state.autoSaveTimeout) { - clearTimeout(state.autoSaveTimeout); - state.autoSaveTimeout = null; - } + // Reset to default state + state.currentPost = { + title: '', + slug: '', + date: new Date().toISOString().slice(0, 16), + lastmod: '', + tags: [], + description: '', + image: '', + imageCaption: '', + authorName: '', + authorEmail: '', + fediverseCreator: '', + content: '' + }; + state.currentArticleId = null; + state.isDirty = false; + state.lastWordCount = 0; + + // Clear any pending auto-save + if (state.autoSaveTimeout) { + clearTimeout(state.autoSaveTimeout); + state.autoSaveTimeout = null; + } - updateFormFromState(); - elements.markdownEditor.focus(); - - elements.saveStatus.textContent = 'New article'; - elements.saveStatus.style.color = 'var(--text-muted)'; - elements.lastSaved.textContent = 'Never saved'; - } + updateFormFromState(); + elements.markdownEditor.focus(); + + elements.saveStatus.textContent = 'New article'; + elements.saveStatus.style.color = 'var(--text-muted)'; + elements.lastSaved.textContent = 'Never saved'; - function openSaveModal() { - updateStateFromForm(); - elements.saveTitle.value = state.currentPost.title || ''; - elements.saveDescription.value = state.currentPost.description || ''; - elements.saveModal.classList.add('active'); - elements.saveTitle.focus(); - } + // Close mobile menu if open + if (state.isMobile) { + closeMobileMenu(); + } + } - function saveCurrentArticle() { - const title = elements.saveTitle.value.trim(); - if (!title) { - alert('Please enter a title for the article.'); - return; - } + function openSaveModal() { + updateStateFromForm(); + elements.saveTitle.value = state.currentPost.title || ''; + elements.saveDescription.value = state.currentPost.description || ''; + elements.saveModal.classList.add('active'); + elements.saveTitle.focus(); + } - updateStateFromForm(); - - const articleData = { - title: title, - description: elements.saveDescription.value.trim(), - post: { ...state.currentPost } - }; + function saveCurrentArticle() { + const title = elements.saveTitle.value.trim(); + if (!title) { + alert('Please enter a title for the article.'); + return; + } - const id = state.currentArticleId || generateArticleId(); - - if (saveArticle(id, articleData)) { - state.currentArticleId = id; - elements.saveModal.classList.remove('active'); - markClean(); - elements.saveStatus.textContent = 'Article saved'; - elements.saveStatus.style.color = 'var(--success)'; - elements.lastSaved.textContent = `Saved at ${new Date().toLocaleTimeString()}`; - } else { - alert('Failed to save article. Please try again.'); - } - } + updateStateFromForm(); + + const articleData = { + title: title, + description: elements.saveDescription.value.trim(), + post: { ...state.currentPost } + }; - function openLoadModal() { - elements.loadModal.classList.add('active'); - displayArticles(); - elements.searchArticles.focus(); - } + const id = state.currentArticleId || generateArticleId(); + + if (saveArticle(id, articleData)) { + state.currentArticleId = id; + elements.saveModal.classList.remove('active'); + markClean(); + elements.saveStatus.textContent = 'Article saved'; + elements.saveStatus.style.color = 'var(--success)'; + elements.lastSaved.textContent = `Saved at ${new Date().toLocaleTimeString()}`; + } else { + alert('Failed to save article. Please try again.'); + } + } - function displayArticles(searchTerm = '') { - const articles = getStoredArticles(); - const articleIds = Object.keys(articles); - - if (articleIds.length === 0) { - elements.articlesList.innerHTML = '<div class="no-articles">No saved articles found.</div>'; - return; - } + function openLoadModal() { + elements.loadModal.classList.add('active'); + displayArticles(); + elements.searchArticles.focus(); + } - const filteredArticles = articleIds - .map(id => articles[id]) - .filter(article => { - if (!searchTerm) return true; - const term = searchTerm.toLowerCase(); - return article.title.toLowerCase().includes(term) || - (article.description && article.description.toLowerCase().includes(term)); - }) - .sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + function displayArticles(searchTerm = '') { + const articles = getStoredArticles(); + const articleIds = Object.keys(articles); + + if (articleIds.length === 0) { + elements.articlesList.innerHTML = '<div class="no-articles">No saved articles found.</div>'; + return; + } - if (filteredArticles.length === 0) { - elements.articlesList.innerHTML = '<div class="no-articles">No articles match your search.</div>'; - return; - } + const filteredArticles = articleIds + .map(id => articles[id]) + .filter(article => { + if (!searchTerm) return true; + const term = searchTerm.toLowerCase(); + return article.title.toLowerCase().includes(term) || + (article.description && article.description.toLowerCase().includes(term)); + }) + .sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); - elements.articlesList.innerHTML = filteredArticles.map(article => { - const lastModified = new Date(article.lastModified).toLocaleDateString(); - const wordCount = article.post.content ? article.post.content.trim().split(/\s+/).length : 0; - - return ` - <div class="article-item" data-id="${article.id}"> - <div class="article-title">${article.title}</div> - <div class="article-meta"> - Last modified: ${lastModified} • ${wordCount} words - </div> - ${article.description ? `<div class="article-description">${article.description}</div>` : ''} - </div> - `; - }).join(''); + if (filteredArticles.length === 0) { + elements.articlesList.innerHTML = '<div class="no-articles">No articles match your search.</div>'; + return; + } - // Add click handlers - elements.articlesList.querySelectorAll('.article-item').forEach(item => { - item.addEventListener('click', () => { - // Remove previous selection - elements.articlesList.querySelectorAll('.article-item').forEach(i => i.classList.remove('selected')); - // Select current item - item.classList.add('selected'); - state.selectedArticleId = item.dataset.id; - elements.deleteSelected.disabled = false; - }); + elements.articlesList.innerHTML = filteredArticles.map(article => { + const lastModified = new Date(article.lastModified).toLocaleDateString(); + const wordCount = article.post.content ? article.post.content.trim().split(/\s+/).length : 0; + + return ` + <div class="article-item" data-id="${article.id}"> + <div class="article-title">${article.title}</div> + <div class="article-meta"> + Last modified: ${lastModified} • ${wordCount} words + </div> + ${article.description ? `<div class="article-description">${article.description}</div>` : ''} + </div> + `; + }).join(''); - item.addEventListener('dblclick', () => { - loadArticle(item.dataset.id); - }); - }); - } + // Add click handlers + elements.articlesList.querySelectorAll('.article-item').forEach(item => { + item.addEventListener('click', () => { + // Remove previous selection + elements.articlesList.querySelectorAll('.article-item').forEach(i => i.classList.remove('selected')); + // Select current item + item.classList.add('selected'); + state.selectedArticleId = item.dataset.id; + elements.deleteSelected.disabled = false; + }); - function loadArticle(id) { - const articles = getStoredArticles(); - const article = articles[id]; - - if (!article) { - alert('Article not found.'); - return; - } + item.addEventListener('dblclick', () => { + loadArticle(item.dataset.id); + }); + }); + } - if (state.isDirty) { - if (!confirm('You have unsaved changes. Are you sure you want to load this article?')) { - return; - } - } + function loadArticle(id) { + const articles = getStoredArticles(); + const article = articles[id]; + + if (!article) { + alert('Article not found.'); + return; + } - state.currentPost = { ...article.post }; - state.currentArticleId = id; - state.isDirty = false; - state.lastWordCount = article.post.content ? article.post.content.trim().split(/\s+/).length : 0; - - // Clear any pending auto-save - if (state.autoSaveTimeout) { - clearTimeout(state.autoSaveTimeout); - state.autoSaveTimeout = null; - } + if (state.isDirty) { + if (!confirm('You have unsaved changes. Are you sure you want to load this article?')) { + return; + } + } - updateFormFromState(); - elements.loadModal.classList.remove('active'); - - elements.saveStatus.textContent = 'Article loaded'; - elements.saveStatus.style.color = 'var(--success)'; - elements.lastSaved.textContent = `Loaded: ${article.title}`; - } + state.currentPost = { ...article.post }; + state.currentArticleId = id; + state.isDirty = false; + state.lastWordCount = article.post.content ? article.post.content.trim().split(/\s+/).length : 0; + + // Clear any pending auto-save + if (state.autoSaveTimeout) { + clearTimeout(state.autoSaveTimeout); + state.autoSaveTimeout = null; + } - function deleteSelectedArticle() { - if (!state.selectedArticleId) return; + updateFormFromState(); + elements.loadModal.classList.remove('active'); + + elements.saveStatus.textContent = 'Article loaded'; + elements.saveStatus.style.color = 'var(--success)'; + elements.lastSaved.textContent = `Loaded: ${article.title}`; + } - const articles = getStoredArticles(); - const article = articles[state.selectedArticleId]; - - if (!article) return; + function deleteSelectedArticle() { + if (!state.selectedArticleId) return; - if (confirm(`Are you sure you want to delete "${article.title}"? This action cannot be undone.`)) { - if (deleteArticle(state.selectedArticleId)) { - // If we're deleting the currently loaded article, reset - if (state.currentArticleId === state.selectedArticleId) { - state.currentArticleId = null; - elements.saveStatus.textContent = 'Article deleted'; - elements.saveStatus.style.color = 'var(--warning)'; - } - - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - displayArticles(elements.searchArticles.value); - } else { - alert('Failed to delete article. Please try again.'); - } - } - } + const articles = getStoredArticles(); + const article = articles[state.selectedArticleId]; + + if (!article) return; + + if (confirm(`Are you sure you want to delete "${article.title}"? This action cannot be undone.`)) { + if (deleteArticle(state.selectedArticleId)) { + // If we're deleting the currently loaded article, reset + if (state.currentArticleId === state.selectedArticleId) { + state.currentArticleId = null; + elements.saveStatus.textContent = 'Article deleted'; + elements.saveStatus.style.color = 'var(--warning)'; + } + + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + displayArticles(elements.searchArticles.value); + } else { + alert('Failed to delete article. Please try again.'); + } + } + } function loadFromLocalStorage() { try { @@ -1587,6 +2036,9 @@ state.currentPost.description = elements.description.value; state.currentPost.image = elements.image.value; state.currentPost.imageCaption = elements.imageCaption.value; + state.currentPost.authorName = elements.authorName.value; + state.currentPost.authorEmail = elements.authorEmail.value; + state.currentPost.fediverseCreator = elements.fediverseCreator.value; state.currentPost.content = elements.markdownEditor.value; } @@ -1598,14 +2050,17 @@ elements.description.value = state.currentPost.description || ''; elements.image.value = state.currentPost.image || ''; elements.imageCaption.value = state.currentPost.imageCaption || ''; + elements.authorName.value = state.currentPost.authorName || ''; + elements.authorEmail.value = state.currentPost.authorEmail || ''; + elements.fediverseCreator.value = state.currentPost.fediverseCreator || ''; elements.markdownEditor.value = state.currentPost.content || ''; - // Update tags - updateTagDisplay(); - - // Update preview and word count - updatePreview(); - updateWordCount(); + // Update tags + updateTagDisplay(); + + // Update preview and word count + updatePreview(); + updateWordCount(); } function updateTagDisplay() { @@ -1643,38 +2098,103 @@ } } - function toggleTheme() { - state.theme = state.theme === 'light' ? 'dark' : 'light'; - document.documentElement.setAttribute('data-theme', state.theme); - localStorage.setItem('bssg-editor-theme', state.theme); - elements.themeToggle.textContent = state.theme === 'light' ? '🌙' : '☀️'; - } + function toggleTheme() { + state.theme = state.theme === 'light' ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', state.theme); + localStorage.setItem('bssg-editor-theme', state.theme); + elements.themeToggle.textContent = state.theme === 'light' ? '🌙' : '☀'; + } - function toggleFocusMode() { - state.focusMode = !state.focusMode; - if (state.focusMode) { - document.body.classList.add('focus-mode'); - elements.markdownEditor.focus(); - } else { - document.body.classList.remove('focus-mode'); - } - } + function toggleFocusMode() { + state.focusMode = !state.focusMode; + if (state.focusMode) { + document.body.classList.add('focus-mode'); + elements.markdownEditor.focus(); + // Close mobile menu and sidebar + closeMobileMenu(); + closeSidebar(); + } else { + document.body.classList.remove('focus-mode'); + } + } - function togglePreview() { - state.previewMode = !state.previewMode; - const editorContent = document.querySelector('.editor-content'); - - if (state.previewMode) { - editorContent.classList.add('split-view'); - elements.previewBtn.textContent = '📝'; - elements.previewBtn.title = 'Hide Preview (Ctrl+P)'; - updatePreview(); // Ensure preview is up to date - } else { - editorContent.classList.remove('split-view'); - elements.previewBtn.textContent = '👁️'; - elements.previewBtn.title = 'Toggle Preview (Ctrl+P)'; - } - } + function togglePreview() { + state.previewMode = !state.previewMode; + const editorContent = elements.editorContent; + + if (state.previewMode) { + editorContent.classList.add('split-view'); + elements.previewBtn.textContent = '📝'; + elements.previewBtn.title = 'Hide Preview (Ctrl+P)'; + updatePreview(); // Ensure preview is up to date + } else { + editorContent.classList.remove('split-view'); + elements.previewBtn.textContent = '👁'; + elements.previewBtn.title = 'Toggle Preview (Ctrl+P)'; + } + } + + // Mobile-specific functions + function toggleMobileMenu() { + const isOpen = elements.headerActions.classList.contains('active'); + if (isOpen) { + closeMobileMenu(); + } else { + openMobileMenu(); + } + } + + function openMobileMenu() { + elements.headerActions.classList.add('active'); + elements.mobileMenuToggle.textContent = '✕'; + } + + function closeMobileMenu() { + elements.headerActions.classList.remove('active'); + elements.mobileMenuToggle.textContent = '☰'; + } + + function toggleSidebar() { + state.sidebarOpen = !state.sidebarOpen; + if (state.sidebarOpen) { + openSidebar(); + } else { + closeSidebar(); + } + } + + function openSidebar() { + if (state.isMobile) { + elements.sidebar.classList.add('mobile-open'); + state.sidebarOpen = true; + } + } + + function closeSidebar() { + if (state.isMobile) { + elements.sidebar.classList.remove('mobile-open'); + state.sidebarOpen = false; + } + } + + function handleResize() { + const wasMobile = state.isMobile; + state.isMobile = window.innerWidth <= 768; + + // If switching from mobile to desktop, reset mobile states + if (wasMobile && !state.isMobile) { + closeMobileMenu(); + closeSidebar(); + elements.sidebar.classList.remove('mobile-open'); + state.sidebarOpen = false; // Reset mobile sidebar state + } + + // If switching from desktop to mobile, ensure sidebar is properly hidden + if (!wasMobile && state.isMobile) { + elements.sidebar.classList.remove('mobile-open'); + state.sidebarOpen = false; + } + } function exportMarkdown() { updateStateFromForm(); @@ -1691,6 +2211,11 @@ a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); + + // Close mobile menu + if (state.isMobile) { + closeMobileMenu(); + } } function copyToClipboard() { @@ -1717,6 +2242,11 @@ elements.copyBtn.innerHTML = '📋 Copy'; }, 2000); }); + + // Close mobile menu + if (state.isMobile) { + closeMobileMenu(); + } } function importMarkdown(file) { @@ -1759,6 +2289,9 @@ description: parsed.description || '', image: parsed.image || '', imageCaption: parsed.image_caption || '', + authorName: parsed.author_name || '', + authorEmail: parsed.author_email || '', + fediverseCreator: parsed.fediverse_creator || '', content: markdownContent.trim() }; @@ -1774,72 +2307,72 @@ } } - // Unsplash integration - async function searchUnsplash(query) { - // Try to use real Unsplash API first, fallback to demo service - const UNSPLASH_ACCESS_KEY = localStorage.getItem('unsplash-access-key'); - - if (UNSPLASH_ACCESS_KEY) { - try { - const response = await fetch(`https://api.unsplash.com/search/photos?query=${encodeURIComponent(query)}&per_page=12&orientation=landscape`, { - headers: { - 'Authorization': `Client-ID ${UNSPLASH_ACCESS_KEY}` - } - }); - - if (response.ok) { - const data = await response.json(); - return data.results.map(photo => ({ - id: photo.id, - urls: { - small: photo.urls.small, - regular: photo.urls.regular, - full: photo.urls.full - }, - user: { - name: photo.user.name, - username: photo.user.username - }, - description: photo.description || photo.alt_description || 'Unsplash photo', - links: { - html: photo.links.html - } - })); - } - } catch (error) { - console.warn('Unsplash API failed, using demo service:', error); - } - } - - // Fallback to demo service (Lorem Picsum with better variety) - const categories = ['nature', 'city', 'technology', 'people', 'abstract', 'architecture']; - const results = []; - - for (let i = 0; i < 12; i++) { - const category = categories[i % categories.length]; - const randomId = Math.floor(Math.random() * 1000) + 1; - results.push({ - id: `demo-${i}`, - urls: { - small: `https://picsum.photos/300/200?random=${randomId}`, - regular: `https://picsum.photos/800/600?random=${randomId}`, - full: `https://picsum.photos/1200/800?random=${randomId}` - }, - user: { - name: `Demo User ${i + 1}`, - username: `demo_user_${i + 1}` - }, - description: `${category} photo - demo image`, - links: { - html: 'https://unsplash.com' - } - }); - } - - return new Promise(resolve => { - setTimeout(() => resolve(results), 300); - }); - } + // Unsplash integration + async function searchUnsplash(query) { + // Try to use real Unsplash API first, fallback to demo service + const UNSPLASH_ACCESS_KEY = localStorage.getItem('unsplash-access-key'); + + if (UNSPLASH_ACCESS_KEY) { + try { + const response = await fetch(`https://api.unsplash.com/search/photos?query=${encodeURIComponent(query)}&per_page=12&orientation=landscape`, { + headers: { + 'Authorization': `Client-ID ${UNSPLASH_ACCESS_KEY}` + } + }); + + if (response.ok) { + const data = await response.json(); + return data.results.map(photo => ({ + id: photo.id, + urls: { + small: photo.urls.small, + regular: photo.urls.regular, + full: photo.urls.full + }, + user: { + name: photo.user.name, + username: photo.user.username + }, + description: photo.description || photo.alt_description || 'Unsplash photo', + links: { + html: photo.links.html + } + })); + } + } catch (error) { + console.warn('Unsplash API failed, using demo service:', error); + } + } + + // Fallback to demo service (Lorem Picsum with better variety) + const categories = ['nature', 'city', 'technology', 'people', 'abstract', 'architecture']; + const results = []; + + for (let i = 0; i < 12; i++) { + const category = categories[i % categories.length]; + const randomId = Math.floor(Math.random() * 1000) + 1; + results.push({ + id: `demo-${i}`, + urls: { + small: `https://picsum.photos/300/200?random=${randomId}`, + regular: `https://picsum.photos/800/600?random=${randomId}`, + full: `https://picsum.photos/1200/800?random=${randomId}` + }, + user: { + name: `Demo User ${i + 1}`, + username: `demo_user_${i + 1}` + }, + description: `${category} photo - demo image`, + links: { + html: 'https://unsplash.com' + } + }); + } + + return new Promise(resolve => { + setTimeout(() => resolve(results), 300); + }); + } function displayUnsplashResults(results) { elements.unsplashResults.innerHTML = ''; @@ -1867,25 +2400,25 @@ }); } - function selectUnsplashImage(image) { - const imageUrl = image.urls.regular || image.urls.small; - let caption; - - if (image.id.startsWith('demo-')) { - caption = `${image.description} (Demo image)`; - } else { - caption = `Photo by ${image.user.name} on Unsplash`; - } - - elements.image.value = imageUrl; - elements.imageCaption.value = caption; - - state.currentPost.image = imageUrl; - state.currentPost.imageCaption = caption; - - elements.unsplashModal.classList.remove('active'); - markDirty(); - } + function selectUnsplashImage(image) { + const imageUrl = image.urls.regular || image.urls.small; + let caption; + + if (image.id.startsWith('demo-')) { + caption = `${image.description} (Demo image)`; + } else { + caption = `Photo by ${image.user.name} on Unsplash`; + } + + elements.image.value = imageUrl; + elements.imageCaption.value = caption; + + state.currentPost.image = imageUrl; + state.currentPost.imageCaption = caption; + + elements.unsplashModal.classList.remove('active'); + markDirty(); + } // Toolbar functions function insertMarkdown(before, after = '', placeholder = '') { @@ -1909,22 +2442,37 @@ // Event listeners function setupEventListeners() { - // Form inputs - elements.title.addEventListener('input', (e) => { - const oldTitle = state.currentPost.title; - state.currentPost.title = e.target.value; - - // Auto-generate slug if it's empty or matches the old title's slug - const oldSlug = generateSlug(oldTitle); - const currentSlug = elements.slug.value; - - if (!currentSlug || currentSlug === oldSlug) { - const newSlug = generateSlug(e.target.value); - elements.slug.value = newSlug; - state.currentPost.slug = newSlug; - } - markDirty(); - }); + // Mobile menu toggle + elements.mobileMenuToggle.addEventListener('click', toggleMobileMenu); + + // Sidebar toggle + elements.sidebarToggle.addEventListener('click', toggleSidebar); + elements.sidebarClose.addEventListener('click', closeSidebar); + + // Preview mobile toggle + elements.previewMobileToggle.addEventListener('click', () => { + togglePreview(); + }); + + // Window resize + window.addEventListener('resize', handleResize); + + // Form inputs + elements.title.addEventListener('input', (e) => { + const oldTitle = state.currentPost.title; + state.currentPost.title = e.target.value; + + // Auto-generate slug if it's empty or matches the old title's slug + const oldSlug = generateSlug(oldTitle); + const currentSlug = elements.slug.value; + + if (!currentSlug || currentSlug === oldSlug) { + const newSlug = generateSlug(e.target.value); + elements.slug.value = newSlug; + state.currentPost.slug = newSlug; + } + markDirty(); + }); elements.slug.addEventListener('input', (e) => { state.currentPost.slug = e.target.value; @@ -1955,20 +2503,35 @@ markDirty(); }); - elements.imageCaption.addEventListener('input', (e) => { - state.currentPost.imageCaption = e.target.value; - markDirty(); - }); + elements.imageCaption.addEventListener('input', (e) => { + state.currentPost.imageCaption = e.target.value; + markDirty(); + }); - // Unsplash API key - elements.unsplashKey.addEventListener('input', (e) => { - const key = e.target.value.trim(); - if (key) { - localStorage.setItem('unsplash-access-key', key); - } else { - localStorage.removeItem('unsplash-access-key'); - } - }); + elements.authorName.addEventListener('input', (e) => { + state.currentPost.authorName = e.target.value; + markDirty(); + }); + + elements.authorEmail.addEventListener('input', (e) => { + state.currentPost.authorEmail = e.target.value; + markDirty(); + }); + + elements.fediverseCreator.addEventListener('input', (e) => { + state.currentPost.fediverseCreator = e.target.value; + markDirty(); + }); + + // Unsplash API key + elements.unsplashKey.addEventListener('input', (e) => { + const key = e.target.value.trim(); + if (key) { + localStorage.setItem('unsplash-access-key', key); + } else { + localStorage.removeItem('unsplash-access-key'); + } + }); // Tag input elements.tagInput.addEventListener('keydown', (e) => { @@ -1998,53 +2561,95 @@ markDirty(); }); - // Toolbar buttons - elements.boldBtn.addEventListener('click', () => insertMarkdown('**', '**', 'bold text')); - elements.italicBtn.addEventListener('click', () => insertMarkdown('*', '*', 'italic text')); - elements.linkBtn.addEventListener('click', () => { - const url = prompt('Enter URL:'); - if (url) { - insertMarkdown('[', `](${url})`, 'link text'); - } - }); - elements.imageBtn.addEventListener('click', () => { - const url = prompt('Enter image URL:'); - if (url) { - const alt = prompt('Enter alt text (optional):') || 'image'; - insertMarkdown(`![${alt}](${url})`, '', ''); - } - }); - elements.codeBtn.addEventListener('click', () => insertMarkdown('`', '`', 'code')); - elements.codeBlockBtn.addEventListener('click', () => insertMarkdown('\n```\n', '\n```\n', 'code block')); - elements.quoteBtn.addEventListener('click', () => insertMarkdown('> ', '', 'quote')); - elements.listBtn.addEventListener('click', () => insertMarkdown('- ', '', 'list item')); - elements.orderedListBtn.addEventListener('click', () => insertMarkdown('1. ', '', 'list item')); - elements.h1Btn.addEventListener('click', () => insertMarkdown('# ', '', 'Heading 1')); - elements.h2Btn.addEventListener('click', () => insertMarkdown('## ', '', 'Heading 2')); - elements.h3Btn.addEventListener('click', () => insertMarkdown('### ', '', 'Heading 3')); - elements.hrBtn.addEventListener('click', () => insertMarkdown('\n---\n', '', '')); - elements.previewBtn.addEventListener('click', togglePreview); - elements.focusBtn.addEventListener('click', toggleFocusMode); - elements.focusExit.addEventListener('click', toggleFocusMode); + // Toolbar buttons - ensure all work properly + elements.boldBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('**', '**', 'bold text'); + }); + elements.italicBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('*', '*', 'italic text'); + }); + elements.linkBtn.addEventListener('click', (e) => { + e.preventDefault(); + const url = prompt('Enter URL:'); + if (url) { + insertMarkdown('[', `](${url})`, 'link text'); + } + }); + elements.imageBtn.addEventListener('click', (e) => { + e.preventDefault(); + const url = prompt('Enter image URL:'); + if (url) { + const alt = prompt('Enter alt text (optional):') || 'image'; + insertMarkdown(`![${alt}](${url})`, '', ''); + } + }); + elements.codeBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('`', '`', 'code'); + }); + elements.codeBlockBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('\n```\n', '\n```\n', 'code block'); + }); + elements.quoteBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('> ', '', 'quote'); + }); + elements.listBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('- ', '', 'list item'); + }); + elements.orderedListBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('1. ', '', 'list item'); + }); + elements.h1Btn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('# ', '', 'Heading 1'); + }); + elements.h2Btn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('## ', '', 'Heading 2'); + }); + elements.h3Btn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('### ', '', 'Heading 3'); + }); + elements.hrBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('\n---\n', '', ''); + }); + elements.previewBtn.addEventListener('click', (e) => { + e.preventDefault(); + togglePreview(); + }); + elements.focusBtn.addEventListener('click', (e) => { + e.preventDefault(); + toggleFocusMode(); + }); + elements.focusExit.addEventListener('click', toggleFocusMode); - // Header buttons - elements.themeToggle.addEventListener('click', toggleTheme); - elements.newBtn.addEventListener('click', newArticle); - elements.saveBtn.addEventListener('click', openSaveModal); - elements.loadBtn.addEventListener('click', openLoadModal); - elements.exportBtn.addEventListener('click', exportMarkdown); - elements.copyBtn.addEventListener('click', copyToClipboard); - - elements.importBtn.addEventListener('click', () => { - elements.fileInput.click(); - }); - - elements.fileInput.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (file) { - importMarkdown(file); - } - }); + // Header buttons + elements.themeToggle.addEventListener('click', toggleTheme); + elements.newBtn.addEventListener('click', newArticle); + elements.saveBtn.addEventListener('click', openSaveModal); + elements.loadBtn.addEventListener('click', openLoadModal); + elements.exportBtn.addEventListener('click', exportMarkdown); + elements.copyBtn.addEventListener('click', copyToClipboard); + + elements.importBtn.addEventListener('click', () => { + elements.fileInput.click(); + closeMobileMenu(); + }); + + elements.fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + importMarkdown(file); + } + }); // Unsplash modal elements.unsplashBtn.addEventListener('click', () => { @@ -2059,172 +2664,256 @@ elements.unsplashModal.classList.remove('active'); }); - elements.unsplashSearch.addEventListener('input', async (e) => { - const query = e.target.value.trim(); - if (query.length > 2) { - elements.unsplashResults.innerHTML = '<div style="text-align: center; grid-column: 1 / -1;"><div class="spinner"></div></div>'; - - // Show status - const hasApiKey = localStorage.getItem('unsplash-access-key'); - elements.unsplashStatus.style.display = 'block'; - if (hasApiKey) { - elements.unsplashStatus.style.background = 'var(--success)'; - elements.unsplashStatus.style.color = 'white'; - elements.unsplashStatus.textContent = '✓ Using Unsplash API - Real photos'; - } else { - elements.unsplashStatus.style.background = 'var(--warning)'; - elements.unsplashStatus.style.color = 'white'; - elements.unsplashStatus.textContent = '⚠ Using demo images - Add Unsplash API key in settings for real photos'; - } - - try { - const results = await searchUnsplash(query); - displayUnsplashResults(results); - } catch (error) { - console.error('Unsplash search failed:', error); - elements.unsplashResults.innerHTML = '<p style="color: var(--error); text-align: center; grid-column: 1 / -1;">Search failed. Please try again.</p>'; - } - } else { - elements.unsplashStatus.style.display = 'none'; - } - }); + elements.unsplashSearch.addEventListener('input', async (e) => { + const query = e.target.value.trim(); + if (query.length > 2) { + elements.unsplashResults.innerHTML = '<div style="text-align: center; grid-column: 1 / -1;"><div class="spinner"></div></div>'; + + // Show status + const hasApiKey = localStorage.getItem('unsplash-access-key'); + elements.unsplashStatus.style.display = 'block'; + if (hasApiKey) { + elements.unsplashStatus.style.background = 'var(--success)'; + elements.unsplashStatus.style.color = 'white'; + elements.unsplashStatus.textContent = '✓ Using Unsplash API - Real photos'; + } else { + elements.unsplashStatus.style.background = 'var(--warning)'; + elements.unsplashStatus.style.color = 'white'; + elements.unsplashStatus.textContent = '⚠ Using demo images - Add Unsplash API key in settings for real photos'; + } + + try { + const results = await searchUnsplash(query); + displayUnsplashResults(results); + } catch (error) { + console.error('Unsplash search failed:', error); + elements.unsplashResults.innerHTML = '<p style="color: var(--error); text-align: center; grid-column: 1 / -1;">Search failed. Please try again.</p>'; + } + } else { + elements.unsplashStatus.style.display = 'none'; + } + }); - // Keyboard shortcuts - document.addEventListener('keydown', (e) => { - if (e.ctrlKey || e.metaKey) { - switch (e.key) { - case 'b': - e.preventDefault(); - insertMarkdown('**', '**', 'bold text'); - break; - case 'i': - e.preventDefault(); - insertMarkdown('*', '*', 'italic text'); - break; - case 'k': - e.preventDefault(); - insertMarkdown('[', '](url)', 'link text'); - break; - case '`': - e.preventDefault(); - insertMarkdown('`', '`', 'code'); - break; - case 's': - e.preventDefault(); - openSaveModal(); - break; - case 'p': - e.preventDefault(); - togglePreview(); - break; - case 'n': - e.preventDefault(); - newArticle(); - break; - case 'o': - e.preventDefault(); - openLoadModal(); - break; - } - } - - // Escape key to exit focus mode - if (e.key === 'Escape' && state.focusMode) { - e.preventDefault(); - toggleFocusMode(); - } - }); + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case 'b': + e.preventDefault(); + insertMarkdown('**', '**', 'bold text'); + break; + case 'i': + e.preventDefault(); + insertMarkdown('*', '*', 'italic text'); + break; + case 'k': + e.preventDefault(); + insertMarkdown('[', '](url)', 'link text'); + break; + case '`': + e.preventDefault(); + insertMarkdown('`', '`', 'code'); + break; + case 's': + e.preventDefault(); + openSaveModal(); + break; + case 'p': + e.preventDefault(); + togglePreview(); + break; + case 'n': + e.preventDefault(); + newArticle(); + break; + case 'o': + e.preventDefault(); + openLoadModal(); + break; + } + } + + // Escape key to exit focus mode or close mobile elements + if (e.key === 'Escape') { + if (state.focusMode) { + e.preventDefault(); + toggleFocusMode(); + } else if (state.isMobile) { + if (elements.headerActions.classList.contains('active')) { + closeMobileMenu(); + } + if (state.sidebarOpen) { + closeSidebar(); + } + } + } + }); - // Note: Auto-save is now handled by: - // 1. Every 10 words (in updateWordCount function) - // 2. After 5 seconds of inactivity (in scheduleAutoSave function) + // Save modal events + elements.closeSaveModal.addEventListener('click', () => { + elements.saveModal.classList.remove('active'); + }); + + elements.cancelSave.addEventListener('click', () => { + elements.saveModal.classList.remove('active'); + }); + + elements.confirmSave.addEventListener('click', saveCurrentArticle); + + elements.saveTitle.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + saveCurrentArticle(); + } + }); - // Save modal events - elements.closeSaveModal.addEventListener('click', () => { - elements.saveModal.classList.remove('active'); - }); - - elements.cancelSave.addEventListener('click', () => { - elements.saveModal.classList.remove('active'); - }); - - elements.confirmSave.addEventListener('click', saveCurrentArticle); - - elements.saveTitle.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - saveCurrentArticle(); - } - }); + // Load modal events + elements.closeLoadModal.addEventListener('click', () => { + elements.loadModal.classList.remove('active'); + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + }); + + elements.cancelLoad.addEventListener('click', () => { + elements.loadModal.classList.remove('active'); + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + }); + + elements.deleteSelected.addEventListener('click', deleteSelectedArticle); + + elements.searchArticles.addEventListener('input', (e) => { + displayArticles(e.target.value); + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + }); - // Load modal events - elements.closeLoadModal.addEventListener('click', () => { - elements.loadModal.classList.remove('active'); - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - }); - - elements.cancelLoad.addEventListener('click', () => { - elements.loadModal.classList.remove('active'); - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - }); - - elements.deleteSelected.addEventListener('click', deleteSelectedArticle); - - elements.searchArticles.addEventListener('input', (e) => { - displayArticles(e.target.value); - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - }); + // Close modals when clicking outside + elements.unsplashModal.addEventListener('click', (e) => { + if (e.target === elements.unsplashModal) { + elements.unsplashModal.classList.remove('active'); + } + }); - // Close modals when clicking outside - elements.unsplashModal.addEventListener('click', (e) => { - if (e.target === elements.unsplashModal) { - elements.unsplashModal.classList.remove('active'); - } - }); + elements.saveModal.addEventListener('click', (e) => { + if (e.target === elements.saveModal) { + elements.saveModal.classList.remove('active'); + } + }); - elements.saveModal.addEventListener('click', (e) => { - if (e.target === elements.saveModal) { - elements.saveModal.classList.remove('active'); - } - }); + elements.loadModal.addEventListener('click', (e) => { + if (e.target === elements.loadModal) { + elements.loadModal.classList.remove('active'); + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + } + }); - elements.loadModal.addEventListener('click', (e) => { - if (e.target === elements.loadModal) { - elements.loadModal.classList.remove('active'); - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - } - }); + // Close mobile menu when clicking outside + document.addEventListener('click', (e) => { + if (state.isMobile && + elements.headerActions.classList.contains('active') && + !elements.headerActions.contains(e.target) && + !elements.mobileMenuToggle.contains(e.target)) { + closeMobileMenu(); + } + }); + + // Close sidebar when clicking backdrop on mobile + document.addEventListener('click', (e) => { + if (state.isMobile && + state.sidebarOpen && + !elements.sidebar.contains(e.target) && + !elements.sidebarToggle.contains(e.target)) { + closeSidebar(); + } + }); + + // Touch gestures for mobile (basic swipe to close sidebar) + let touchStartX = 0; + let touchStartY = 0; + + elements.sidebar.addEventListener('touchstart', (e) => { + touchStartX = e.touches[0].clientX; + touchStartY = e.touches[0].clientY; + }); + + elements.sidebar.addEventListener('touchmove', (e) => { + if (!state.isMobile || !state.sidebarOpen) return; + + const touchX = e.touches[0].clientX; + const touchY = e.touches[0].clientY; + const diffX = touchStartX - touchX; + const diffY = Math.abs(touchStartY - touchY); + + // If horizontal swipe is more significant than vertical + if (Math.abs(diffX) > diffY && diffX > 50) { + closeSidebar(); + } + }); } // Initialize the application function init() { - // Set initial date + // Set initial date in local time const now = new Date(); - elements.date.value = now.toISOString().slice(0, 16); + const localDateTime = new Date(now.getTime() - (now.getTimezoneOffset() * 60000)); + elements.date.value = localDateTime.toISOString().slice(0, 16); state.currentPost.date = elements.date.value; - // Apply saved theme - document.documentElement.setAttribute('data-theme', state.theme); - elements.themeToggle.textContent = state.theme === 'light' ? '🌙' : '☀️'; + // Apply saved theme + document.documentElement.setAttribute('data-theme', state.theme); + elements.themeToggle.textContent = state.theme === 'light' ? '🌙' : '☀'; - // Load saved Unsplash key - const savedKey = localStorage.getItem('unsplash-access-key'); - if (savedKey) { - elements.unsplashKey.value = savedKey; - } + // Load saved Unsplash key + const savedKey = localStorage.getItem('unsplash-access-key'); + if (savedKey) { + elements.unsplashKey.value = savedKey; + } - // Load saved draft - loadFromLocalStorage(); + // Check if mobile and set initial sidebar state + state.isMobile = window.innerWidth <= 768; + if (state.isMobile) { + // On mobile, sidebar starts hidden + state.sidebarOpen = false; + elements.sidebar.classList.remove('mobile-open'); + } else { + // On desktop, sidebar is always visible (default CSS) + state.sidebarOpen = false; // Not using mobile open state + } - // Setup event listeners - setupEventListeners(); + // Load saved draft + loadFromLocalStorage(); - // Initial update - updateWordCount(); + // Setup event listeners + setupEventListeners(); + + // Initial update + updateWordCount(); + + // Set initial viewport height for mobile + if (state.isMobile) { + const setVH = () => { + const vh = window.innerHeight * 0.01; + document.documentElement.style.setProperty('--vh', `${vh}px`); + }; + setVH(); + window.addEventListener('resize', setVH); + window.addEventListener('orientationchange', setVH); + } + + // Prevent zoom on inputs for iOS + if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { + const inputs = document.querySelectorAll('input, textarea, select'); + inputs.forEach(input => { + input.addEventListener('focus', () => { + input.style.fontSize = '16px'; + }); + input.addEventListener('blur', () => { + input.style.fontSize = ''; + }); + }); + } console.log('BSSG Post Editor initialized'); } @@ -2233,4 +2922,4 @@ document.addEventListener('DOMContentLoaded', init); </script> </body> -</html> \ No newline at end of file +</html> diff --git a/config.sh b/config.sh index eaa0895..0a615f0 100644 --- a/config.sh +++ b/config.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # BSSG - Configuration File -# Version 0.16 +# Version 0.32 # Contains all configurable parameters for the static site generator # Developed by Stefano Marinelli (stefano@dragas.it) # @@ -27,6 +27,8 @@ CACHE_DIR=".bssg_cache" # Default cache directory location (relative to BSSG roo CLEAN_OUTPUT=false # If true, BSSG will always perform a full rebuild REBUILD_AFTER_POST=true # Build site automatically after creating a new post (scripts/post.sh) REBUILD_AFTER_EDIT=true # Build site automatically after editing a post (scripts/edit.sh) +PRECOMPRESS_ASSETS="false" # Options: "true", "false". If true, compress text assets (HTML, CSS, XML, JS) with gzip during build. +BUILD_MODE="ram" # Options: "normal", "ram". "ram" preloads inputs and keeps build state in memory (writes only output artifacts). # Customization CUSTOM_CSS="" # Optional: Path to custom CSS file relative to output root (e.g., "/css/custom.css"). File should be placed in STATIC_DIR. @@ -37,6 +39,19 @@ SITE_DESCRIPTION="A complete SSG - written in bash" SITE_URL="http://localhost:8000" AUTHOR_NAME="Anonymous" AUTHOR_EMAIL="anonymous@example.com" +REL_ME_URL="" # Optional fediverse profile URL for <link rel="me"> verification, e.g. "https://mastodon.example/@john" +# Optional additional rel="me" verification links. BSSG emits all unique values from +# REL_ME_URL and REL_ME_URLS. +# REL_ME_URLS=( +# "https://mastodon.example/@john" +# "https://another-fedi.example/@john" +# ) +FEDIVERSE_CREATOR="" # Optional default fediverse:creator value for posts, e.g. "@you@example.social" +# Optional per-author overrides matched against author_name exactly. +# declare -A AUTHOR_FEDIVERSE_CREATORS=( +# ["Jane Smith"]="@jane@example.social" +# ["John Doe"]="@john@example.com" +# ) # Content configuration DATE_FORMAT="%Y-%m-%d %H:%M:%S %z" @@ -46,7 +61,11 @@ POSTS_PER_PAGE=10 RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". Include full post content in RSS feed. RSS_FILENAME="rss.xml" # The filename for the main RSS feed (e.g., feed.xml, rss.xml) +INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". Show full post content on homepage instead of just description/excerpt. ENABLE_ARCHIVES=true # Enable or disable archive pages +ENABLE_AUTHOR_PAGES=false # Enable or disable author pages (default: false) +ENABLE_AUTHOR_RSS=false # Enable or disable author-specific RSS feeds (default: false) +SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum authors to show menu (default: 2) URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs. Available: Year, Month, Day, slug ENABLE_TAG_RSS=true # Enable or disable tag-specific RSS feed generation (default: true) @@ -62,6 +81,10 @@ MARKDOWN_PROCESSOR="commonmark" # Options: "pandoc", "commonmark", or "markdown. # Language Configuration SITE_LANG="en" # Default language code (e.g., en, es, fr). See locales/ directory. +# Related Posts Configuration +ENABLE_RELATED_POSTS=true # Enable or disable related posts feature +RELATED_POSTS_COUNT=3 # Number of related posts to show (default: 3) + # Server Configuration (for 'bssg.sh server' command) # These are the defaults used by 'bssg.sh server' if not overridden by command-line options. BSSG_SERVER_PORT_DEFAULT="8000" # Default port for the local development server diff --git a/generate_theme_previews.sh b/generate_theme_previews.sh index e14246d..ef22659 100755 --- a/generate_theme_previews.sh +++ b/generate_theme_previews.sh @@ -11,10 +11,13 @@ set -euo pipefail # Ensure BSSG_MAIN_SCRIPT points to the main bssg.sh in the project root # Ensure this script (generate_theme_previews.sh) is run from the project root. readonly BSSG_MAIN_SCRIPT="./bssg.sh" -readonly THEMES_DIR="./themes" -# EXAMPLE_ROOT_DIR is now dynamic, see determine_example_root_dir function +THEMES_DIR="./themes" +TEMPLATES_DIR="./templates" CONFIG_FILE="config.sh" # For reading default SITE_URL if not overridden LOCAL_CONFIG_FILE="config.sh.local" # For reading default SITE_URL if not overridden +CMD_LINE_CONFIG_FILE="" +FINAL_CONFIG_OVERRIDE="" +site_url_from_cli="" # Global variable for the dynamic example root directory EXAMPLE_ROOT_DIR_DYNAMIC="./example" # Default value, will be updated @@ -27,8 +30,9 @@ BLUE='\033[0;34m' NC='\033[0m' # No Color # Default SITE_URL from config.sh if no other is specified by script's --site-url -# This will be the BASE for theme preview URLs. SITE_URL_BASE="http://localhost" +FULL_BUILD_MODE=false +SITE_URL_TOKEN="__BSSG_THEME_SITE_URL__" # --- Helper Functions --- info() { @@ -50,13 +54,9 @@ error() { # --- Cleanup Functions --- cleanup_directories() { - # This function is called on EXIT by the trap. - # Currently, EXAMPLE_ROOT_DIR_DYNAMIC is cleaned at the start of build_previews. - # If any other script-specific temporary files were created, they would be cleaned here. info "Cleanup function called on exit. No specific preview-script files to clean in this version." } -# Trap EXIT signal to ensure cleanup (if any specific cleanup actions are needed later) trap cleanup_directories EXIT # --- Print Help --- @@ -68,35 +68,51 @@ Generate preview sites for all available BSSG themes. Options: -h, --help Display this help message and exit + --config PATH Use a custom BSSG configuration file --site-url URL Set the base SITE_URL for theme previews (overrides config files) + --full-build Build each theme independently (slower fallback mode) Configuration: + BSSG configuration is selected in this order: + 1. Command line argument (--config) + 2. BSSG_LCONF environment variable + 3. Local config file ($LOCAL_CONFIG_FILE) + 4. Main config file ($CONFIG_FILE) + The script will use the SITE_URL from the following sources in order of precedence: 1. Command line argument (--site-url) - 2. Local config file ($LOCAL_CONFIG_FILE) - 3. Main config file ($CONFIG_FILE) - 4. Default value (http://localhost) + 2. Selected BSSG configuration + 3. Default value (http://localhost) Output: Theme previews will be generated in the '$EXAMPLE_ROOT_DIR_DYNAMIC' directory, with each theme in its own subdirectory. An index.html file will be created to navigate between themes. + +Performance: + By default, this script builds the site once and then clones it per theme, + replacing css/style.css and SITE_URL references. This is significantly faster. + Use --full-build to force one full BSSG build per theme. EOF exit 0 } # --- Parse Command Line Arguments (for this script) --- parse_args() { - # Initialize variable - site_url_from_cli="" - - # Parse command line arguments while [[ $# -gt 0 ]]; do case "$1" in -h|--help) print_help ;; + --config) + if [[ -n "${2:-}" && "$2" != -* ]]; then + CMD_LINE_CONFIG_FILE="$2" + shift 2 + else + error "--config requires a path to a BSSG configuration file" + fi + ;; --site-url) if [[ -n "${2:-}" ]]; then site_url_from_cli="$2" @@ -105,6 +121,10 @@ parse_args() { error "--site-url requires a value for the base URL of previews" fi ;; + --full-build) + FULL_BUILD_MODE=true + shift + ;; *) warn "Unknown option: $1 (ignored)" shift @@ -113,49 +133,53 @@ parse_args() { done } +resolve_config_override() { + if [ -n "$CMD_LINE_CONFIG_FILE" ]; then + FINAL_CONFIG_OVERRIDE="$CMD_LINE_CONFIG_FILE" + info "Using configuration file specified via --config: $FINAL_CONFIG_OVERRIDE" + elif [ -v BSSG_LCONF ] && [ -n "${BSSG_LCONF}" ]; then + FINAL_CONFIG_OVERRIDE="$BSSG_LCONF" + info "Using configuration file specified via BSSG_LCONF: $FINAL_CONFIG_OVERRIDE" + fi +} + +load_effective_bssg_configuration() { + local project_root_abs config_dump + local config_separator=$'\037' + + project_root_abs=$(pwd -P) + config_dump=$( + export BSSG_SCRIPT_DIR="$project_root_abs" + bash -c ' + source "$BSSG_SCRIPT_DIR/scripts/build/config_loader.sh" "$1" >/dev/null 2>&1 + printf "%s\037%s\037%s\037%s" "$SITE_URL" "$OUTPUT_DIR" "$THEMES_DIR" "$TEMPLATES_DIR" + ' bash "$FINAL_CONFIG_OVERRIDE" + ) || { + if [ -n "$FINAL_CONFIG_OVERRIDE" ]; then + error "Failed to load BSSG configuration from '$FINAL_CONFIG_OVERRIDE'." + fi + error "Failed to load the default BSSG configuration." + } + + IFS="$config_separator" read -r SITE_URL OUTPUT_DIR THEMES_DIR TEMPLATES_DIR <<< "$config_dump" +} + # --- Load Configuration (for this script's SITE_URL_BASE) --- load_config() { info "Loading base SITE_URL configuration for previews..." - - # Load main config if it exists to get a default SITE_URL - if [ -f "$CONFIG_FILE" ]; then - # Source with subshell to avoid polluting global namespace - # but extract SITE_URL if defined. - local main_conf_site_url - main_conf_site_url=$(grep -m 1 "^SITE_URL=" "$CONFIG_FILE" | cut -d'"' -f2 || echo "") - if [ -n "$main_conf_site_url" ]; then - SITE_URL_BASE="$main_conf_site_url" - info "Using SITE_URL_BASE='$SITE_URL_BASE' from $CONFIG_FILE as default" - fi - else - warn "Main configuration file '$CONFIG_FILE' not found, using default SITE_URL_BASE='$SITE_URL_BASE'." - fi - - # Load local config if it exists (overrides main config for SITE_URL_BASE) - if [ -f "$LOCAL_CONFIG_FILE" ]; then - local local_conf_site_url - if grep -q "^SITE_URL=" "$LOCAL_CONFIG_FILE" 2>/dev/null; then - local_conf_site_url=$(grep -m 1 "^SITE_URL=" "$LOCAL_CONFIG_FILE" | cut -d'"' -f2 || echo "") - if [ -n "$local_conf_site_url" ]; then - SITE_URL_BASE="$local_conf_site_url" - info "Overridden SITE_URL_BASE='$SITE_URL_BASE' from $LOCAL_CONFIG_FILE" - else - warn "Found $LOCAL_CONFIG_FILE but failed to extract SITE_URL, using current SITE_URL_BASE='$SITE_URL_BASE'" - fi - fi - fi - - # Command line argument for this script overrides all config files for SITE_URL_BASE + + load_effective_bssg_configuration + SITE_URL_BASE="$SITE_URL" + info "Using SITE_URL_BASE='$SITE_URL_BASE' from the effective BSSG configuration" + if [ -n "$site_url_from_cli" ]; then SITE_URL_BASE="$site_url_from_cli" info "Using SITE_URL_BASE='$SITE_URL_BASE' from command line argument for previews" fi - + success "Configuration loaded. Using SITE_URL_BASE='$SITE_URL_BASE' for theme previews." } - - # --- Sanity Checks --- check_dependencies() { info "Checking requirements..." @@ -168,92 +192,196 @@ check_dependencies() { if [ ! -d "$THEMES_DIR" ]; then error "Themes directory not found at '$THEMES_DIR'. This script must be run from the BSSG project root." fi - # Check for essential commands - for cmd in find basename mkdir mv cat date rm ls grep cut git realpath; do # Added realpath + for cmd in find basename mkdir mv cat date rm ls grep awk sed dirname sort printf bash; do # Added awk, sed, printf, bash if ! command -v "$cmd" >/dev/null 2>&1; then error "Required command '$cmd' not found in PATH." fi done + # Check for pwd -P behavior (standard in POSIX sh, but good to be aware) + if ! (pwd -P >/dev/null 2>&1); then + warn "pwd -P might not be supported or behave as expected on this system. Path resolution might be affected." + fi success "Requirements met." } +# --- Path Normalization Helper (replaces realpath -m) --- +_normalize_path_string() { + local path_to_normalize="$1" + local current_dir + current_dir=$(pwd -P) # Get current physical working directory + + local temp_path + # Make path absolute if it's relative, using the script's CWD as base + if [[ "$path_to_normalize" != /* ]]; then + temp_path="$current_dir/$path_to_normalize" + else + temp_path="$path_to_normalize" + fi + + # Add a sentinel component to handle leading '..' correctly by making the path e.g., /sentinel/actual/path + # This simplifies logic for popping '..' at the "root" + temp_path="/sentinel${temp_path}" + + local OIFS="$IFS" + IFS='/' + # shellcheck disable=SC2206 # Word splitting is desired here for path components + local components=($temp_path) + IFS="$OIFS" + + local result_components=() + for comp in "${components[@]}"; do + if [[ -z "$comp" || "$comp" == "." ]]; then + continue # Skip empty or current dir components + fi + if [[ "$comp" == ".." ]]; then + # Only pop if result_components is not empty and last component is not 'sentinel' + if [[ ${#result_components[@]} -gt 0 && "${result_components[${#result_components[@]}-1]}" != "sentinel" ]]; then + unset 'result_components[${#result_components[@]}-1]' + fi + else + result_components+=("$comp") + fi + done + + # Reconstruct the path + local final_path + # Remove 'sentinel' if it's the first component + if [[ ${#result_components[@]} -gt 0 && "${result_components[0]}" == "sentinel" ]]; then + # Handle case where only sentinel remains (e.g. /sentinel/../..) -> / + if [[ ${#result_components[@]} -eq 1 ]]; then + final_path="/" + else + # Join remaining components. ${array[*]:1} gives elements from index 1. + final_path="/$(IFS=/; echo "${result_components[*]:1}")" + fi + else + # This case implies original path was something like /../../.. that resolved above sentinel + # or the sentinel was incorrectly processed. Should resolve to root. + final_path="/" + fi + + # Post-process: remove multiple slashes, trailing slash (unless it's just "/") + final_path=$(echo "$final_path" | sed 's#//*#/#g') + if [[ "$final_path" != "/" && "${final_path: -1}" == "/" ]]; then + final_path="${final_path%/}" + fi + # If final_path is empty after all this (e.g. input was just "/"), ensure it's "/" + if [[ -z "$final_path" ]]; then + echo "/" + else + echo "$final_path" + fi +} + + # --- Main Logic --- -# 1. Find all themes (directories inside the themes directory) find_themes() { info "Searching for themes in '$THEMES_DIR'..." - # Check if directory exists first if [ ! -d "$THEMES_DIR" ]; then error "Themes directory '$THEMES_DIR' does not exist!" fi - # Debug the find command output echo "Debug: listing themes directory content with ls" - ls -la "$THEMES_DIR" + ls -la "$THEMES_DIR" # Keep for debugging if needed - echo "Debug: attempting BSD/FreeBSD compatible find" - - # Use a more compatible approach for FreeBSD and other systems - themes=() + local theme_names=() for d in "$THEMES_DIR"/*; do if [ -d "$d" ]; then - # Extract just the basename - theme_name=$(basename "$d") - themes+=("$theme_name") + theme_names+=("$(basename "$d")") fi done - if [ ${#themes[@]} -eq 0 ]; then + if [ ${#theme_names[@]} -eq 0 ]; then error "No valid theme directories found in '$THEMES_DIR'." fi - # Sort the themes array (not needed with find command previously) - # Simple bubble sort - for ((i=0; i<${#themes[@]}; i++)); do - for ((j=0; j<${#themes[@]}-i-1; j++)); do - if [[ "${themes[j]}" > "${themes[j+1]}" ]]; then - # swap - temp="${themes[j]}" - themes[j]="${themes[j+1]}" - themes[j+1]="$temp" - fi - done - done + # Sort themes using standard sort command + # Store sorted names back into the global 'themes' array + local sorted_theme_names_nl + sorted_theme_names_nl=$(printf "%s\n" "${theme_names[@]}" | sort) + + themes=() # Clear global themes array before repopulating + while IFS= read -r line; do + if [ -n "$line" ]; then # Ensure no empty lines become theme names + themes+=("$line") + fi + done <<< "$sorted_theme_names_nl" info "Found ${#themes[@]} themes: ${themes[*]}" } -# 2. Build preview for each theme -build_previews() { - info "Clearing existing example directory: '$EXAMPLE_ROOT_DIR_DYNAMIC'" - # Ensure the EXAMPLE_ROOT_DIR_DYNAMIC itself exists, then clear its contents - mkdir -p "$EXAMPLE_ROOT_DIR_DYNAMIC" - # Remove contents including hidden files, suppress errors for non-existent hidden files - rm -rf "${EXAMPLE_ROOT_DIR_DYNAMIC:?}"/* "${EXAMPLE_ROOT_DIR_DYNAMIC:?}"/.??* 2>/dev/null || true - success "Example directory cleared and ready." +run_bssg_build() { + local -a cmd=("$BSSG_MAIN_SCRIPT") + local formatted_cmd + if [ -n "$FINAL_CONFIG_OVERRIDE" ]; then + cmd+=(--config "$FINAL_CONFIG_OVERRIDE") + fi + + cmd+=(build "$@") + formatted_cmd=$(printf '%q ' "${cmd[@]}") + info "Executing: ${formatted_cmd% }" + + "${cmd[@]}" +} + +build_previews() { + prepare_example_directory + + if [ "$FULL_BUILD_MODE" = true ]; then + info "Using full-build mode (one BSSG build per theme)." + build_previews_full + return + fi + + if has_theme_specific_templates; then + warn "Theme-specific templates detected under templates/<theme>/. Falling back to full per-theme builds." + build_previews_full + return + fi + + info "Using fast preview mode: single build + clone + theme CSS swap." + build_previews_fast +} + +prepare_example_directory() { + info "Clearing existing example directory: '$EXAMPLE_ROOT_DIR_DYNAMIC'" + mkdir -p "$EXAMPLE_ROOT_DIR_DYNAMIC" + # More robustly clear contents. Using find is safer for unusual filenames. + # However, rm -rf with :? guard is common. + # Ensure EXAMPLE_ROOT_DIR_DYNAMIC is not empty and not root, for safety. + if [ -z "$EXAMPLE_ROOT_DIR_DYNAMIC" ] || [ "$EXAMPLE_ROOT_DIR_DYNAMIC" = "/" ] || [ "$EXAMPLE_ROOT_DIR_DYNAMIC" = "." ] || [ "$EXAMPLE_ROOT_DIR_DYNAMIC" = ".." ]; then + error "Safety check failed: EXAMPLE_ROOT_DIR_DYNAMIC is '$EXAMPLE_ROOT_DIR_DYNAMIC'. Aborting clear." + fi + rm -rf "${EXAMPLE_ROOT_DIR_DYNAMIC:?}"/* "${EXAMPLE_ROOT_DIR_DYNAMIC:?}"/.* 2>/dev/null || true + # Note: .??* misses files like .a but covers most common dotfiles. /.* is more thorough but needs care. + # A safer alternative if `find` is available: + # find "$EXAMPLE_ROOT_DIR_DYNAMIC" -mindepth 1 -delete + success "Example directory cleared and ready." +} + +build_previews_full() { info "Starting theme preview builds..." - info "Previews will use content from the BSSG site configured by your standard config.sh/config.sh.local files." + if [ -n "$FINAL_CONFIG_OVERRIDE" ]; then + info "Previews will use content from the BSSG site configured by '$FINAL_CONFIG_OVERRIDE'." + else + info "Previews will use content from the BSSG site configured by your standard config.sh/config.sh.local files." + fi for theme in "${themes[@]}"; do info "Building preview for theme: '$theme'" - local theme_site_url="${SITE_URL_BASE%/}/${theme}" # Ensure no double slashes if SITE_URL_BASE ends with / + local theme_site_url="${SITE_URL_BASE%/}/${theme}" local theme_output_path="${EXAMPLE_ROOT_DIR_DYNAMIC}/${theme}" info "Theme Site URL: $theme_site_url" info "Theme Output Path: $theme_output_path" - # Ensure the specific theme's output directory exists mkdir -p "$theme_output_path" - # Run the main bssg.sh build script for this theme. - # It will use the standard BSSG configuration loading (respecting config.sh.local). - # The -f flag forces rebuild, which also clears the active cache for that build. - info "Executing: $BSSG_MAIN_SCRIPT build -f --theme \"$theme\" --site-url \"$theme_site_url\" --output \"$theme_output_path\"" - - if ! "$BSSG_MAIN_SCRIPT" build -f --theme "$theme" --site-url "$theme_site_url" --output "$theme_output_path"; then + if ! run_bssg_build -f --theme "$theme" --site-url "$theme_site_url" --output "$theme_output_path"; then error "Build failed for theme '$theme'. Check output above." fi success "Preview for theme '$theme' built successfully in '$theme_output_path'" @@ -262,19 +390,98 @@ build_previews() { success "All theme previews built." } -# 3. Create an index.html in EXAMPLE_ROOT_DIR_DYNAMIC to navigate themes +has_theme_specific_templates() { + local template_root="$TEMPLATES_DIR" + local theme + for theme in "${themes[@]}"; do + if [ -d "$template_root/$theme" ]; then + if [ -f "$template_root/$theme/header.html" ] || [ -f "$template_root/$theme/footer.html" ]; then + return 0 + fi + fi + done + return 1 +} + +replace_site_url_token_in_output() { + local output_dir="$1" + local replacement_url="$2" + local token="$3" + local escaped_replacement tmp_file file + + escaped_replacement=$(printf '%s' "$replacement_url" | sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g') + + while IFS= read -r -d '' file; do + if LC_ALL=C grep -Fq "$token" "$file"; then + tmp_file="${file}.tmp.$$" + sed "s|${token}|${escaped_replacement}|g" "$file" > "$tmp_file" + mv "$tmp_file" "$file" + fi + done < <(find "$output_dir" -type f \( -name "*.html" -o -name "*.xml" -o -name "*.txt" -o -name "*.css" -o -name "*.json" -o -name "*.js" \) -print0) +} + +clone_base_site_to_theme() { + local base_output_path="$1" + local theme_output_path="$2" + + mkdir -p "$theme_output_path" + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete --exclude='.DS_Store' "${base_output_path}/" "${theme_output_path}/" + else + cp -Rp "${base_output_path}/." "$theme_output_path/" + fi +} + +build_previews_fast() { + local base_theme="default" + local base_output_path="${EXAMPLE_ROOT_DIR_DYNAMIC}/.base-preview" + local theme theme_site_url theme_output_path + + if [ ! -f "${THEMES_DIR}/${base_theme}/style.css" ]; then + base_theme="${themes[0]}" + fi + + info "Building base preview once with theme '$base_theme' and SITE_URL token '$SITE_URL_TOKEN'..." + if ! run_bssg_build -f --theme "$base_theme" --site-url "$SITE_URL_TOKEN" --output "$base_output_path"; then + error "Base build failed in fast preview mode." + fi + + for theme in "${themes[@]}"; do + theme_site_url="${SITE_URL_BASE%/}/${theme}" + theme_output_path="${EXAMPLE_ROOT_DIR_DYNAMIC}/${theme}" + + info "Preparing fast preview for theme '$theme'" + info "Theme Site URL: $theme_site_url" + info "Theme Output Path: $theme_output_path" + + clone_base_site_to_theme "$base_output_path" "$theme_output_path" + + if [ ! -f "${THEMES_DIR}/${theme}/style.css" ]; then + error "style.css not found for theme '$theme' in '${THEMES_DIR}/${theme}'." + fi + cp "${THEMES_DIR}/${theme}/style.css" "${theme_output_path}/css/style.css" + + replace_site_url_token_in_output "$theme_output_path" "$theme_site_url" "$SITE_URL_TOKEN" + + # If precompressed assets were generated in base build, they are now stale after token replacement. + find "$theme_output_path" -type f -name "*.gz" -delete 2>/dev/null || true + + success "Fast preview for theme '$theme' prepared successfully." + done + + rm -rf "$base_output_path" + success "All fast theme previews built." +} + create_index_page() { local index_file="$EXAMPLE_ROOT_DIR_DYNAMIC/index.html" info "Generating index file at '$index_file'..." - # Get current date for the footer local current_date - current_date=$(date) # Use default date format - - # Theme count for display + current_date=$(date) local theme_count=${#themes[@]} - # Use cat heredoc to create the HTML file + # HTML content remains the same, heredoc is portable cat << EOF > "$index_file" <!DOCTYPE html> <html lang="en"> @@ -284,222 +491,82 @@ create_index_page() { <title>BSSG Theme Previews @@ -510,15 +577,13 @@ create_index_page() {
${theme_count} Themes Available

Browse these theme previews using the current site content. Click on any theme to explore its design.

-
EOF - # Add grid items for each theme for theme in "${themes[@]}"; do - local safe_theme_name="${theme//&/&}" - safe_theme_name="${safe_theme_name///>}" + local safe_theme_name="${theme//&/&}" + safe_theme_name="${safe_theme_name///>}" cat << EOF >> "$index_file"
@@ -529,10 +594,8 @@ EOF EOF done - # Close the HTML structure cat << EOF >> "$index_file"
-

Generated on ${current_date}

Base SITE_URL: ${SITE_URL_BASE}

@@ -545,67 +608,68 @@ EOF success "Index file generated successfully with ${theme_count} themes." } -# --- Determine Dynamic EXAMPLE_ROOT_DIR --- determine_example_root_dir() { info "Determining effective site root for EXAMPLE_ROOT_DIR_DYNAMIC..." local project_root_abs - project_root_abs=$(realpath "$(pwd)") # Assuming generate_theme_previews.sh is in BSSG root + # Portable way to get absolute path of current directory + project_root_abs=$( (cd . && pwd -P) || { error "Could not determine project root."; exit 1; } ) - local effective_output_dir - # Use a subshell to source config_loader.sh and get OUTPUT_DIR value - # config_loader.sh can output messages, so redirect its stdout/stderr to /dev/null - # The final echo "$OUTPUT_DIR" will be captured by the command substitution. - # Ensure BSSG_SCRIPT_DIR is exported to the subshell environment. - effective_output_dir=$(export BSSG_SCRIPT_DIR="$project_root_abs"; \ - bash -c 'source "$BSSG_SCRIPT_DIR/scripts/build/config_loader.sh" "" &>/dev/null; echo "$OUTPUT_DIR"') - - if [ -z "$effective_output_dir" ]; then - warn "Could not determine effective OUTPUT_DIR from BSSG configuration. Defaulting EXAMPLE_ROOT_DIR_DYNAMIC to \'$EXAMPLE_ROOT_DIR_DYNAMIC\'." - # EXAMPLE_ROOT_DIR_DYNAMIC remains ./example (its default) + if [ -z "${OUTPUT_DIR:-}" ]; then + warn "Could not determine effective OUTPUT_DIR from BSSG configuration. Defaulting EXAMPLE_ROOT_DIR_DYNAMIC to '$EXAMPLE_ROOT_DIR_DYNAMIC'." return fi - info "Effective OUTPUT_DIR from BSSG configuration: '$effective_output_dir'" + info "Effective OUTPUT_DIR from BSSG configuration: '$OUTPUT_DIR'" - local effective_output_dir_abs - if [[ "$effective_output_dir" == /* ]]; then # Already absolute - effective_output_dir_abs="$effective_output_dir" - else # Relative, resolve it from project_root_abs - effective_output_dir_abs="$project_root_abs/$effective_output_dir" + local effective_output_dir_abs_unnormalized + if [[ "$OUTPUT_DIR" == /* ]]; then + effective_output_dir_abs_unnormalized="$OUTPUT_DIR" + else + effective_output_dir_abs_unnormalized="$project_root_abs/$OUTPUT_DIR" fi - # Normalize the path (remove ., .. if any) - effective_output_dir_abs=$(realpath -m "$effective_output_dir_abs") + + # Normalize the path using our helper (handles ., .., and non-existent paths) + local effective_output_dir_abs + effective_output_dir_abs=$(_normalize_path_string "$effective_output_dir_abs_unnormalized") + info "Normalized effective_output_dir_abs: '$effective_output_dir_abs'" + - # Derive site root from output_dir. Typically output_dir is a direct child of site_root. local site_root_candidate site_root_candidate=$(dirname "$effective_output_dir_abs") + # dirname /foo is / ; dirname / is / + # Ensure site_root_candidate is cleaned up if it's just "//" or similar from dirname + if [[ "$site_root_candidate" != "/" ]]; then + site_root_candidate=$(echo "$site_root_candidate" | sed 's#//*#/#g') + fi - # Check if the site_root_candidate is different from the BSSG project root AND - # if the original effective_output_dir was specified as an absolute path. - # This suggests an external site configuration. - if [[ "$site_root_candidate" != "$project_root_abs" && "$effective_output_dir" == /* ]]; then - info "Detected external site configuration. Previews will be generated in \'$site_root_candidate/example\'." + + if [[ "$site_root_candidate" != "$project_root_abs" && "$OUTPUT_DIR" == /* ]]; then + info "Detected external site configuration. Previews will be generated in '$site_root_candidate/example'." EXAMPLE_ROOT_DIR_DYNAMIC="$site_root_candidate/example" else - info "Using BSSG project directory for previews. Previews will be generated in \'$project_root_abs/example\'." - EXAMPLE_ROOT_DIR_DYNAMIC="$project_root_abs/example" # Ensures absolute path for clarity + info "Using BSSG project directory for previews. Previews will be generated in '$project_root_abs/example'." + EXAMPLE_ROOT_DIR_DYNAMIC="$project_root_abs/example" fi - success "EXAMPLE_ROOT_DIR_DYNAMIC set to \'$EXAMPLE_ROOT_DIR_DYNAMIC\'." + # Normalize the final EXAMPLE_ROOT_DIR_DYNAMIC as well + EXAMPLE_ROOT_DIR_DYNAMIC=$(_normalize_path_string "$EXAMPLE_ROOT_DIR_DYNAMIC") + success "EXAMPLE_ROOT_DIR_DYNAMIC set to '$EXAMPLE_ROOT_DIR_DYNAMIC'." } -# --- Script Execution --- main() { - parse_args "$@" - load_config # Load SITE_URL_BASE for previews - check_dependencies - determine_example_root_dir # Determine the correct EXAMPLE_ROOT_DIR_DYNAMIC + # Ensure global 'themes' array is declared if not implicitly through find_themes + declare -a themes - find_themes + parse_args "$@" + resolve_config_override + load_config + check_dependencies + determine_example_root_dir + + find_themes # Populates global 'themes' array build_previews create_index_page - success "Theme previews generated successfully in \'$EXAMPLE_ROOT_DIR_DYNAMIC\'" - info "Open \'$EXAMPLE_ROOT_DIR_DYNAMIC/index.html\' in your browser to view them." + success "Theme previews generated successfully in '$EXAMPLE_ROOT_DIR_DYNAMIC'" + info "Open '$EXAMPLE_ROOT_DIR_DYNAMIC/index.html' in your browser to view them." } -# Call main function with all script arguments main "$@" diff --git a/locales/de.sh b/locales/de.sh index 66d5a00..5b6e572 100644 --- a/locales/de.sh +++ b/locales/de.sh @@ -3,14 +3,17 @@ export MSG_HOME="Startseite" export MSG_TAGS="Tags" +export MSG_AUTHORS="Autoren" export MSG_ARCHIVES="Archive" export MSG_RSS="RSS" export MSG_PAGES="Seiten" export MSG_SUBSCRIBE_RSS="Per RSS abonnieren" export MSG_PUBLISHED_ON="Veröffentlicht am" export MSG_BY="von" +export MSG_POSTS_BY="Beiträge von" export MSG_TAG_PAGE_TITLE="Beiträge mit dem Tag" export MSG_ALL_TAGS="Alle Tags" +export MSG_ALL_AUTHORS="Alle Autoren" export MSG_ALL_PAGES="Alle Seiten" export MSG_ARCHIVES_FOR="Archive für" export MSG_BACK_TO="Zurück zu" @@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d Min. Lesezeit" export MSG_MINUTE="Minute" export MSG_MINUTES="Minuten" export MSG_UPDATED_ON="Aktualisiert am" -export MSG_BACK_TO_TOP="Nach oben" \ No newline at end of file +export MSG_BACK_TO_TOP="Nach oben" +export MSG_RELATED_POSTS="Ähnliche Beiträge" \ No newline at end of file diff --git a/locales/en.sh b/locales/en.sh index 8652166..9b15347 100644 --- a/locales/en.sh +++ b/locales/en.sh @@ -3,14 +3,17 @@ export MSG_HOME="Home" export MSG_TAGS="Tags" +export MSG_AUTHORS="Authors" export MSG_ARCHIVES="Archives" export MSG_RSS="RSS" export MSG_PAGES="Pages" export MSG_SUBSCRIBE_RSS="Subscribe via RSS" export MSG_PUBLISHED_ON="Published on" export MSG_BY="by" +export MSG_POSTS_BY="Posts by" export MSG_TAG_PAGE_TITLE="Posts tagged with" export MSG_ALL_TAGS="All Tags" +export MSG_ALL_AUTHORS="All Authors" export MSG_ALL_PAGES="All Pages" export MSG_ARCHIVES_FOR="Archives for" export MSG_BACK_TO="Back to" @@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min read" export MSG_MINUTE="minute" export MSG_MINUTES="minutes" export MSG_UPDATED_ON="Updated on" -export MSG_BACK_TO_TOP="Back to Top" \ No newline at end of file +export MSG_BACK_TO_TOP="Back to Top" +export MSG_RELATED_POSTS="Related Posts" \ No newline at end of file diff --git a/locales/es.sh b/locales/es.sh index 9149f23..33e6f33 100644 --- a/locales/es.sh +++ b/locales/es.sh @@ -3,14 +3,17 @@ export MSG_HOME="Inicio" export MSG_TAGS="Etiquetas" +export MSG_AUTHORS="Autores" export MSG_ARCHIVES="Archivos" export MSG_RSS="RSS" export MSG_PAGES="Páginas" export MSG_SUBSCRIBE_RSS="Suscribirse vía RSS" export MSG_PUBLISHED_ON="Publicado el" export MSG_BY="por" +export MSG_POSTS_BY="Entradas de" export MSG_TAG_PAGE_TITLE="Entradas etiquetadas con" export MSG_ALL_TAGS="Todas las etiquetas" +export MSG_ALL_AUTHORS="Todos los autores" export MSG_ALL_PAGES="Todas las páginas" export MSG_ARCHIVES_FOR="Archivos de" export MSG_BACK_TO="Volver a" @@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min de lectura" export MSG_MINUTE="minuto" export MSG_MINUTES="minutos" export MSG_UPDATED_ON="Actualizado el" -export MSG_BACK_TO_TOP="Volver arriba" \ No newline at end of file +export MSG_BACK_TO_TOP="Volver arriba" +export MSG_RELATED_POSTS="Artículos relacionados" \ No newline at end of file diff --git a/locales/fr.sh b/locales/fr.sh index 4e71e26..8d88750 100644 --- a/locales/fr.sh +++ b/locales/fr.sh @@ -3,14 +3,17 @@ export MSG_HOME="Accueil" export MSG_TAGS="Étiquettes" +export MSG_AUTHORS="Auteurs" export MSG_ARCHIVES="Archives" export MSG_RSS="RSS" export MSG_PAGES="Pages" export MSG_SUBSCRIBE_RSS="S'abonner via RSS" export MSG_PUBLISHED_ON="Publié le" export MSG_BY="par" +export MSG_POSTS_BY="Articles de" export MSG_TAG_PAGE_TITLE="Articles étiquetés avec" export MSG_ALL_TAGS="Toutes les étiquettes" +export MSG_ALL_AUTHORS="Tous les auteurs" export MSG_ALL_PAGES="Toutes les pages" export MSG_ARCHIVES_FOR="Archives pour" export MSG_BACK_TO="Retour à" @@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min de lecture" export MSG_MINUTE="minute" export MSG_MINUTES="minutes" export MSG_UPDATED_ON="Mis à jour le" -export MSG_BACK_TO_TOP="Retour en haut" \ No newline at end of file +export MSG_BACK_TO_TOP="Retour en haut" +export MSG_RELATED_POSTS="Articles connexes" \ No newline at end of file diff --git a/locales/it.sh b/locales/it.sh index 501b34b..04f0169 100644 --- a/locales/it.sh +++ b/locales/it.sh @@ -3,14 +3,17 @@ export MSG_HOME="Home" export MSG_TAGS="Tag" +export MSG_AUTHORS="Autori" export MSG_ARCHIVES="Archivi" export MSG_RSS="RSS" export MSG_PAGES="Pagine" export MSG_SUBSCRIBE_RSS="Abbonati via RSS" export MSG_PUBLISHED_ON="Pubblicato il" export MSG_BY="da" +export MSG_POSTS_BY="Articoli di" export MSG_TAG_PAGE_TITLE="Articoli taggati con" export MSG_ALL_TAGS="Tutti i tag" +export MSG_ALL_AUTHORS="Tutti gli autori" export MSG_ALL_PAGES="Tutte le pagine" export MSG_ARCHIVES_FOR="Archivi per" export MSG_BACK_TO="Torna a" @@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min di lettura" export MSG_MINUTE="minuto" export MSG_MINUTES="minuti" export MSG_UPDATED_ON="Aggiornato il" -export MSG_BACK_TO_TOP="Torna su" \ No newline at end of file +export MSG_BACK_TO_TOP="Torna in cima" +export MSG_RELATED_POSTS="Articoli correlati" \ No newline at end of file diff --git a/locales/ja.sh b/locales/ja.sh index bbfcda0..6c14574 100644 --- a/locales/ja.sh +++ b/locales/ja.sh @@ -3,14 +3,17 @@ export MSG_HOME="ホーム" export MSG_TAGS="タグ" +export MSG_AUTHORS="著者" export MSG_ARCHIVES="アーカイブ" export MSG_RSS="RSS" export MSG_PAGES="ページ" export MSG_SUBSCRIBE_RSS="RSSで購読する" export MSG_PUBLISHED_ON="公開日" export MSG_BY="作成者" +export MSG_POSTS_BY="の投稿" export MSG_TAG_PAGE_TITLE="タグ付きの投稿" export MSG_ALL_TAGS="すべてのタグ" +export MSG_ALL_AUTHORS="すべての著者" export MSG_ALL_PAGES="すべてのページ" export MSG_ARCHIVES_FOR="のアーカイブ" export MSG_BACK_TO="に戻る" @@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="読了時間 %d分" export MSG_MINUTE="分" export MSG_MINUTES="分" export MSG_UPDATED_ON="更新日" -export MSG_BACK_TO_TOP="トップに戻る" \ No newline at end of file +export MSG_BACK_TO_TOP="トップに戻る" +export MSG_RELATED_POSTS="関連記事" \ No newline at end of file diff --git a/locales/nl.sh b/locales/nl.sh new file mode 100644 index 0000000..aee094f --- /dev/null +++ b/locales/nl.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Dutch Locale for BSSG + +export MSG_HOME="Home" +export MSG_TAGS="Tags" +export MSG_AUTHORS="Autheurs" +export MSG_ARCHIVES="Archieven" +export MSG_RSS="RSS" +export MSG_PAGES="Paginas" +export MSG_SUBSCRIBE_RSS="Volg via RSS" +export MSG_PUBLISHED_ON="Gepubliceerd op" +export MSG_BY="door" +export MSG_POSTS_BY="Posts door" +export MSG_TAG_PAGE_TITLE="Posts getagd met" +export MSG_ALL_TAGS="All Tags" +export MSG_ALL_AUTHORS="Alle Authors" +export MSG_ALL_PAGES="All Paginas" +export MSG_ARCHIVES_FOR="Archiven voor" +export MSG_BACK_TO="Terug naar" +export MSG_POSTS_FROM="Posts van" +export MSG_OLDER_POSTS="Oudere Posts" +export MSG_NEWER_POSTS="Nieuwere Posts" +export MSG_PAGE_INFO_TEMPLATE="Pagina %d van %d" +export MSG_PAGE_TITLE_PREFIX="Pagina" +export MSG_RSS_FEED_TITLE="${SITE_TITLE} - RSS Feed" +export MSG_RSS_FEED_DESCRIPTION="${SITE_DESCRIPTION}" +export MSG_RSS_FEED="RSS Feed" +export MSG_ALL_RIGHTS_RESERVED="Alle rechten reserved." +export MSG_GENERATED_WITH="Deze site was gegenereerd met" +export MSG_LATEST_POSTS="Laatste Posts" +export MSG_GENERATOR_DESCRIPTION="." +export MSG_POSTS="posts" +export MSG_READ_MORE="Lees meer" +export MSG_MONTH_01="Januari" +export MSG_MONTH_02="Februari" +export MSG_MONTH_03="Maart" +export MSG_MONTH_04="April" +export MSG_MONTH_05="Mei" +export MSG_MONTH_06="Juni" +export MSG_MONTH_07="Juli" +export MSG_MONTH_08="Augustus" +export MSG_MONTH_09="September" +export MSG_MONTH_10="October" +export MSG_MONTH_11="November" +export MSG_MONTH_12="December" +export MSG_READING_TIME_TEMPLATE="%d min read" +export MSG_MINUTE="minuut" +export MSG_MINUTES="minuten" +export MSG_UPDATED_ON="Bijgewerkt op" +export MSG_BACK_TO_TOP="Terug naar Boven" +export MSG_RELATED_POSTS="Gerelateede Posts" \ No newline at end of file diff --git a/locales/pt.sh b/locales/pt.sh index baa89a3..5c0f7d5 100644 --- a/locales/pt.sh +++ b/locales/pt.sh @@ -3,14 +3,17 @@ export MSG_HOME="Início" export MSG_TAGS="Etiquetas" +export MSG_AUTHORS="Autores" export MSG_ARCHIVES="Arquivos" export MSG_RSS="RSS" export MSG_PAGES="Páginas" export MSG_SUBSCRIBE_RSS="Subscrever via RSS" export MSG_PUBLISHED_ON="Publicado em" export MSG_BY="por" +export MSG_POSTS_BY="Posts de" export MSG_TAG_PAGE_TITLE="Posts etiquetados com" export MSG_ALL_TAGS="Todas as Etiquetas" +export MSG_ALL_AUTHORS="Todos os Autores" export MSG_ALL_PAGES="Todas as Páginas" export MSG_ARCHIVES_FOR="Arquivos de" export MSG_BACK_TO="Voltar para" @@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min de leitura" export MSG_MINUTE="minuto" export MSG_MINUTES="minutos" export MSG_UPDATED_ON="Atualizado em" -export MSG_BACK_TO_TOP="Voltar ao topo" \ No newline at end of file +export MSG_BACK_TO_TOP="Voltar ao topo" +export MSG_RELATED_POSTS="Posts Relacionados" \ No newline at end of file diff --git a/locales/zh.sh b/locales/zh.sh index b00fbbc..26a491e 100644 --- a/locales/zh.sh +++ b/locales/zh.sh @@ -3,14 +3,17 @@ export MSG_HOME="首页" export MSG_TAGS="标签" +export MSG_AUTHORS="作者" export MSG_ARCHIVES="归档" export MSG_RSS="RSS" export MSG_PAGES="页面" export MSG_SUBSCRIBE_RSS="通过 RSS 订阅" export MSG_PUBLISHED_ON="发布于" export MSG_BY="作者" +export MSG_POSTS_BY="的文章" export MSG_TAG_PAGE_TITLE="标签为 的文章" export MSG_ALL_TAGS="所有标签" +export MSG_ALL_AUTHORS="所有作者" export MSG_ALL_PAGES="所有页面" export MSG_ARCHIVES_FOR="的归档" export MSG_BACK_TO="返回" @@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="阅读时间 %d 分钟" export MSG_MINUTE="分钟" export MSG_MINUTES="分钟" export MSG_UPDATED_ON="更新于" -export MSG_BACK_TO_TOP="返回顶部" \ No newline at end of file +export MSG_BACK_TO_TOP="返回顶部" +export MSG_RELATED_POSTS="相关文章" \ No newline at end of file diff --git a/scripts/bssg.sh b/scripts/bssg.sh index c9ea3e9..4f38a52 100755 --- a/scripts/bssg.sh +++ b/scripts/bssg.sh @@ -99,17 +99,24 @@ fi # Terminal colors (still needed here if config_loader doesn't export them, though it should) # These are now primarily set and exported by config_loader.sh based on config files. # The ':-' syntax provides a fallback if they somehow aren't set, using tput. -RED="${RED:-$(tput setaf 1)}" -GREEN="${GREEN:-$(tput setaf 2)}" -YELLOW="${YELLOW:-$(tput setaf 3)}" -NC="${NC:-$(tput sgr0)}" # Reset color +if [[ -t 1 ]] && command -v tput > /dev/null 2>&1 && tput setaf 1 > /dev/null 2>&1; then + RED="${RED:-$(tput setaf 1)}" + GREEN="${GREEN:-$(tput setaf 2)}" + YELLOW="${YELLOW:-$(tput setaf 3)}" + NC="${NC:-$(tput sgr0)}" # Reset color +else + RED="${RED:-}" + GREEN="${GREEN:-}" + YELLOW="${YELLOW:-}" + NC="${NC:-}" +fi # Make sure all scripts are executable chmod +x scripts/*.sh 2>/dev/null || true # Function to display help information show_help() { - echo "BSSG - Bash Static Site Generator (v0.16)" + echo "BSSG - Bash Static Site Generator (v0.33)" echo "=========================================" echo "" echo "Usage: $0 [--config ] command [options]" @@ -163,6 +170,7 @@ show_build_help() { echo " --static DIR Override Static directory (from config: ${STATIC_DIR:-static})" echo " --clean-output [bool] Clean output directory before building (default from config: ${CLEAN_OUTPUT:-false})" echo " --force-rebuild, -f Force rebuild of all files regardless of modification time" + echo " --build-mode MODE Build mode: normal or ram (default from config: ${BUILD_MODE:-normal})" echo " --site-title TITLE Override Site title" echo " --site-url URL Override Site URL" echo " --site-description DESC Override Site description" @@ -210,6 +218,9 @@ main() { command="$1" shift # Consume the command itself + # expand variables such as POSTS_DIR, PAGES_DIR embedded in the command-line + set -- $(eval echo "$@") + case "$command" in post) scripts/post.sh "$@" @@ -294,10 +305,26 @@ main() { shift 1 fi ;; - -f) + -f|--force-rebuild) export FORCE_REBUILD=true shift 1 ;; + --build-mode) + if [[ -z "$2" || "$2" == -* ]]; then + echo -e "${RED}Error: --build-mode requires a value (normal|ram).${NC}" >&2 + exit 1 + fi + case "$2" in + normal|ram) + export BUILD_MODE="$2" + ;; + *) + echo -e "${RED}Error: Invalid --build-mode '$2'. Use 'normal' or 'ram'.${NC}" >&2 + exit 1 + ;; + esac + shift 2 + ;; --site-title) export SITE_TITLE="$2" shift 2 diff --git a/scripts/build/cache.sh b/scripts/build/cache.sh index 11ab1fd..8836073 100755 --- a/scripts/build/cache.sh +++ b/scripts/build/cache.sh @@ -184,6 +184,12 @@ clean_stale_cache() { # Also remove tag and archive pages to force their regeneration find "${OUTPUT_DIR:-output}/tags" -name "*.html" -type f -delete 2>/dev/null || true find "${OUTPUT_DIR:-output}/archives" -name "*.html" -type f -delete 2>/dev/null || true + + # Clean related posts cache when posts are removed + if [ -d "${CACHE_DIR}/related_posts" ]; then + echo -e "${YELLOW}Cleaning related posts cache due to post removal...${NC}" + rm -rf "${CACHE_DIR}/related_posts" + fi fi echo -e "${GREEN}Cache cleaned!${NC}" diff --git a/scripts/build/config_loader.sh b/scripts/build/config_loader.sh index a9e6815..244c6b2 100755 --- a/scripts/build/config_loader.sh +++ b/scripts/build/config_loader.sh @@ -24,6 +24,11 @@ SITE_DESCRIPTION="${SITE_DESCRIPTION:-A personal journal and introspective newsp SITE_URL="${SITE_URL:-http://localhost}" AUTHOR_NAME="${AUTHOR_NAME:-Anonymous}" AUTHOR_EMAIL="${AUTHOR_EMAIL:-anonymous@example.com}" +REL_ME_URL="${REL_ME_URL:-}" +REL_ME_URLS_SERIALIZED="${REL_ME_URLS_SERIALIZED:-}" +FEDIVERSE_CREATOR="${FEDIVERSE_CREATOR:-}" +AUTHOR_FEDIVERSE_CREATORS_SERIALIZED="${AUTHOR_FEDIVERSE_CREATORS_SERIALIZED:-}" +SITE_FEDIVERSE_CREATOR_META_TAG="${SITE_FEDIVERSE_CREATOR_META_TAG:-}" DATE_FORMAT="${DATE_FORMAT:-%Y-%m-%d %H:%M:%S}" TIMEZONE="${TIMEZONE:-local}" SHOW_TIMEZONE="${SHOW_TIMEZONE:-false}" @@ -31,8 +36,10 @@ POSTS_PER_PAGE="${POSTS_PER_PAGE:-10}" RSS_ITEM_LIMIT="${RSS_ITEM_LIMIT:-15}" # Default RSS item limit RSS_INCLUDE_FULL_CONTENT="${RSS_INCLUDE_FULL_CONTENT:-false}" # Default RSS full content RSS_FILENAME="${RSS_FILENAME:-rss.xml}" # Default RSS filename +INDEX_SHOW_FULL_CONTENT="${INDEX_SHOW_FULL_CONTENT:-false}" # Default: show excerpt on homepage CLEAN_OUTPUT="${CLEAN_OUTPUT:-false}" FORCE_REBUILD="${FORCE_REBUILD:-false}" +BUILD_MODE="${BUILD_MODE:-normal}" # Build mode: normal or ram SITE_LANG="${SITE_LANG:-en}" LOCALE_DIR="${LOCALE_DIR:-locales}" PAGES_DIR="${PAGES_DIR:-pages}" @@ -42,6 +49,13 @@ ENABLE_ARCHIVES="${ENABLE_ARCHIVES:-true}" URL_SLUG_FORMAT="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" PAGE_URL_FORMAT="${PAGE_URL_FORMAT:-slug}" ENABLE_TAG_RSS="${ENABLE_TAG_RSS:-false}" # Generate RSS feed for each tag +ENABLE_AUTHOR_PAGES="${ENABLE_AUTHOR_PAGES:-true}" # Generate author index pages +ENABLE_AUTHOR_RSS="${ENABLE_AUTHOR_RSS:-false}" # Generate RSS feed for each author +SHOW_AUTHORS_MENU_THRESHOLD="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" # Minimum authors to show menu + +# Related Posts Configuration Defaults +ENABLE_RELATED_POSTS="${ENABLE_RELATED_POSTS:-true}" # Enable or disable related posts feature +RELATED_POSTS_COUNT="${RELATED_POSTS_COUNT:-3}" # Number of related posts to show # --- Backup Directory --- Added --- BACKUP_DIR="${BACKUP_DIR:-backup}" # Default backup location @@ -54,11 +68,19 @@ BSSG_SERVER_HOST_DEFAULT="${BSSG_SERVER_HOST_DEFAULT:-localhost}" CUSTOM_CSS="${CUSTOM_CSS:-}" # Default to empty string # Define default colors here so utils.sh can use them if not overridden by config -RED="${RED:-$(tput setaf 1)}" -GREEN="${GREEN:-$(tput setaf 2)}" -YELLOW="${YELLOW:-$(tput setaf 3)}" -BLUE="${BLUE:-$(tput setaf 4)}" # Added Blue for print_info, using tput -NC="${NC:-$(tput sgr0)}" # No Color, using tput +if [[ -t 1 ]] && command -v tput > /dev/null 2>&1 && tput setaf 1 > /dev/null 2>&1; then + RED="${RED:-$(tput setaf 1)}" + GREEN="${GREEN:-$(tput setaf 2)}" + YELLOW="${YELLOW:-$(tput setaf 3)}" + BLUE="${BLUE:-$(tput setaf 4)}" + NC="${NC:-$(tput sgr0)}" +else + RED="${RED:-}" + GREEN="${GREEN:-}" + YELLOW="${YELLOW:-}" + BLUE="${BLUE:-}" + NC="${NC:-}" +fi # --- Default Configuration Variables --- END --- @@ -78,10 +100,17 @@ if [ -f "$UTILS_SCRIPT" ]; then else # Define basic color functions as fallback if utils.sh is missing # Needed for messages printed *before* utils.sh is sourced, or if it fails. - RED='\033[0;31m' - GREEN='\033[0;32m' - YELLOW='\033[0;33m' - NC='\033[0m' # No Color + if [[ -t 1 ]] && [[ -z $NO_COLOR ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + NC='\033[0m' # No Color + else + RED="" + GREEN="" + YELLOW="" + NC="" + fi print_error() { echo -e "${RED}Error: $1${NC}" >&2; } print_warning() { echo -e "${YELLOW}Warning: $1${NC}"; } print_success() { echo -e "${GREEN}$1${NC}"; } @@ -194,6 +223,68 @@ done # --- Expand Tilde in Path Variables --- END --- +# --- Derived Configuration --- START --- +serialize_author_fediverse_creators() { + local serialized="" + local author_name + + if ! declare -p AUTHOR_FEDIVERSE_CREATORS >/dev/null 2>&1; then + printf '%s' "$serialized" + return 0 + fi + + if [[ "$(declare -p AUTHOR_FEDIVERSE_CREATORS 2>/dev/null)" != "declare -A"* ]]; then + print_warning "AUTHOR_FEDIVERSE_CREATORS is set but is not an associative array. Ignoring it." + printf '%s' "$serialized" + return 0 + fi + + while IFS= read -r author_name; do + serialized+="${author_name}"$'\t'"${AUTHOR_FEDIVERSE_CREATORS[$author_name]}"$'\n' + done < <(printf '%s\n' "${!AUTHOR_FEDIVERSE_CREATORS[@]}" | LC_ALL=C sort) + + printf '%s' "$serialized" +} + +serialize_rel_me_urls() { + local serialized="" + local rel_me_url="" + + rel_me_url=$(printf '%s' "$REL_ME_URL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [ -n "$rel_me_url" ]; then + serialized+="${rel_me_url}"$'\n' + fi + + if declare -p REL_ME_URLS >/dev/null 2>&1; then + local rel_me_decl + rel_me_decl="$(declare -p REL_ME_URLS 2>/dev/null)" + if [[ "$rel_me_decl" == "declare -a"* ]]; then + local rel_me_entry + for rel_me_entry in "${REL_ME_URLS[@]}"; do + rel_me_entry=$(printf '%s' "$rel_me_entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [ -n "$rel_me_entry" ]; then + serialized+="${rel_me_entry}"$'\n' + fi + done + else + print_warning "REL_ME_URLS is set but is not a standard array. Ignoring it." + fi + fi + + if [ -z "$serialized" ]; then + printf '%s' "" + return 0 + fi + + printf '%s' "$serialized" | awk 'NF && !seen[$0]++' +} + +REL_ME_URLS_SERIALIZED="$(serialize_rel_me_urls)" +AUTHOR_FEDIVERSE_CREATORS_SERIALIZED="$(serialize_author_fediverse_creators)" +SITE_FEDIVERSE_CREATOR_META_TAG="$(build_fediverse_creator_meta_tag "${AUTHOR_NAME:-Anonymous}" "")" +# --- Derived Configuration --- END --- + + # --- Export All Variables --- START --- # Define the list of configuration variables relevant for hashing/exporting @@ -201,16 +292,20 @@ done # and that should trigger a cache rebuild if changed. BSSG_CONFIG_VARS_ARRAY=( CONFIG_FILE SRC_DIR OUTPUT_DIR TEMPLATES_DIR THEMES_DIR STATIC_DIR THEME - SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME AUTHOR_EMAIL + SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME AUTHOR_EMAIL REL_ME_URL REL_ME_URLS_SERIALIZED + FEDIVERSE_CREATOR AUTHOR_FEDIVERSE_CREATORS_SERIALIZED SITE_FEDIVERSE_CREATOR_META_TAG DATE_FORMAT TIMEZONE SHOW_TIMEZONE POSTS_PER_PAGE RSS_ITEM_LIMIT RSS_INCLUDE_FULL_CONTENT RSS_FILENAME - CLEAN_OUTPUT FORCE_REBUILD SITE_LANG LOCALE_DIR PAGES_DIR MARKDOWN_PROCESSOR + INDEX_SHOW_FULL_CONTENT + CLEAN_OUTPUT FORCE_REBUILD BUILD_MODE SITE_LANG LOCALE_DIR PAGES_DIR MARKDOWN_PROCESSOR MARKDOWN_PL_PATH ENABLE_ARCHIVES URL_SLUG_FORMAT PAGE_URL_FORMAT DRAFTS_DIR REBUILD_AFTER_POST REBUILD_AFTER_EDIT CUSTOM_CSS - ENABLE_TAG_RSS - BACKUP_DIR CACHE_DIR - DEPLOY_AFTER_BUILD DEPLOY_SCRIPT + ENABLE_TAG_RSS ENABLE_AUTHOR_PAGES ENABLE_AUTHOR_RSS SHOW_AUTHORS_MENU_THRESHOLD + BACKUP_DIR CACHE_DIR + DEPLOY_AFTER_BUILD DEPLOY_SCRIPT ARCHIVES_LIST_ALL_POSTS + ENABLE_RELATED_POSTS RELATED_POSTS_COUNT + PRECOMPRESS_ASSETS # Add any other custom config variables here if needed BSSG_SERVER_PORT_DEFAULT BSSG_SERVER_HOST_DEFAULT # Server defaults ) @@ -233,6 +328,11 @@ export SITE_DESCRIPTION export SITE_URL export AUTHOR_NAME export AUTHOR_EMAIL +export REL_ME_URL +export REL_ME_URLS_SERIALIZED +export FEDIVERSE_CREATOR +export AUTHOR_FEDIVERSE_CREATORS_SERIALIZED +export SITE_FEDIVERSE_CREATOR_META_TAG export DATE_FORMAT export TIMEZONE export SHOW_TIMEZONE @@ -240,8 +340,10 @@ export POSTS_PER_PAGE export RSS_ITEM_LIMIT export RSS_INCLUDE_FULL_CONTENT export RSS_FILENAME +export INDEX_SHOW_FULL_CONTENT export CLEAN_OUTPUT export FORCE_REBUILD +export BUILD_MODE export SITE_LANG export LOCALE_DIR export PAGES_DIR @@ -255,11 +357,17 @@ export REBUILD_AFTER_POST export REBUILD_AFTER_EDIT export CUSTOM_CSS export ENABLE_TAG_RSS +export ENABLE_AUTHOR_PAGES +export ENABLE_AUTHOR_RSS +export SHOW_AUTHORS_MENU_THRESHOLD export BACKUP_DIR -export CACHE_DIR +export CACHE_DIR export DEPLOY_AFTER_BUILD -export DEPLOY_SCRIPT +export DEPLOY_SCRIPT export ARCHIVES_LIST_ALL_POSTS +export ENABLE_RELATED_POSTS +export RELATED_POSTS_COUNT +export PRECOMPRESS_ASSETS # Server defaults export export BSSG_SERVER_PORT_DEFAULT @@ -286,4 +394,9 @@ export MSG_MONTH_09 MSG_MONTH_10 MSG_MONTH_11 MSG_MONTH_12 # Fallback using compgen (use with caution, might export unintended vars) # compgen -v MSG_ | while read -r var; do export "$var"; done -# --- Export All Variables --- END --- \ No newline at end of file +# --- Export All Variables --- END --- + +# --- Final Path Adjustments (after all sourcing) --- START --- +# Ensure relevant directory paths are exported if not already absolute. +# ... existing code ... +# --- Final Path Adjustments (after all sourcing) --- END --- diff --git a/scripts/build/content.sh b/scripts/build/content.sh index b3d28ea..458c6e9 100755 --- a/scripts/build/content.sh +++ b/scripts/build/content.sh @@ -14,10 +14,35 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s parse_metadata() { local file="$1" local field="$2" + local value="" + + # Ignore empty or directory inputs so callers can safely scan optional lists. + if [[ -z "$file" || -d "$file" ]]; then + echo "$value" + return 0 + fi + + # RAM mode: parse directly from preloaded content to avoid disk/cache I/O. + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then + local file_content frontmatter + file_content=$(ram_mode_get_content "$file") + frontmatter=$(printf '%s\n' "$file_content" | awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { exit; } + } + in_fm { print; } + ') + if [ -n "$frontmatter" ]; then + value=$(printf '%s\n' "$frontmatter" | grep -m 1 "^$field:[[:space:]]*" | cut -d ':' -f 2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + fi + echo "$value" + return 0 + fi # IMPORTANT: Assumes CACHE_DIR is exported/available local cache_file="${CACHE_DIR:-.bssg_cache}/meta/$(basename "$file")" - local value="" # Get locks for cache access # IMPORTANT: Assumes lock_file/unlock_file are sourced/available @@ -68,11 +93,20 @@ parse_metadata() { # Extract metadata from markdown file (builds cache) extract_metadata() { local file="$1" + if [[ -z "$file" || -d "$file" ]]; then + echo "ERROR_FILE_NOT_FOUND" + return 1 + fi + local metadata_cache_file="${CACHE_DIR:-.bssg_cache}/meta/$(basename "$file")" local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" + local ram_mode_active=false + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then + ram_mode_active=true + fi # Check if file exists - if [ ! -f "$file" ]; then + if ! $ram_mode_active && [ ! -f "$file" ]; then echo "ERROR_FILE_NOT_FOUND" return 1 fi @@ -81,7 +115,7 @@ extract_metadata() { local frontmatter_changed=false # Check if cache exists and is newer than the source file - if [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$metadata_cache_file" ] && [ "$metadata_cache_file" -nt "$file" ]; then + if ! $ram_mode_active && [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$metadata_cache_file" ] && [ "$metadata_cache_file" -nt "$file" ]; then # Read from cache file (optimized - read once) echo "$(cat "$metadata_cache_file")" return 0 @@ -91,30 +125,46 @@ extract_metadata() { fi # If we're here, we need to parse the file - local title="" date="" lastmod="" tags="" slug="" image="" image_caption="" description="" + local title="" date="" lastmod="" tags="" slug="" image="" image_caption="" description="" author_name="" author_email="" # Check file type and parse accordingly if [[ "$file" == *.html ]]; then # Parse tags for HTML files # Use grep -m 1 for efficiency, handle missing tags gracefully # Note: This is basic parsing, assumes simple meta tag structure. - title=$(grep -m 1 -o '[^<]*' "$file" 2>/dev/null | sed -e 's///' -e 's/<\/title>//') - date=$(grep -m 1 -o 'name="date" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - lastmod=$(grep -m 1 -o 'name="lastmod" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - tags=$(grep -m 1 -o 'name="tags" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - slug=$(grep -m 1 -o 'name="slug" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - image=$(grep -m 1 -o 'name="image" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - image_caption=$(grep -m 1 -o 'name="image_caption" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - description=$(grep -m 1 -o 'name="description" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + local html_source="" + if $ram_mode_active; then + html_source=$(ram_mode_get_content "$file") + title=$(printf '%s\n' "$html_source" | grep -m 1 -o '<title>[^<]*' 2>/dev/null | sed -e 's///' -e 's/<\/title>//') + date=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="date" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + lastmod=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="lastmod" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + tags=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="tags" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + slug=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="slug" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + image=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="image" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + image_caption=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="image_caption" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + description=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="description" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_name=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="author_name" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_email=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="author_email" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + else + title=$(grep -m 1 -o '<title>[^<]*' "$file" 2>/dev/null | sed -e 's///' -e 's/<\/title>//') + date=$(grep -m 1 -o 'name="date" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + lastmod=$(grep -m 1 -o 'name="lastmod" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + tags=$(grep -m 1 -o 'name="tags" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + slug=$(grep -m 1 -o 'name="slug" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + image=$(grep -m 1 -o 'name="image" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + image_caption=$(grep -m 1 -o 'name="image_caption" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + description=$(grep -m 1 -o 'name="description" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_name=$(grep -m 1 -o 'name="author_name" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_email=$(grep -m 1 -o 'name="author_email" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + fi # Note: Excerpt generation (fallback for description) might not work well for HTML elif [[ "$file" == *.md ]]; then # Parse YAML frontmatter for Markdown files - # Use awk with a here document for reliable script passing - - # Run awk and read results + # Use a shared awk parser for both disk and RAM paths. local parsed_data - parsed_data=$(awk -f - "$file" <<'EOF' + local awk_frontmatter_parser + awk_frontmatter_parser=$(cat <<'EOF' BEGIN { in_fm = 0; found_fm = 0; @@ -122,6 +172,7 @@ extract_metadata() { vars["title"] = ""; vars["date"] = ""; vars["lastmod"] = ""; vars["tags"] = ""; vars["slug"] = ""; vars["image"] = ""; vars["image_caption"] = ""; vars["description"] = ""; + vars["author_name"] = ""; vars["author_email"] = ""; } /^---$/ { if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } @@ -154,12 +205,19 @@ extract_metadata() { # Print values in specific order print vars["title"] "|" vars["date"] "|" vars["lastmod"] "|" \ vars["tags"] "|" vars["slug"] "|" vars["image"] "|" \ - vars["image_caption"] "|" vars["description"]; + vars["image_caption"] "|" vars["description"] "|" \ + vars["author_name"] "|" vars["author_email"]; } EOF ) + + if $ram_mode_active; then + parsed_data=$(printf '%s\n' "$(ram_mode_get_content "$file")" | awk "$awk_frontmatter_parser") + else + parsed_data=$(awk "$awk_frontmatter_parser" "$file") + fi - IFS='|' read -r title date lastmod tags slug image image_caption description <<< "$parsed_data" + IFS='|' read -r title date lastmod tags slug image image_caption description author_name author_email <<< "$parsed_data" else echo "Warning: Unknown file type '$file' for metadata extraction." >&2 @@ -188,12 +246,22 @@ EOF echo "[DEBUG] Generating excerpt for $file" >&2 description=$(generate_excerpt "$file") fi + + # Apply fallback logic for author fields + if [ -z "$author_name" ]; then + author_name="${AUTHOR_NAME:-Anonymous}" + fi + if [ -z "$author_email" ] && [ -n "$author_name" ] && [ "$author_name" = "${AUTHOR_NAME:-Anonymous}" ]; then + # Only use default email if using default name + author_email="${AUTHOR_EMAIL:-anonymous@example.com}" + fi + # If author_name is specified but author_email is empty, leave email empty # Construct the metadata string for comparison and caching - local new_metadata="$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description" + local new_metadata="$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description|$author_name|$author_email" # Check if there was a previous metadata file and compare - if [ -f "$metadata_cache_file" ]; then + if ! $ram_mode_active && [ -f "$metadata_cache_file" ]; then local old_metadata=$(cat "$metadata_cache_file") if [ "$old_metadata" != "$new_metadata" ]; then frontmatter_changed=true @@ -201,13 +269,15 @@ EOF fi # Store all metadata in one write operation - lock_file "$metadata_cache_file" - mkdir -p "$(dirname "$metadata_cache_file")" - echo "$new_metadata" > "$metadata_cache_file" - unlock_file "$metadata_cache_file" + if ! $ram_mode_active; then + lock_file "$metadata_cache_file" + mkdir -p "$(dirname "$metadata_cache_file")" + echo "$new_metadata" > "$metadata_cache_file" + unlock_file "$metadata_cache_file" + fi # If frontmatter has changed, update the marker file's timestamp - if $frontmatter_changed; then + if ! $ram_mode_active && $frontmatter_changed; then touch "$frontmatter_changes_marker" fi @@ -220,17 +290,30 @@ generate_excerpt() { local file="$1" local max_length="${2:-160}" # Default to 160 characters - # Extract content after frontmatter - local start_line=$(grep -n "^---$" "$file" | head -1 | cut -d: -f1) - local end_line=$(grep -n "^---$" "$file" | head -n 2 | tail -1 | cut -d: -f1) - local raw_content_stream - if [[ -n "$start_line" && -n "$end_line" && $start_line -lt $end_line ]]; then - # Stream content after frontmatter - raw_content_stream=$(tail -n +$((end_line + 1)) "$file") + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then + # Remove frontmatter directly from preloaded content + raw_content_stream=$(printf '%s\n' "$(ram_mode_get_content "$file")" | awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { in_fm = 0; next; } + } + { if (!in_fm) print; } + ') else - # No valid frontmatter, stream the whole file - raw_content_stream=$(cat "$file") + # Extract content after frontmatter + local start_line end_line + start_line=$(grep -n "^---$" "$file" | head -1 | cut -d: -f1) + end_line=$(grep -n "^---$" "$file" | head -n 2 | tail -1 | cut -d: -f1) + + if [[ -n "$start_line" && -n "$end_line" && $start_line -lt $end_line ]]; then + # Stream content after frontmatter + raw_content_stream=$(tail -n +$((end_line + 1)) "$file") + else + # No valid frontmatter, stream the whole file + raw_content_stream=$(cat "$file") + fi fi # Sanitize and extract the first non-empty paragraph/line @@ -310,26 +393,19 @@ convert_markdown_to_html() { elif [ "$MARKDOWN_PROCESSOR" = "markdown.pl" ]; then # Preprocess content to handle fenced code blocks for markdown.pl local preprocessed_content="$content" - local temp_file - temp_file=$(mktemp) - # Use printf to avoid issues with content starting with - - printf '%s' "$preprocessed_content" > "$temp_file" - # Handle fenced code blocks (``` and ~~~) -> indented # Requires awk if command -v awk &> /dev/null; then - preprocessed_content=$(awk ' + preprocessed_content=$(printf '%s' "$preprocessed_content" | awk ' BEGIN { in_code = 0; } /^```[a-zA-Z0-9]*$/ || /^~~~[a-zA-Z0-9]*$/ { if (!in_code) { in_code = 1; print ""; next; } } /^```$/ || /^~~~$/ { if (in_code) { in_code = 0; print ""; next; } } { if (in_code) { print " " $0; } else { print $0; } } - ' "$temp_file") - rm "$temp_file" + ') else echo -e "${YELLOW}Warning: awk not found, markdown.pl fenced code block conversion skipped.${NC}" >&2 # Content remains as original if awk fails - preprocessed_content=$(cat "$temp_file") - rm "$temp_file" + preprocessed_content="$content" fi # Ensure MARKDOWN_PL_PATH is set and executable @@ -352,4 +428,4 @@ convert_markdown_to_html() { return 0 } -# --- Content Functions --- END --- \ No newline at end of file +# --- Content Functions --- END --- diff --git a/scripts/build/deps.sh b/scripts/build/deps.sh old mode 100755 new mode 100644 index 7166ad7..4968a9f --- a/scripts/build/deps.sh +++ b/scripts/build/deps.sh @@ -96,7 +96,7 @@ check_dependencies() { if [[ "$(uname)" == "NetBSD" ]]; then echo -e "${YELLOW}Parallel processing is unreliable on NetBSD. Using sequential processing.${NC}" export HAS_PARALLEL=false - elif command -v parallel > /dev/null 2>&1; then + elif command -v parallel > /dev/null 2>&1 && { read -r _version < <(parallel -V 2>/dev/null ) && [[ "${_version:0:3}" = "GNU" ]]; }; then echo -e "${GREEN}GNU parallel found! Using parallel processing.${NC}" export HAS_PARALLEL=true else diff --git a/scripts/build/generate_archives.sh b/scripts/build/generate_archives.sh index 9d6d8a9..7cfe8f0 100755 --- a/scripts/build/generate_archives.sh +++ b/scripts/build/generate_archives.sh @@ -14,6 +14,315 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s # Helper Functions for Archive Generation # ============================================================================== +_generate_ram_year_archive_page() { + local year="$1" + [ -z "$year" ] && return 0 + + local year_index_page="$OUTPUT_DIR/archives/$year/index.html" + mkdir -p "$(dirname "$year_index_page")" + + local year_header="$HEADER_TEMPLATE" + local year_footer="$FOOTER_TEMPLATE" + local year_page_title="${MSG_ARCHIVES_FOR:-"Archives for"} $year" + local year_archive_rel_url="/archives/$year/" + year_header=${year_header//\{\{site_title\}\}/"$SITE_TITLE"} + year_header=${year_header//\{\{page_title\}\}/"$year_page_title"} + year_header=${year_header//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + year_header=${year_header//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + year_header=${year_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + year_header=${year_header//\{\{og_type\}\}/"website"} + year_header=${year_header//\{\{page_url\}\}/"$year_archive_rel_url"} + year_header=${year_header//\{\{site_url\}\}/"$SITE_URL"} + year_header=${year_header//\{\{og_image\}\}/""} + year_header=${year_header//\{\{twitter_image\}\}/""} + year_header=${year_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + local year_schema_json + year_schema_json='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$year_page_title"'","description": "Archive of posts from '"$year"'","url": "'"$SITE_URL$year_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' + year_header=${year_header//\{\{schema_json_ld\}\}/"$year_schema_json"} + year_footer=${year_footer//\{\{current_year\}\}/$(date +%Y)} + year_footer=${year_footer//\{\{author_name\}\}/"$AUTHOR_NAME"} + + { + echo "$year_header" + echo "<h1>$year_page_title</h1>" + echo "<ul class=\"month-list\">" + local month_key + for month_key in $(printf '%s\n' "${!month_posts[@]}" | awk -F'|' -v y="$year" '$1 == y { print $0 }' | sort -t'|' -k2,2nr); do + local month_num="${month_key#*|}" + local month_name="${month_name_map[$month_key]}" + local month_post_count + month_post_count=$(printf '%s\n' "${month_posts[$month_key]}" | awk 'NF { c++ } END { print c+0 }') + local month_idx_formatted + month_idx_formatted=$(printf "%02d" "$((10#$month_num))") + local month_var_name="MSG_MONTH_${month_idx_formatted}" + local current_month_name="${!month_var_name:-$month_name}" + local month_url + month_url=$(fix_url "/archives/$year/$month_idx_formatted/") + echo "<li><a href=\"$month_url\">$current_month_name ($month_post_count)</a></li>" + done + echo "</ul>" + echo "$year_footer" + } > "$year_index_page" +} + +_generate_ram_month_archive_page() { + local month_key="$1" + [ -z "$month_key" ] && return 0 + + local year="${month_key%|*}" + local month_num="${month_key#*|}" + local month_idx_formatted + month_idx_formatted=$(printf "%02d" "$((10#$month_num))") + local month_index_page="$OUTPUT_DIR/archives/$year/$month_idx_formatted/index.html" + mkdir -p "$(dirname "$month_index_page")" + + local month_name_var="MSG_MONTH_${month_idx_formatted}" + local month_name="${!month_name_var:-${month_name_map[$month_key]}}" + [ -z "$month_name" ] && month_name="Month $month_idx_formatted" + + local month_header="$HEADER_TEMPLATE" + local month_footer="$FOOTER_TEMPLATE" + local month_page_title="${MSG_ARCHIVES_FOR:-"Archives for"} $month_name $year" + local month_archive_rel_url="/archives/$year/$month_idx_formatted/" + month_header=${month_header//\{\{site_title\}\}/"$SITE_TITLE"} + month_header=${month_header//\{\{page_title\}\}/"$month_page_title"} + month_header=${month_header//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + month_header=${month_header//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + month_header=${month_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + month_header=${month_header//\{\{og_type\}\}/"website"} + month_header=${month_header//\{\{page_url\}\}/"$month_archive_rel_url"} + month_header=${month_header//\{\{site_url\}\}/"$SITE_URL"} + month_header=${month_header//\{\{og_image\}\}/""} + month_header=${month_header//\{\{twitter_image\}\}/""} + month_header=${month_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + local month_schema_json + month_schema_json='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$month_page_title"'","description": "Archive of posts from '"$month_name $year"'","url": "'"$SITE_URL$month_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' + month_header=${month_header//\{\{schema_json_ld\}\}/"$month_schema_json"} + month_footer=${month_footer//\{\{current_year\}\}/$(date +%Y)} + month_footer=${month_footer//\{\{author_name\}\}/"$AUTHOR_NAME"} + + { + echo "$month_header" + echo "<h1>$month_page_title</h1>" + echo "<div class=\"posts-list\">" + while IFS='|' read -r _ _ _ title date lastmod filename slug image image_caption description author_name author_email; do + [ -z "$title" ] && continue + local post_year post_month post_day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d) + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$post_year}" + url_path="${url_path//Month/$post_month}" + url_path="${url_path//Day/$post_day}" + url_path="${url_path//slug/$slug}" + local post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')" + post_url="${SITE_URL}${post_url}" + + local display_date_format="$DATE_FORMAT" + if [ "${SHOW_TIMEZONE:-false}" = false ]; then + display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//') + fi + local formatted_date + formatted_date=$(format_date "$date" "$display_date_format") + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + + cat << EOF + <article> + <h2><a href="${post_url}">$title</a></h2> + <div class="meta">${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date ${MSG_BY:-\"by\"} <strong>$display_author_name</strong></div> +EOF + if [ -n "$image" ]; then + local image_url + image_url=$(fix_url "$image") + local alt_text="${image_caption:-$title}" + local figcaption_content="${image_caption:-$title}" + cat << EOF + <figure class="featured-image tag-image"> + <a href="${post_url}"> + <img src="$image_url" alt="$alt_text" /> + </a> + <figcaption>$figcaption_content</figcaption> + </figure> +EOF + fi + if [ -n "$description" ]; then + cat << EOF + <div class="summary"> + $description + </div> +EOF + fi + cat << EOF + </article> +EOF + done < <(printf '%s\n' "${month_posts[$month_key]}" | awk 'NF' | sort -t'|' -k5,5r) + echo "</div>" + echo "$month_footer" + } > "$month_index_page" +} + +_generate_archive_pages_ram() { + echo -e "${YELLOW}Processing archive pages...${NC}" + + local archive_index_data + archive_index_data=$(ram_mode_get_dataset "archive_index") + if [ -z "$archive_index_data" ]; then + echo -e "${YELLOW}Warning: No archive index data in RAM. Skipping archive generation.${NC}" + return 0 + fi + + declare -A month_posts=() + declare -A month_name_map=() + declare -A year_map=() + + local line + while IFS= read -r line; do + [ -z "$line" ] && continue + local year month month_name + IFS='|' read -r year month month_name _ <<< "$line" + [ -z "$year" ] && continue + [ -z "$month" ] && continue + local month_key="${year}|${month}" + month_posts["$month_key"]+="$line"$'\n' + month_name_map["$month_key"]="$month_name" + year_map["$year"]=1 + done <<< "$archive_index_data" + + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"${MSG_ARCHIVES:-"Archives"}"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"/archives/"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + header_content=${header_content//\{\{og_image\}\}/""} + header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + local schema_json_ld + schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "Archives","description": "'"$SITE_DESCRIPTION"'","url": "'"$SITE_URL"'/archives/","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' + header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} + footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + + local archives_index_page="$OUTPUT_DIR/archives/index.html" + mkdir -p "$(dirname "$archives_index_page")" + { + echo "$header_content" + echo "<h1>${MSG_ARCHIVES:-"Archives"}</h1>" + echo "<div class=\"archives-list year-list\">" + + local year + for year in $(printf '%s\n' "${!year_map[@]}" | sort -nr); do + [ -z "$year" ] && continue + local year_url + year_url=$(fix_url "/archives/$year/") + echo " <h2><a href=\"$year_url\">$year</a></h2>" + echo " <ul class=\"month-list-detailed\">" + + local month_key + for month_key in $(printf '%s\n' "${!month_posts[@]}" | awk -F'|' -v y="$year" '$1 == y { print $0 }' | sort -t'|' -k2,2nr); do + local month_num="${month_key#*|}" + local month_name="${month_name_map[$month_key]}" + local month_idx_formatted + month_idx_formatted=$(printf "%02d" "$((10#$month_num))") + local month_var_name="MSG_MONTH_${month_idx_formatted}" + local current_month_name="${!month_var_name:-$month_name}" + local month_url + month_url=$(fix_url "/archives/$year/$month_idx_formatted/") + local month_post_count + month_post_count=$(printf '%s\n' "${month_posts[$month_key]}" | awk 'NF { c++ } END { print c+0 }') + + echo " <li>" + echo " <a href=\"$month_url\">$current_month_name ($month_post_count)</a>" + + if [ "${ARCHIVES_LIST_ALL_POSTS:-false}" = true ] && [ "$month_post_count" -gt 0 ]; then + echo " <ul class=\"post-list-condensed-inline\">" + while IFS='|' read -r _ _ _ title date _ filename slug _ _ _ author_name author_email; do + [ -z "$title" ] && continue + local post_year post_month post_day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d) + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$post_year}" + url_path="${url_path//Month/$post_month}" + url_path="${url_path//Day/$post_day}" + url_path="${url_path//slug/$slug}" + local post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')" + post_url=$(fix_url "$post_url") + local display_date + display_date=$(echo "$date" | cut -d' ' -f1) + echo " <li><a href=\"$post_url\">[$display_date] $title</a></li>" + done < <(printf '%s\n' "${month_posts[$month_key]}" | awk 'NF' | sort -t'|' -k5,5r) + echo " </ul>" + fi + echo " </li>" + done + echo " </ul>" + done + + echo "</div>" + echo "$footer_content" + } > "$archives_index_page" + + local year_count=${#year_map[@]} + local month_count=${#month_posts[@]} + local year_jobs month_jobs max_workers + max_workers=$(get_parallel_jobs) + year_jobs="$max_workers" + month_jobs="$max_workers" + if [ "$year_jobs" -gt "$year_count" ]; then + year_jobs="$year_count" + fi + if [ "$month_jobs" -gt "$month_count" ]; then + month_jobs="$month_count" + fi + + if [ "$year_jobs" -gt 1 ] && [ "$year_count" -gt 1 ]; then + echo -e "${GREEN}Using shell parallel workers for ${year_count} RAM-mode year archive pages${NC}" + run_parallel "$year_jobs" < <( + while IFS= read -r year; do + [ -z "$year" ] && continue + printf "_generate_ram_year_archive_page '%s'\n" "$year" + done < <(printf '%s\n' "${!year_map[@]}" | sort -nr) + ) || return 1 + else + local year + for year in $(printf '%s\n' "${!year_map[@]}" | sort -nr); do + _generate_ram_year_archive_page "$year" + done + fi + + if [ "$month_jobs" -gt 1 ] && [ "$month_count" -gt 1 ]; then + echo -e "${GREEN}Using shell parallel workers for ${month_count} RAM-mode monthly archive pages${NC}" + run_parallel "$month_jobs" < <( + while IFS= read -r month_key; do + [ -z "$month_key" ] && continue + printf "_generate_ram_month_archive_page '%s'\n" "$month_key" + done < <(printf '%s\n' "${!month_posts[@]}" | sort -t'|' -k1,1nr -k2,2nr) + ) || return 1 + else + local month_key + for month_key in $(printf '%s\n' "${!month_posts[@]}" | sort -t'|' -k1,1nr -k2,2nr); do + _generate_ram_month_archive_page "$month_key" + done + fi + + echo -e "${GREEN}Archive page processing complete.${NC}" +} + # Check if the main archive index page needs rebuilding _check_archive_index_rebuild_needed() { local archive_index_file="$CACHE_DIR/archive_index.txt" @@ -93,10 +402,11 @@ _generate_main_archive_index() { header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"} header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} header_content=${header_content//\{\{og_type\}\}/"website"} - header_content=${header_content//\{\{page_url\}\}/"archives/"} # Relative URL for header placeholder + header_content=${header_content//\{\{page_url\}\}/"/archives/"} # Relative URL for header placeholder header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Add schema local schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "Archives","description": "'"$SITE_DESCRIPTION"'","url": "'"$SITE_URL"'/archives/","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} @@ -161,7 +471,7 @@ _generate_main_archive_index() { local post_year post_month post_day url_path post_url # Grep posts for this specific year and numeric month, sort REVERSE chronologically - grep "^$year|$month|" "$archive_index_file" 2>/dev/null | sort -t'|' -k5,5r | while IFS='|' read -r _ _ _ title date _ filename slug _; do + grep "^$year|$month|" "$archive_index_file" 2>/dev/null | sort -t'|' -k5,5r | while IFS='|' read -r _ _ _ title date _ filename slug _ _ _ author_name author_email; do # Construct post URL (logic adapted from process_single_month) if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then post_year="${BASH_REMATCH[1]}" @@ -231,6 +541,7 @@ _generate_year_index() { header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Add schema local schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$year_page_title"'","description": "Archive of posts from '"$year"'","url": "'"$SITE_URL$year_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} @@ -307,7 +618,7 @@ process_single_month() { # Generate header local header_content="$HEADER_TEMPLATE" - local month_page_title="${MSG_ARCHIVES_FOR:-\"Archives for\"} $month_name $year" + local month_page_title="${MSG_ARCHIVES_FOR:-"Archives for"} $month_name $year" local month_archive_rel_url="/archives/$year/$month_num/" header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} header_content=${header_content//\{\{page_title\}\}/"$month_page_title"} @@ -319,6 +630,7 @@ process_single_month() { header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Add schema local schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$month_page_title"'","description": "Archive of posts from '"$month_name $year"'","url": "'"$SITE_URL$month_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} @@ -335,7 +647,7 @@ process_single_month() { echo "<div class=\"posts-list\">" # Grep for posts from this specific month and year - grep "^$year|$month_num|" "$archive_index_file" 2>/dev/null | while IFS='|' read -r _ _ _ title date lastmod filename slug image image_caption description; do + grep "^$year|$month_num|" "$archive_index_file" 2>/dev/null | while IFS='|' read -r _ _ _ title date lastmod filename slug image image_caption description author_name author_email; do # --- Start: Card Generation Logic (copied from generate_tags.sh) --- local post_url post_year post_month post_day url_path @@ -367,11 +679,14 @@ process_single_month() { fi local formatted_date=$(format_date "$date" "$display_date_format") + # Determine author for display (with fallback) + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + # Use cat heredoc for multi-line article structure cat << EOF <article> - <h3><a href="${post_url}">$title</a></h3> - <div class="meta">${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date</div> + <h2><a href="${post_url}">$title</a></h2> + <div class="meta">${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date ${MSG_BY:-\"by\"} <strong>$display_author_name</strong></div> EOF if [ -n "$image" ]; then @@ -429,6 +744,11 @@ _process_single_month_parallel_wrapper() { # Main Archive Generation Orchestrator # ============================================================================== generate_archive_pages() { + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + _generate_archive_pages_ram + return $? + fi + echo -e "${YELLOW}Processing archive pages...${NC}" local archive_index_file="$CACHE_DIR/archive_index.txt" @@ -546,4 +866,4 @@ generate_archive_pages() { } # Make the function available for sourcing -export -f generate_archive_pages \ No newline at end of file +export -f generate_archive_pages diff --git a/scripts/build/generate_authors.sh b/scripts/build/generate_authors.sh new file mode 100644 index 0000000..e8c87c8 --- /dev/null +++ b/scripts/build/generate_authors.sh @@ -0,0 +1,676 @@ +#!/usr/bin/env bash +# +# BSSG - Author Page Generation +# Handles the creation of individual author pages and the main author index. +# + +# Source dependencies +# shellcheck source=utils.sh disable=SC1091 +source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_authors.sh"; exit 1; } +# shellcheck source=cache.sh disable=SC1091 +source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_authors.sh"; exit 1; } +# Source the feed generator script for the reusable RSS function +# shellcheck source=generate_feeds.sh disable=SC1091 +source "$(dirname "$0")/generate_feeds.sh" || { echo >&2 "Error: Failed to source generate_feeds.sh from generate_authors.sh"; exit 1; } + +_generate_author_pages_ram() { + echo -e "${YELLOW}Processing author pages${NC}${ENABLE_AUTHOR_RSS:+" and RSS feeds"}...${NC}" + + local authors_index_data + authors_index_data=$(ram_mode_get_dataset "authors_index") + local main_authors_index_output="$OUTPUT_DIR/authors/index.html" + + mkdir -p "$OUTPUT_DIR/authors" + + if [ -z "$authors_index_data" ]; then + echo -e "${YELLOW}No authors found in RAM index. Skipping author page generation.${NC}" + return 0 + fi + + declare -A author_posts_by_slug=() + declare -A author_name_by_slug=() + declare -A author_email_by_slug=() + local line author author_slug author_email + while IFS= read -r line; do + [ -z "$line" ] && continue + IFS='|' read -r author author_slug author_email _ <<< "$line" + [ -z "$author" ] && continue + [ -z "$author_slug" ] && continue + if [[ -z "${author_name_by_slug[$author_slug]+_}" ]]; then + author_name_by_slug["$author_slug"]="$author" + author_email_by_slug["$author_slug"]="$author_email" + fi + author_posts_by_slug["$author_slug"]+="$line"$'\n' + done <<< "$authors_index_data" + + local author_slug_key + for author_slug_key in $(printf '%s\n' "${!author_name_by_slug[@]}" | sort); do + author="${author_name_by_slug[$author_slug_key]}" + local author_data="${author_posts_by_slug[$author_slug_key]}" + local author_page_html_file="$OUTPUT_DIR/authors/$author_slug_key/index.html" + local author_rss_file="$OUTPUT_DIR/authors/$author_slug_key/${RSS_FILENAME:-rss.xml}" + local author_page_rel_url="/authors/${author_slug_key}/" + local author_rss_rel_url="/authors/${author_slug_key}/${RSS_FILENAME:-rss.xml}" + local post_count + post_count=$(printf '%s\n' "$author_data" | awk 'NF { c++ } END { print c+0 }') + + mkdir -p "$(dirname "$author_page_html_file")" + + local author_page_content="" + author_page_content+="<h1>${MSG_POSTS_BY:-Posts by} $author</h1>"$'\n' + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + author_page_content+="<p><a href=\"$author_rss_rel_url\">${MSG_RSS_FEED:-RSS Feed}</a></p>"$'\n' + fi + author_page_content+="<div class=\"posts-list\">"$'\n' + + while IFS='|' read -r author_name_inner author_slug_inner author_email_inner post_title post_date post_lastmod post_filename post_slug post_image post_image_caption post_description; do + [ -z "$post_title" ] && continue + + local post_url + if [ -n "$post_date" ] && [[ "$post_date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + local year month day url_path + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}" + url_path="${url_path//Month/$month}" + url_path="${url_path//Day/$day}" + url_path="${url_path//slug/$post_slug}" + post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')" + else + post_url="/$(echo "$post_slug" | sed 's|^/||; s|/*$|/|')" + fi + post_url="${BASE_URL}${post_url}" + local formatted_date + formatted_date=$(format_date "$post_date") + + author_page_content+="<article>"$'\n' + author_page_content+=" <h2><a href=\"$post_url\">$post_title</a></h2>"$'\n' + author_page_content+=" <div class=\"meta\">"$'\n' + author_page_content+=" <time datetime=\"$post_date\">$formatted_date</time>"$'\n' + author_page_content+=" </div>"$'\n' + if [ -n "$post_description" ]; then + author_page_content+=" <p class=\"summary\">$post_description</p>"$'\n' + fi + if [ -n "$post_image" ]; then + author_page_content+=" <div class=\"author-image\">"$'\n' + author_page_content+=" <img src=\"$post_image\" alt=\"$post_image_caption\" loading=\"lazy\">"$'\n' + author_page_content+=" </div>"$'\n' + fi + author_page_content+="</article>"$'\n' + done < <(printf '%s\n' "$author_data" | awk 'NF' | sort -t'|' -k5,5r) + + author_page_content+="</div>"$'\n' + + local page_title="${MSG_POSTS_BY:-Posts by} $author" + local page_description="${MSG_POSTS_BY:-Posts by} $author - $post_count ${MSG_POSTS:-posts}" + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"$page_title"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$page_description"} + header_content=${header_content//\{\{twitter_description\}\}/"$page_description"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"$author_page_rel_url"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + header_content=${header_content//\{\{og_image\}\}/} + header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + header_content=${header_content//<!-- bssg:tag_rss_link -->/} + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + local author_rss_link="<link rel=\"alternate\" type=\"application/rss+xml\" title=\"$author RSS Feed\" href=\"$SITE_URL$author_rss_rel_url\">" + header_content=${header_content//<!-- bssg:tag_rss_link -->/$author_rss_link} + fi + local schema_json + schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"$page_description\",\"url\": \"$SITE_URL$author_page_rel_url\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}" + header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"} + + local current_year + current_year=$(date +%Y) + footer_content=${footer_content//\{\{current_year\}\}/"$current_year"} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"} + + { + echo "$header_content" + echo "$author_page_content" + echo "$footer_content" + } > "$author_page_html_file" + + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + local author_post_data + author_post_data=$(printf '%s\n' "$author_data" | awk 'NF' | sort -t'|' -k5,5r | awk -F'|' '{ + author_name = $1 + author_email = $3 + title = $4 + date = $5 + lastmod = $6 + filename = $7 + post_slug = $8 + image = $9 + image_caption = $10 + description = $11 + printf "%s|%s|%s|%s|%s||%s|%s|%s|%s|%s|%s\n", filename, filename, title, date, lastmod, post_slug, image, image_caption, description, author_name, author_email + }') + _generate_rss_feed "$author_rss_file" "$SITE_TITLE - ${MSG_POSTS_BY:-Posts by} $author" "${MSG_POSTS_BY:-Posts by} $author" "$author_page_rel_url" "$author_rss_rel_url" "$author_post_data" + fi + done + + local page_title="${MSG_ALL_AUTHORS:-All Authors}" + local page_description="${MSG_ALL_AUTHORS:-All Authors} - $SITE_DESCRIPTION" + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + local main_content="" + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"$page_title"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$page_description"} + header_content=${header_content//\{\{twitter_description\}\}/"$page_description"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"/authors/"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + header_content=${header_content//\{\{og_image\}\}/} + header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + header_content=${header_content//<!-- bssg:tag_rss_link -->/} + local schema_json + schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"List of all authors on $SITE_TITLE\",\"url\": \"$SITE_URL/authors/\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}" + header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"} + local current_year + current_year=$(date +%Y) + footer_content=${footer_content//\{\{current_year\}\}/"$current_year"} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"} + + main_content+="<h1>${MSG_ALL_AUTHORS:-All Authors}</h1>"$'\n' + main_content+="<div class=\"tags-list\">"$'\n' + for author_slug_key in $(printf '%s\n' "${!author_name_by_slug[@]}" | sort); do + author="${author_name_by_slug[$author_slug_key]}" + local post_count + post_count=$(printf '%s\n' "${author_posts_by_slug[$author_slug_key]}" | awk 'NF { c++ } END { print c+0 }') + if [ "$post_count" -gt 0 ]; then + main_content+=" <a href=\"$BASE_URL/authors/$author_slug_key/\">$author <span class=\"tag-count\">($post_count)</span></a>"$'\n' + fi + done + main_content+="</div>"$'\n' + + { + echo "$header_content" + echo "$main_content" + echo "$footer_content" + } > "$main_authors_index_output" + + echo -e "${GREEN}Author pages processed!${NC}" + echo -e "${GREEN}Generated author list pages.${NC}" +} + +# Generate author pages +generate_author_pages() { + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + _generate_author_pages_ram + return $? + fi + + echo -e "${YELLOW}Processing author pages${NC}${ENABLE_AUTHOR_RSS:+" and RSS feeds"}...${NC}" + + local authors_index_file="$CACHE_DIR/authors_index.txt" + local main_authors_index_output="$OUTPUT_DIR/authors/index.html" + local modified_authors_list_file="${CACHE_DIR:-.bssg_cache}/modified_authors.list" + + # Check if the authors index file exists (needed for listing authors) + if [ ! -f "$authors_index_file" ]; then + echo -e "${YELLOW}Authors index file not found at $authors_index_file. Skipping author page generation.${NC}" + # If the index doesn't exist, no authors were found in posts. + # Ensure the main output directory exists but is empty. + mkdir -p "$(dirname "$main_authors_index_output")" + echo -e "${GREEN}Author pages processed! (No authors found)${NC}" + echo -e "${GREEN}Generated author list pages. (No authors found)${NC}" + return 0 + fi + + # --- Calculate Latest Common Dependency Time --- START --- + # Get mtimes of config hash, templates, and locale file + local latest_common_dep_time=0 + local config_hash_time=$(get_file_mtime "$CONFIG_HASH_FILE") + latest_common_dep_time=$(( config_hash_time > latest_common_dep_time ? config_hash_time : latest_common_dep_time )) + + local template_dir="${TEMPLATES_DIR:-templates}" + if [ -d "$template_dir/${THEME:-default}" ]; then + template_dir="$template_dir/${THEME:-default}" + fi + local header_template="$template_dir/header.html" + local footer_template="$template_dir/footer.html" + local header_time=$(get_file_mtime "$header_template") + local footer_time=$(get_file_mtime "$footer_template") + latest_common_dep_time=$(( header_time > latest_common_dep_time ? header_time : latest_common_dep_time )) + latest_common_dep_time=$(( footer_time > latest_common_dep_time ? footer_time : latest_common_dep_time )) + + local active_locale_file="" + if [ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]; then + active_locale_file="${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" + elif [ -f "${LOCALE_DIR:-locales}/en.sh" ]; then + active_locale_file="${LOCALE_DIR:-locales}/en.sh" + fi + local locale_time=$(get_file_mtime "$active_locale_file") + latest_common_dep_time=$(( locale_time > latest_common_dep_time ? locale_time : latest_common_dep_time )) + # --- Calculate Latest Common Dependency Time --- END --- + + # --- Simplified Global Check --- START --- + # Decide if we need to proceed with any author generation steps at all. + local proceed_with_generation=false + local force_rebuild_status="${FORCE_REBUILD:-false}" + + if [ "$force_rebuild_status" = true ]; then + proceed_with_generation=true + echo "Force rebuild enabled, proceeding with author generation." >&2 # Debug + elif [ "$latest_common_dep_time" -gt 0 ] && { [ ! -f "$main_authors_index_output" ] || (( $(get_file_mtime "$main_authors_index_output") < latest_common_dep_time )); }; then + # Common dependencies are newer than the main output (or main output missing) + proceed_with_generation=true + echo "Common dependencies changed, proceeding with author generation." >&2 # Debug + elif [ -s "$modified_authors_list_file" ]; then + # Modified authors list exists and is not empty + proceed_with_generation=true + echo "Modified authors detected, proceeding with author generation." >&2 # Debug + elif [ ! -f "$main_authors_index_output" ]; then + # Fallback: if main output is missing, we should generate it + proceed_with_generation=true + echo "Main authors index missing, proceeding with author generation." >&2 # Debug + fi + + if [ "$proceed_with_generation" = false ]; then + echo -e "${GREEN}Authors index, author pages${NC}${ENABLE_AUTHOR_RSS:+, and author RSS feeds} appear up to date based on common dependencies and modified posts, skipping.${NC}" + echo -e "${GREEN}Author pages processed!${NC}" # Keep consistent final message + echo -e "${GREEN}Generated author list pages.${NC}" # Keep consistent final message + return 0 + fi + # --- Simplified Global Check --- END --- + + # --- Proceed with Generation --- + + # Get unique authors (Author|Slug pairs) + local unique_authors_lines=$(awk -F'|' '{print $1 "|" $2}' "$authors_index_file" | sort | uniq) + local author_count=$(echo "$unique_authors_lines" | grep -v '^$' | wc -l) + echo -e "Checking ${GREEN}$author_count${NC} author pages${NC}${ENABLE_AUTHOR_RSS:+/feeds} for changes (based on common deps & modified authors)" # Updated message + + # --- Pre-group posts by author slug --- START --- + local author_data_dir="$CACHE_DIR/author_data" + rm -rf "$author_data_dir" # Clean previous data + mkdir -p "$author_data_dir" + echo -e "Pre-grouping posts by author into ${BLUE}$author_data_dir${NC}..." + if awk -F'|' -v author_dir="$author_data_dir" ' + NF >= 2 { # Ensure at least author and slug fields exist + author_slug = $2; + if (author_slug != "") { + # Sanitize slug just in case for filename safety? (basic: remove /) + gsub(/\//, "_", author_slug); + output_file = author_dir "/" author_slug ".tmp"; + print $0 >> output_file; # Append the whole line + close(output_file); # Close file handle to avoid too many open files + } else { + print "Warning: Skipping line with empty author slug in authors_index: " $0 > "/dev/stderr"; + } + } + ' "$authors_index_file"; then + echo -e "${GREEN}Pre-grouping complete.${NC}" + else + echo -e "${RED}Error: Failed to pre-group author data using awk.${NC}" >&2 + return 1 + fi + # --- Pre-group posts by author slug --- END --- + + # Define a modified file_needs_rebuild function for parallel use + parallel_file_needs_rebuild() { + local output_file="$1" + local latest_dep_time="$2" # This should be latest_common_dep_time + + # Rebuild if output file doesn't exist + if [ ! -f "$output_file" ]; then + return 0 # Rebuild needed + fi + + local output_time=$(get_file_mtime "$output_file") + + # Rebuild if output is older than the latest relevant *common* dependency + if (( output_time < latest_dep_time )); then + return 0 # Rebuild needed + fi + + return 1 # No rebuild needed + } + + # Define a function to process a single author + process_author() { + local author_line="$1" + local author_data_dir="$2" + local latest_common_dep_time_for_author="$3" + local modified_authors_file="$4" # Accept filename instead of hash + + # --- Load modified authors from file --- + declare -A modified_authors_hash + if [ -f "$modified_authors_file" ]; then + local mod_author_local + while IFS= read -r mod_author_local || [[ -n "$mod_author_local" ]]; do + if [ -n "$mod_author_local" ]; then # Ensure not empty line + modified_authors_hash["$mod_author_local"]=1 + fi + done < "$modified_authors_file" + fi + + local author author_slug + IFS='|' read -r author author_slug <<< "$author_line" + + if [ -n "$author" ]; then + local author_page_html_file="$OUTPUT_DIR/authors/$author_slug/index.html" + local author_rss_file="$OUTPUT_DIR/authors/$author_slug/${RSS_FILENAME:-rss.xml}" + local author_page_rel_url="/authors/${author_slug}/" + local author_rss_rel_url="/authors/${author_slug}/${RSS_FILENAME:-rss.xml}" + local rebuild_html=false + local rebuild_rss=false + + # --- Force rebuild flags if author was modified --- + local author_was_modified=false + if [ -n "${modified_authors_hash[$author]}" ]; then + author_was_modified=true + rebuild_html=true # Force rebuild if author was modified + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + rebuild_rss=true # Force rebuild if author was modified + fi + fi + + # --- Check if HTML rebuild is needed --- + if [ "$rebuild_html" = false ]; then + if parallel_file_needs_rebuild "$author_page_html_file" "$latest_common_dep_time_for_author"; then + rebuild_html=true + fi + fi + + # --- Check if RSS rebuild is needed --- + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ] && [ "$rebuild_rss" = false ]; then + if parallel_file_needs_rebuild "$author_rss_file" "$latest_common_dep_time_for_author"; then + rebuild_rss=true + fi + fi + + # --- Skip if no rebuilds needed --- + if [ "$rebuild_html" = false ] && { [ "${ENABLE_AUTHOR_RSS:-false}" = false ] || [ "$rebuild_rss" = false ]; }; then + echo "Author '$author' pages are up to date, skipping." + return 0 + fi + + # --- Load author posts data --- + local author_data_file="$author_data_dir/${author_slug}.tmp" + if [ ! -f "$author_data_file" ]; then + echo "Warning: No posts found for author '$author' (expected file: $author_data_file)" >&2 + return 0 + fi + + # Count posts for this author + local post_count=$(wc -l < "$author_data_file") + + echo "Processing author '$author' ($post_count posts)..." + + # --- Generate Author HTML Page --- + if [ "$rebuild_html" = true ]; then + mkdir -p "$(dirname "$author_page_html_file")" + + # Generate author page content + local author_page_content="" + author_page_content+="<h1>${MSG_POSTS_BY:-Posts by} $author</h1>"$'\n' + + # Add RSS link if enabled + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + author_page_content+="<p><a href=\"$author_rss_rel_url\">${MSG_RSS_FEED:-RSS Feed}</a></p>"$'\n' + fi + + # Add posts list + author_page_content+="<div class=\"posts-list\">"$'\n' + + # Sort posts by date (newest first) and generate HTML + local posts_html="" + while IFS='|' read -r author_name author_slug_inner author_email post_title post_date post_lastmod post_filename post_slug post_image post_image_caption post_description; do + # Construct post URL using URL_SLUG_FORMAT (same logic as generate_posts.sh) + local post_url="" + if [ -n "$post_date" ]; then + local year month day + if [[ "$post_date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + year=$(date +%Y); month=$(date +%m); day=$(date +%d) # Fallback + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; + url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$post_slug}" + # Ensure relative post_url starts with / and ends with / + post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')" + else + # Fallback for posts without date + post_url="/$(echo "$post_slug" | sed 's|^/||; s|/*$|/|')" + fi + # Convert to full URL with BASE_URL + post_url="$BASE_URL$post_url" + local formatted_date=$(format_date "$post_date") + + posts_html+="<article>"$'\n' + posts_html+=" <h2><a href=\"$post_url\">$post_title</a></h2>"$'\n' + posts_html+=" <div class=\"meta\">"$'\n' + posts_html+=" <time datetime=\"$post_date\">$formatted_date</time>"$'\n' + posts_html+=" </div>"$'\n' + + if [ -n "$post_description" ]; then + posts_html+=" <p class=\"summary\">$post_description</p>"$'\n' + fi + + if [ -n "$post_image" ]; then + posts_html+=" <div class=\"author-image\">"$'\n' + posts_html+=" <img src=\"$post_image\" alt=\"$post_image_caption\" loading=\"lazy\">"$'\n' + posts_html+=" </div>"$'\n' + fi + + posts_html+="</article>"$'\n' + done < <(sort -t'|' -k5,5r "$author_data_file") + + author_page_content+="$posts_html" + + author_page_content+="</div>"$'\n' + + # Generate full HTML page + local page_title="${MSG_POSTS_BY:-Posts by} $author" + local page_description="${MSG_POSTS_BY:-Posts by} $author - $post_count ${MSG_POSTS:-posts}" + + # Process templates with placeholder replacement + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + + # Replace placeholders in the header (following tags generator pattern) + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"$page_title"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$page_description"} + header_content=${header_content//\{\{twitter_description\}\}/"$page_description"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"$author_page_rel_url"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + + # Remove unprocessed image placeholders + header_content=${header_content//\{\{og_image\}\}/} + header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + + # Remove the placeholder for the tag-specific RSS feed link + header_content=${header_content//<!-- bssg:tag_rss_link -->/} + + # Add author RSS link if enabled + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + local author_rss_link="<link rel=\"alternate\" type=\"application/rss+xml\" title=\"$author RSS Feed\" href=\"$SITE_URL$author_rss_rel_url\">" + header_content=${header_content//<!-- bssg:tag_rss_link -->/$author_rss_link} + fi + + # Schema.org structured data + local schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"$page_description\",\"url\": \"$SITE_URL$author_page_rel_url\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}" + header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"} + + # Replace placeholders in the footer + local current_year=$(date +%Y) + footer_content=${footer_content//\{\{current_year\}\}/"$current_year"} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"} + + # Create the full HTML page + { + echo "$header_content" + echo "$author_page_content" + echo "$footer_content" + } > "$author_page_html_file" + + echo "Generated author page: $author_page_html_file" + fi + + # --- Generate Author RSS Feed --- + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ] && [ "$rebuild_rss" = true ]; then + mkdir -p "$(dirname "$author_rss_file")" + + # Generate RSS feed for this author + local rss_title="$SITE_TITLE - ${MSG_POSTS_BY:-Posts by} $author" + local rss_description="${MSG_POSTS_BY:-Posts by} $author" + local feed_link_rel="$author_page_rel_url" + local feed_atom_link_rel="$author_rss_rel_url" + + # Read and format author post data for RSS generation + local author_post_data="" + if [ -f "$author_data_file" ]; then + # Transform author data format to RSS format and sort by date (newest first) + # Author format: Author|Slug|Email|Title|Date|LastMod|Filename|PostSlug|Image|ImageCaption|Description + # RSS format: file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email + author_post_data=$(sort -t'|' -k5,5r "$author_data_file" | awk -F'|' '{ + # Map fields from author format to RSS format + author_name = $1 + author_slug = $2 + author_email = $3 + title = $4 + date = $5 + lastmod = $6 + filename = $7 + post_slug = $8 + image = $9 + image_caption = $10 + description = $11 + + # RSS format: file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email + printf "%s|%s|%s|%s|%s||%s|%s|%s|%s|%s|%s\n", filename, filename, title, date, lastmod, post_slug, image, image_caption, description, author_name, author_email + }') + fi + + # Check if _generate_rss_feed function exists + if ! command -v _generate_rss_feed > /dev/null 2>&1; then + echo -e "${RED}Error: _generate_rss_feed function not found. Ensure generate_feeds.sh is sourced correctly.${NC}" >&2 + else + _generate_rss_feed "$author_rss_file" "$rss_title" "$rss_description" "$feed_link_rel" "$feed_atom_link_rel" "$author_post_data" + echo "Generated author RSS feed: $author_rss_file" + fi + fi + fi + } + + # Export the function for potential parallel use + export -f process_author parallel_file_needs_rebuild + + # Process each unique author + echo "$unique_authors_lines" | while IFS= read -r author_line || [[ -n "$author_line" ]]; do + if [ -n "$author_line" ]; then + process_author "$author_line" "$author_data_dir" "$latest_common_dep_time" "$modified_authors_list_file" + fi + done + + # --- Generate Main Authors Index Page --- + if [ "${AUTHORS_INDEX_NEEDS_REBUILD:-false}" = true ] || [ ! -f "$main_authors_index_output" ] || (( $(get_file_mtime "$main_authors_index_output") < latest_common_dep_time )); then + echo "Generating main authors index page..." + mkdir -p "$(dirname "$main_authors_index_output")" + + # Count posts per author and generate the main index + local authors_with_counts="" + echo "$unique_authors_lines" | while IFS= read -r author_line || [[ -n "$author_line" ]]; do + if [ -n "$author_line" ]; then + local author author_slug + IFS='|' read -r author author_slug <<< "$author_line" + local author_data_file="$author_data_dir/${author_slug}.tmp" + if [ -f "$author_data_file" ]; then + local post_count=$(wc -l < "$author_data_file") + echo "$author|$author_slug|$post_count" + fi + fi + done | sort > "${CACHE_DIR}/authors_with_counts.tmp" + + # Generate main authors index HTML + local main_content="" + main_content+="<h1>${MSG_ALL_AUTHORS:-All Authors}</h1>"$'\n' + main_content+="<div class=\"tags-list\">"$'\n' # Reuse tags styling + + while IFS='|' read -r author author_slug post_count; do + if [ -n "$author" ] && [ "$post_count" -gt 0 ]; then + main_content+=" <a href=\"$BASE_URL/authors/$author_slug/\">$author <span class=\"tag-count\">($post_count)</span></a>"$'\n' + fi + done < "${CACHE_DIR}/authors_with_counts.tmp" + + main_content+="</div>"$'\n' + + # Generate full HTML page for main authors index + local page_title="${MSG_ALL_AUTHORS:-All Authors}" + local page_description="${MSG_ALL_AUTHORS:-All Authors} - $SITE_DESCRIPTION" + local authors_index_rel_url="/authors/" + + # Process templates with placeholder replacement (following tags generator pattern) + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + + # Replace placeholders in the header + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"$page_title"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$page_description"} + header_content=${header_content//\{\{twitter_description\}\}/"$page_description"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"$authors_index_rel_url"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + + # Remove unprocessed image placeholders + header_content=${header_content//\{\{og_image\}\}/} + header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + + # Remove the placeholder for the tag-specific RSS feed link in the main authors index + header_content=${header_content//<!-- bssg:tag_rss_link -->/} + + # Schema.org structured data + local schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"List of all authors on $SITE_TITLE\",\"url\": \"$SITE_URL/authors/\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}" + header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"} + + # Replace placeholders in the footer + local current_year=$(date +%Y) + footer_content=${footer_content//\{\{current_year\}\}/"$current_year"} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"} + + { + echo "$header_content" + echo "$main_content" + echo "$footer_content" + } > "$main_authors_index_output" + + echo "Generated main authors index: $main_authors_index_output" + + # Clean up temporary file + rm -f "${CACHE_DIR}/authors_with_counts.tmp" + else + echo "Main authors index is up to date, skipping..." + fi + + # Clean up author data directory + rm -rf "$author_data_dir" + + echo -e "${GREEN}Author pages processed!${NC}" + echo -e "${GREEN}Generated author list pages.${NC}" +} diff --git a/scripts/build/generate_feeds.sh b/scripts/build/generate_feeds.sh index 4f868e4..19035c8 100755 --- a/scripts/build/generate_feeds.sh +++ b/scripts/build/generate_feeds.sh @@ -14,6 +14,180 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s source "$(dirname "$0")/content.sh" || { echo >&2 "Error: Failed to source content.sh from generate_feeds.sh"; exit 1; } # Note: Needs access to primary_pages and SECONDARY_PAGES which should be exported by templates.sh +declare -gA BSSG_RAM_RSS_FULL_CONTENT_CACHE=() +declare -g BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=false +declare -gA BSSG_RAM_RSS_PUBDATE_CACHE=() +declare -gA BSSG_RAM_RSS_UPDATED_ISO_CACHE=() +declare -gA BSSG_RAM_RSS_URL_CACHE=() +declare -gA BSSG_RAM_RSS_ITEM_XML_CACHE=() +declare -g BSSG_RAM_RSS_METADATA_CACHE_READY=false + +_normalize_relative_url_path() { + local path="$1" + while [[ "$path" == */ ]]; do + path="${path%/}" + done + path="${path#/}" + if [ -z "$path" ]; then + printf '/' + else + printf '/%s/' "$path" + fi +} + +_ram_strip_frontmatter_for_rss() { + awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { in_fm = 0; next; } + } + { if (!in_fm) print; } + ' +} + +_ram_cache_full_content_for_file() { + local file="$1" + local resolved="$file" + + if declare -F ram_mode_resolve_key > /dev/null; then + resolved=$(ram_mode_resolve_key "$file") + fi + + if [[ -z "$resolved" ]]; then + return 1 + fi + + if [[ -n "${BSSG_RAM_RSS_FULL_CONTENT_CACHE[$resolved]+_}" ]]; then + return 0 + fi + + if ! declare -F ram_mode_has_file > /dev/null || ! ram_mode_has_file "$resolved"; then + return 1 + fi + + local raw_content + raw_content=$(ram_mode_get_content "$resolved") + + local stripped_content + stripped_content=$(printf '%s\n' "$raw_content" | _ram_strip_frontmatter_for_rss) + + local converted_html + converted_html=$(convert_markdown_to_html "$stripped_content" "$resolved") + local convert_status=$? + if [ $convert_status -ne 0 ] || [ -z "$converted_html" ]; then + return 1 + fi + + BSSG_RAM_RSS_FULL_CONTENT_CACHE["$resolved"]="$converted_html" + return 0 +} + +prepare_ram_rss_full_content_cache() { + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RSS_INCLUDE_FULL_CONTENT:-false}" != true ]; then + return 0 + fi + + if [ "$BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY" = true ]; then + return 0 + fi + + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=true + return 0 + fi + + local file filename title date lastmod tags slug image image_caption description author_name author_email + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + [ -z "$file" ] && continue + _ram_cache_full_content_for_file "$file" > /dev/null || true + done <<< "$file_index_data" + + BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=true +} + +_ram_prime_rss_metadata_entry() { + local date="$1" + local lastmod="$2" + local slug="$3" + local rss_date_fmt="$4" + local build_timestamp_iso="$5" + local source_file="$6" + + if [ -n "$date" ] && [[ -z "${BSSG_RAM_RSS_PUBDATE_CACHE[$date]+_}" ]]; then + BSSG_RAM_RSS_PUBDATE_CACHE["$date"]=$(format_date "$date" "$rss_date_fmt") + fi + + if [ -n "$lastmod" ] && [[ -z "${BSSG_RAM_RSS_UPDATED_ISO_CACHE[$lastmod]+_}" ]]; then + local updated_date_iso + updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z") + if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then + updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}" + fi + [ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso" + BSSG_RAM_RSS_UPDATED_ISO_CACHE["$lastmod"]="$updated_date_iso" + fi + + if [ -n "$date" ] && [ -n "$slug" ]; then + local url_key="${date}|${slug}" + if [[ -z "${BSSG_RAM_RSS_URL_CACHE[$url_key]+_}" ]]; then + local year month day formatted_path item_url + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo "Warning: Invalid date format '$date' for file $source_file, cannot precompute RSS URL." >&2 + fi + return 1 + fi + formatted_path="${URL_SLUG_FORMAT//Year/$year}" + formatted_path="${formatted_path//Month/$month}" + formatted_path="${formatted_path//Day/$day}" + formatted_path="${formatted_path//slug/$slug}" + item_url=$(_normalize_relative_url_path "$formatted_path") + BSSG_RAM_RSS_URL_CACHE["$url_key"]=$(fix_url "$item_url") + fi + fi + + return 0 +} + +prepare_ram_rss_metadata_cache() { + if [ "${BSSG_RAM_MODE:-false}" != true ]; then + return 0 + fi + + if [ "$BSSG_RAM_RSS_METADATA_CACHE_READY" = true ]; then + return 0 + fi + + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + BSSG_RAM_RSS_METADATA_CACHE_READY=true + return 0 + fi + + local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z" + local build_timestamp_iso + build_timestamp_iso=$(format_date "now" "%Y-%m-%dT%H:%M:%S%z") + if [[ "$build_timestamp_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then + build_timestamp_iso="${build_timestamp_iso::${#build_timestamp_iso}-2}:${BASH_REMATCH[2]}" + fi + + local file filename title date lastmod tags slug image image_caption description author_name author_email + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + [ -z "$file" ] && continue + _ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" >/dev/null || true + done <<< "$file_index_data" + + BSSG_RAM_RSS_METADATA_CACHE_READY=true +} + # Function to get the latest lastmod date from a file index, optionally filtered # Usage: get_latest_mod_date <index_file> [field_index] [filter_pattern] [date_format] # Example: get_latest_mod_date "$file_index" 5 "" "%Y-%m-%d" # Latest overall post @@ -53,6 +227,212 @@ get_latest_mod_date() { fi } +# Fast path for RAM datasets: pick max YYYY-MM-DD from a given field without external sort/head. +_ram_latest_date_from_dataset() { + local dataset="$1" + local field_index="$2" + local date_format="${3:-%Y-%m-%d}" + + local latest_date_str + latest_date_str=$(printf '%s\n' "$dataset" | awk -F'|' -v field_index="$field_index" ' + NF { + value = substr($field_index, 1, 10) + if (value != "" && value > max_date) { + max_date = value + } + } + END { + if (max_date != "") { + print max_date + } + } + ') + + if [ -n "$latest_date_str" ]; then + printf '%s\n' "$latest_date_str" + else + format_date "now" "$date_format" + fi +} + +_generate_sitemap_with_awk_inputs() { + local sitemap="$1" + local file_index_input="$2" + local primary_pages_input="$3" + local secondary_pages_input="$4" + local tags_index_input="$5" + local authors_index_input="$6" + local latest_post_mod_date="$7" + local latest_tag_page_mod_date="$8" + local latest_author_page_mod_date="$9" + local sitemap_date_fmt="${10:-%Y-%m-%d}" + + # Determine the best awk command locally to avoid potential scoping issues with AWK_CMD. + local effective_awk_cmd="awk" + if command -v gawk > /dev/null 2>&1; then + effective_awk_cmd="gawk" + fi + + "$effective_awk_cmd" -v site_url="$SITE_URL" \ + -v url_slug_format="$URL_SLUG_FORMAT" \ + -v latest_post_mod_date="$latest_post_mod_date" \ + -v latest_tag_page_mod_date="$latest_tag_page_mod_date" \ + -v latest_author_page_mod_date="$latest_author_page_mod_date" \ + -v enable_author_pages="${ENABLE_AUTHOR_PAGES:-true}" \ + -v sitemap_date_fmt="$sitemap_date_fmt" \ + -F'|' \ + -f - \ + "$file_index_input" "$primary_pages_input" "$secondary_pages_input" "$tags_index_input" "$authors_index_input" <<'AWK_EOF' > "$sitemap" +# AWK script for sitemap generation. +BEGIN { + OFS = "" + print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + print "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">" + + # Homepage + print " <url>" + print " <loc>" fix_url_awk("/", site_url) "</loc>" + print " <lastmod>" latest_post_mod_date "</lastmod>" + print " <changefreq>daily</changefreq>" + print " <priority>1.0</priority>" + print " </url>" +} + +function fix_url_awk(path, base_url) { + if (substr(path, 1, 1) == "/") { + sub(/\/$/, "", base_url) + sub(/^\/+/, "/", path) + sub(/\/index\.html$/, "/", path) + if (substr(path, length(path), 1) != "/") { + path = path "/" + } + if (base_url == "" || base_url ~ /^http:\/\/localhost(:[0-9]+)?$/) { + return path + } else { + return base_url path + } + } else { + return path + } +} + +# Process file_index (posts). +FILENAME == ARGV[1] { + file = $1 + date = $4 + lastmod = $5 + slug = $7 + if (length(file) == 0 || length(date) == 0 || length(lastmod) == 0 || length(slug) == 0) next + + year = substr(date, 1, 4) + month = substr(date, 6, 2) + day = substr(date, 9, 2) + if (year ~ /^[0-9]{4}$/ && month ~ /^[0-9]{2}$/ && day ~ /^[0-9]{2}$/) { + formatted_path = url_slug_format + gsub(/Year/, year, formatted_path) + gsub(/Month/, month, formatted_path) + gsub(/Day/, day, formatted_path) + gsub(/slug/, slug, formatted_path) + item_url = "/" formatted_path + sub(/\/+$/, "/", item_url) + + mod_time = substr(lastmod, 1, 10) + if (mod_time == "") next + + print " <url>" + print " <loc>" fix_url_awk(item_url, site_url) "</loc>" + print " <lastmod>" mod_time "</lastmod>" + print " <changefreq>weekly</changefreq>" + print " <priority>0.8</priority>" + print " </url>" + } +} + +# Process primary pages. +FILENAME == ARGV[2] { + url = $2 + date = $3 + if (length(url) == 0 || length(date) == 0) next + sitemap_url = url + sub(/index\.html$/, "", sitemap_url) + sub(/\/+$/, "/", sitemap_url) + mod_time = substr(date, 1, 10) + if (mod_time == "") next + print " <url>" + print " <loc>" fix_url_awk(sitemap_url, site_url) "</loc>" + print " <lastmod>" mod_time "</lastmod>" + print " <changefreq>monthly</changefreq>" + print " <priority>0.7</priority>" + print " </url>" +} + +# Process secondary pages. +FILENAME == ARGV[3] { + url = $2 + date = $3 + if (length(url) == 0 || length(date) == 0) next + sitemap_url = url + sub(/index\.html$/, "", sitemap_url) + sub(/\/+$/, "/", sitemap_url) + mod_time = substr(date, 1, 10) + if (mod_time == "") next + print " <url>" + print " <loc>" fix_url_awk(sitemap_url, site_url) "</loc>" + print " <lastmod>" mod_time "</lastmod>" + print " <changefreq>monthly</changefreq>" + print " <priority>0.6</priority>" + print " </url>" +} + +# Process tags index. +FILENAME == ARGV[4] { + tag_slug = $2 + if (length(tag_slug) == 0) next + if (!(tag_slug in processed_tags)) { + processed_tags[tag_slug] = 1 + item_url = "/tags/" tag_slug "/" + print " <url>" + print " <loc>" fix_url_awk(item_url, site_url) "</loc>" + print " <lastmod>" latest_tag_page_mod_date "</lastmod>" + print " <changefreq>weekly</changefreq>" + print " <priority>0.5</priority>" + print " </url>" + } +} + +# Process authors index. +FILENAME == ARGV[5] && enable_author_pages == "true" { + author_slug = $2 + if (length(author_slug) == 0) next + if (!(author_slug in processed_authors)) { + processed_authors[author_slug] = 1 + + if (!authors_index_added) { + authors_index_added = 1 + print " <url>" + print " <loc>" fix_url_awk("/authors/", site_url) "</loc>" + print " <lastmod>" latest_author_page_mod_date "</lastmod>" + print " <changefreq>weekly</changefreq>" + print " <priority>0.6</priority>" + print " </url>" + } + + item_url = "/authors/" author_slug "/" + print " <url>" + print " <loc>" fix_url_awk(item_url, site_url) "</loc>" + print " <lastmod>" latest_author_page_mod_date "</lastmod>" + print " <changefreq>weekly</changefreq>" + print " <priority>0.5</priority>" + print " </url>" + } +} + +END { + print "</urlset>" +} +AWK_EOF +} + # Core RSS generation function # Usage: _generate_rss_feed <output_file> <feed_title> <feed_description> <feed_link_rel> <feed_atom_link_rel> <post_data_input> # <post_data_input> should be a string containing the filtered, sorted, and limited post data, @@ -80,67 +460,95 @@ _generate_rss_feed() { # Ensure output directory exists mkdir -p "$(dirname "$output_file")" - # Create the RSS feed header - cat > "$output_file" << EOF -<?xml version="1.0" encoding="UTF-8" ?> -<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> -<channel> - <title>${feed_title} - $(fix_url "$feed_link_rel") - ${feed_description} - ${SITE_LANG:-en} - $(format_date "now" "$rss_date_fmt") - -EOF + local escaped_feed_title escaped_feed_description feed_link feed_atom_link channel_last_build_date + escaped_feed_title=$(html_escape "$feed_title") + escaped_feed_description=$(html_escape "$feed_description") + feed_link=$(fix_url "$feed_link_rel") + feed_atom_link=$(fix_url "$feed_atom_link_rel") + channel_last_build_date=$(format_date "now" "$rss_date_fmt") + + exec 4> "$output_file" || return 1 + printf '%s\n' \ + '' \ + '' \ + '' \ + " ${escaped_feed_title}" \ + " ${feed_link}" \ + " ${escaped_feed_description}" \ + " ${SITE_LANG:-en}" \ + " ${channel_last_build_date}" \ + " " >&4 # Process the provided post data - echo "$post_data_input" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description; do + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + # Ignore blank trailing lines from callers. + if [ -z "$file" ] && [ -z "$filename" ] && [ -z "$title" ] && [ -z "$date" ] && [ -z "$lastmod" ] && [ -z "$tags" ] && [ -z "$slug" ] && [ -z "$image" ] && [ -z "$image_caption" ] && [ -z "$description" ] && [ -z "$author_name" ] && [ -z "$author_email" ]; then + continue + fi # Skip if essential fields are missing (robustness) if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ] || [ -z "$lastmod" ] || [ -z "$slug" ]; then echo "Warning: Skipping RSS item due to missing fields in input line: file=$file, title=$title, date=$date, lastmod=$lastmod, slug=$slug" >&2 continue fi - # Format dates for RSS - local pub_date=$(format_date "$date" "$rss_date_fmt") - local updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z") - # Convert timezone format again if needed - if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then - updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}" + local rss_item_cache_key="" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + rss_item_cache_key="${RSS_INCLUDE_FULL_CONTENT:-false}|${file}|${date}|${lastmod}|${slug}|${title}" + if [[ -n "${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]+_}" ]]; then + printf '%s' "${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]}" >&4 + continue + fi fi - # Fallback for updated_date_iso - [ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso" - # Construct post URL based on URL_SLUG_FORMAT - local year month day formatted_path item_url - if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then - year="${BASH_REMATCH[1]}" - month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") - day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + # Format dates and URL (RAM mode caches repeated values across many tag feeds). + local pub_date updated_date_iso full_url + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + _ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" || { + echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2 + continue + } + pub_date="${BSSG_RAM_RSS_PUBDATE_CACHE[$date]}" + updated_date_iso="${BSSG_RAM_RSS_UPDATED_ISO_CACHE[$lastmod]}" + full_url="${BSSG_RAM_RSS_URL_CACHE[${date}|${slug}]}" else - echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2 - continue # Skip item if URL cannot be generated - fi - formatted_path="${URL_SLUG_FORMAT//Year/$year}" - formatted_path="${formatted_path//Month/$month}" - formatted_path="${formatted_path//Day/$day}" - formatted_path="${formatted_path//slug/$slug}" - item_url="/$(echo "$formatted_path" | sed 's|/*$|/|')" # Ensure trailing slash + pub_date=$(format_date "$date" "$rss_date_fmt") + updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z") + if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then + updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}" + fi + [ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso" - local full_url=$(fix_url "$item_url") # Use fix_url to prepend SITE_URL + local year month day formatted_path item_url + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2 + continue + fi + formatted_path="${URL_SLUG_FORMAT//Year/$year}" + formatted_path="${formatted_path//Month/$month}" + formatted_path="${formatted_path//Day/$day}" + formatted_path="${formatted_path//slug/$slug}" + item_url=$(_normalize_relative_url_path "$formatted_path") + full_url=$(fix_url "$item_url") + fi # --- RSS Item Description Enhancement --- local item_description_content="" local figure_part="" local caption_part="" local content_part="" + local escaped_title + escaped_title=$(html_escape "$title") # Build figure part if [ -n "$image" ]; then local img_src [[ "$image" =~ ^https?:// ]] && img_src="$image" || img_src=$(fix_url "$image") # Escape alt/title attributes safely using html_escape from utils.sh - local img_alt=$(html_escape "$title") + local img_alt="$escaped_title" local img_title=$(html_escape "$image_caption") [ -z "$img_title" ] && img_title="$img_alt" # Use alt if title is empty @@ -155,8 +563,24 @@ EOF # Build content part (excerpt or full) if [ "${RSS_INCLUDE_FULL_CONTENT:-false}" = true ]; then - local raw_content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")" - if [ -f "$raw_content_cache_file" ]; then + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local resolved_file="$file" + if declare -F ram_mode_resolve_key > /dev/null; then + resolved_file=$(ram_mode_resolve_key "$file") + fi + + if _ram_cache_full_content_for_file "$resolved_file"; then + content_part="${BSSG_RAM_RSS_FULL_CONTENT_CACHE[$resolved_file]}" + else + # RAM mode is memory-only: never fall back to disk cache reads. + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo "Warning: RAM content not available for RSS item ($file). Falling back to excerpt." >&2 + fi + content_part="$description" + fi + else + local raw_content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")" + if [ -f "$raw_content_cache_file" ]; then local raw_content=$(cat "$raw_content_cache_file") local converted_html=$(convert_markdown_to_html "$raw_content" "$file") local convert_status=$? @@ -166,39 +590,64 @@ EOF echo "Warning: Failed to convert markdown to HTML for RSS item ($file, status: $convert_status). Falling back to excerpt." >&2 content_part="$description" fi - else - echo "Warning: Cached raw markdown content file '$raw_content_cache_file' not found for RSS item ($file). Falling back to excerpt." >&2 - content_part="$description" + else + echo "Warning: Cached raw markdown content file '$raw_content_cache_file' not found for RSS item ($file). Falling back to excerpt." >&2 + content_part="$description" + fi fi else content_part="$description" fi # Combine parts safely - item_description_content="${figure_part}${caption_part}${content_part}" + item_description_content="${figure_part}${content_part}" # Wrap final description in CDATA local final_description="" - cat >> "$output_file" << EOF - - ${title} + # Determine author for RSS item (with fallback) + local rss_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + local rss_author_email="${author_email}" + + # Build author element if we have author info + local author_element="" + if [ -n "$rss_author_name" ]; then + if [ -n "$rss_author_email" ]; then + author_element=" $(html_escape "$rss_author_name") ($(html_escape "$rss_author_email"))" + else + author_element=" $(html_escape "$rss_author_name")" + fi + fi + + local rss_item_xml + rss_item_xml=" + ${escaped_title} ${full_url} - ${full_url} + ${full_url} ${pub_date} ${updated_date_iso} ${final_description} - -EOF - done +" + if [ -n "$author_element" ]; then + rss_item_xml+="${author_element}"$'\n' + fi + rss_item_xml+=" +" + + printf '%s' "$rss_item_xml" >&4 + + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + BSSG_RAM_RSS_ITEM_XML_CACHE["$rss_item_cache_key"]="$rss_item_xml" + fi + done <<< "$post_data_input" # Close the RSS feed - cat >> "$output_file" << EOF - - -EOF + printf '%s\n' '' '' >&4 + exec 4>&- - echo -e "${GREEN}RSS feed generated at $output_file${NC}" + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${GREEN}RSS feed generated at $output_file${NC}" + fi } export -f _generate_rss_feed # Export for potential parallel use or sourcing @@ -206,6 +655,28 @@ export -f _generate_rss_feed # Export for potential parallel use or sourcing generate_rss() { echo -e "${YELLOW}Generating main RSS feed...${NC}" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + echo -e "${YELLOW}No file index data in RAM. Skipping RSS generation.${NC}" + return 0 + fi + + prepare_ram_rss_metadata_cache >/dev/null || true + + local rss="$OUTPUT_DIR/${RSS_FILENAME:-rss.xml}" + local feed_title="${MSG_RSS_FEED_TITLE:-${SITE_TITLE} - RSS Feed}" + local feed_desc="${MSG_RSS_FEED_DESCRIPTION:-${SITE_DESCRIPTION}}" + local feed_link_rel="/" + local feed_atom_link_rel="/${RSS_FILENAME:-rss.xml}" + local rss_item_limit=${RSS_ITEM_LIMIT:-15} + local sorted_posts + sorted_posts=$(printf '%s\n' "$file_index_data" | awk 'NF' | sort -t'|' -k4,4r -k5,5r | head -n "$rss_item_limit") + _generate_rss_feed "$rss" "$feed_title" "$feed_desc" "$feed_link_rel" "$feed_atom_link_rel" "$sorted_posts" + return 0 + fi + # Ensure needed functions/vars are available if ! command -v convert_markdown_to_html &> /dev/null; then echo -e "${RED}Error: convert_markdown_to_html function not found.${NC}" >&2; return 1; fi @@ -281,9 +752,43 @@ export -f generate_rss generate_sitemap() { echo -e "${YELLOW}Generating sitemap.xml...${NC}" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local sitemap="$OUTPUT_DIR/sitemap.xml" + local file_index_data tags_index_data authors_index_data primary_pages_data secondary_pages_data + file_index_data=$(ram_mode_get_dataset "file_index") + tags_index_data=$(ram_mode_get_dataset "tags_index") + authors_index_data=$(ram_mode_get_dataset "authors_index") + primary_pages_data=$(ram_mode_get_dataset "primary_pages") + secondary_pages_data=$(ram_mode_get_dataset "secondary_pages") + + local latest_post_mod_date latest_tag_page_mod_date latest_author_page_mod_date + latest_post_mod_date=$(_ram_latest_date_from_dataset "$file_index_data" 5 "%Y-%m-%d") + latest_tag_page_mod_date=$(_ram_latest_date_from_dataset "$tags_index_data" 5 "%Y-%m-%d") + latest_author_page_mod_date=$(_ram_latest_date_from_dataset "$authors_index_data" 6 "%Y-%m-%d") + + [ -z "$latest_tag_page_mod_date" ] && latest_tag_page_mod_date="$latest_post_mod_date" + [ -z "$latest_author_page_mod_date" ] && latest_author_page_mod_date="$latest_post_mod_date" + + _generate_sitemap_with_awk_inputs \ + "$sitemap" \ + <(printf '%s\n' "$file_index_data") \ + <(printf '%s\n' "$primary_pages_data") \ + <(printf '%s\n' "$secondary_pages_data") \ + <(printf '%s\n' "$tags_index_data") \ + <(printf '%s\n' "$authors_index_data") \ + "$latest_post_mod_date" \ + "$latest_tag_page_mod_date" \ + "$latest_author_page_mod_date" \ + "%Y-%m-%d" + + echo -e "${GREEN}Sitemap generated!${NC}" + return 0 + fi + local sitemap="$OUTPUT_DIR/sitemap.xml" local file_index="$CACHE_DIR/file_index.txt" local tags_index="$CACHE_DIR/tags_index.txt" + local authors_index="$CACHE_DIR/authors_index.txt" local primary_pages_cache="$CACHE_DIR/primary_pages.tmp" local secondary_pages_cache="$CACHE_DIR/secondary_pages.tmp" local config_hash_file="$CONFIG_HASH_FILE" # Use the global var @@ -309,6 +814,7 @@ generate_sitemap() { # Check main content index files if [ -f "$file_index" ] && [ "$(get_file_mtime "$file_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi if ! $rebuild_needed && [ -f "$tags_index" ] && [ "$(get_file_mtime "$tags_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi + if ! $rebuild_needed && [ -f "$authors_index" ] && [ "$(get_file_mtime "$authors_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi if ! $rebuild_needed && [ -f "$primary_pages_cache" ] && [ "$(get_file_mtime "$primary_pages_cache")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi if ! $rebuild_needed && [ -f "$secondary_pages_cache" ] && [ "$(get_file_mtime "$secondary_pages_cache")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi # Removed checks for script, config, locale mtime for simplicity to avoid sourcing errors @@ -320,167 +826,28 @@ generate_sitemap() { return 0 fi - # --- Pre-calculate latest dates (Still needed for Homepage/Tags) --- + # --- Pre-calculate latest dates (Still needed for Homepage/Tags/Authors) --- local latest_post_mod_date=$(get_latest_mod_date "$file_index" 5 "" "$sitemap_date_fmt") local latest_tag_page_mod_date=$(get_latest_mod_date "$tags_index" 5 "" "$sitemap_date_fmt") # Assumes lastmod is relevant field in tags_index + local latest_author_page_mod_date=$(get_latest_mod_date "$authors_index" 6 "" "$sitemap_date_fmt") # Field 6 is lastmod in authors_index - # --- Generate Sitemap using AWK --- START --- echo "Generating sitemap content using awk..." - - # Determine the best awk command locally to avoid potential scoping issues with AWK_CMD - local effective_awk_cmd="awk" # Default to standard awk - if command -v gawk > /dev/null 2>&1; then - effective_awk_cmd="gawk" # Prefer gawk if available - fi - - # Use awk with a here-doc for the script for cleaner quoting - # Use the locally determined effective_awk_cmd - "$effective_awk_cmd" -v site_url="$SITE_URL" \ - -v url_slug_format="$URL_SLUG_FORMAT" \ - -v latest_post_mod_date="$latest_post_mod_date" \ - -v latest_tag_page_mod_date="$latest_tag_page_mod_date" \ - -v sitemap_date_fmt="$sitemap_date_fmt" \ - -F'|' \ - -f - \ - "$file_index" "$primary_pages_cache" "$secondary_pages_cache" "$tags_index" <<'AWK_EOF' > "$sitemap" -# AWK script for sitemap generation (fed via here-doc) -BEGIN { - OFS=""; # No output field separator needed for XML - print ""; - print ""; - - # Homepage - print " "; - print " " fix_url_awk("/", site_url) ""; - print " " latest_post_mod_date ""; - print " daily"; - print " 1.0"; - print " "; -} - -# Custom function to replicate fix_url shell function logic -function fix_url_awk(path, base_url) { - if (substr(path, 1, 1) == "/") { - # Remove trailing slash from base_url if present - sub(/\/$/, "", base_url); - # Ensure path doesnt start with // - sub(/^\/+/, "/", path); - # Remove index.html if present - sub(/\/index\.html$/, "/", path); - # Ensure trailing slash - if (substr(path, length(path), 1) != "/") { - path = path "/"; - } - # Handle case where base_url is empty or just http://localhost* - skip prepending - if (base_url == "" || base_url ~ /^http:\/\/localhost(:[0-9]+)?$/) { - return path - } else { - return base_url path; - } - } else { - return path; # Should not happen for sitemap paths? - } -} - -# Process file_index.txt (Posts) -FILENAME == ARGV[1] { - file=$1; filename=$2; title=$3; date=$4; lastmod=$5; tags=$6; slug=$7; - if (length(file) == 0 || length(date) == 0 || length(lastmod) == 0 || length(slug) == 0) next; - - year=substr(date, 1, 4); - month=substr(date, 6, 2); - day=substr(date, 9, 2); - # Ensure valid numbers? Basic check: - if (year ~ /^[0-9]{4}$/ && month ~ /^[0-9]{2}$/ && day ~ /^[0-9]{2}$/) { - formatted_path = url_slug_format; - gsub(/Year/, year, formatted_path); - gsub(/Month/, month, formatted_path); - gsub(/Day/, day, formatted_path); - gsub(/slug/, slug, formatted_path); - item_url = "/" formatted_path; - # Clean URL logic from shell script - sub(/\/+$/, "/", item_url); - - mod_time = substr(lastmod, 1, 10); # Extract YYYY-MM-DD from lastmod ($5) - if (mod_time == "") next; # Skip if date is invalid/empty - - print " "; - print " " fix_url_awk(item_url, site_url) ""; - print " " mod_time ""; - print " weekly"; - print " 0.8"; - print " "; - } -} - -# Process primary_pages.tmp -FILENAME == ARGV[2] { - url=$2; date=$3; # $1=_, $4=source_file - if (length(url) == 0 || length(date) == 0) next; - sitemap_url = url; - sub(/index\.html$/, "", sitemap_url); # Remove index.html - sub(/\/+$/, "/", sitemap_url); # Ensure trailing slash - mod_time = substr(date, 1, 10); # Extract YYYY-MM-DD from date ($3) - if (mod_time == "") next; # Skip if date is invalid/empty - print " "; - print " " fix_url_awk(sitemap_url, site_url) ""; - print " " mod_time ""; - print " monthly"; - print " 0.7"; - print " "; -} - -# Process secondary_pages.tmp -FILENAME == ARGV[3] { - url=$2; date=$3; # $1=_, $4=source_file - if (length(url) == 0 || length(date) == 0) next; - sitemap_url = url; - sub(/index\.html$/, "", sitemap_url); - sub(/\/+$/, "/", sitemap_url); - mod_time = substr(date, 1, 10); # Extract YYYY-MM-DD from date ($3) - if (mod_time == "") next; # Skip if date is invalid/empty - print " "; - print " " fix_url_awk(sitemap_url, site_url) ""; - print " " mod_time ""; - print " monthly"; - print " 0.6"; # Lower priority for secondary? - print " "; -} - -# Process tags_index.txt (Tag Pages) -FILENAME == ARGV[4] { - tag=$1; tag_slug=$2; # $5 = lastmod for posts with this tag - if (length(tag_slug) == 0) next; - # Check if tag slug already processed - if ( !(tag_slug in processed_tags) ) { - processed_tags[tag_slug] = 1; # Mark as processed - item_url = "/tags/" tag_slug "/"; - # Use the overall latest tag mod date for all tag pages? - mod_time = latest_tag_page_mod_date; - print " "; - print " " fix_url_awk(item_url, site_url) ""; - print " " mod_time ""; - print " weekly"; - print " 0.5"; - print " "; - } -} - -END { - print ""; -} -AWK_EOF - # awk exit status check - optional - # local awk_status=$? - # if [ $awk_status -ne 0 ]; then - # echo -e "${RED}Error: awk script for sitemap generation failed with status $awk_status${NC}" >&2 - # # Decide whether to return 1 or continue - # fi - - # --- Generate Sitemap using AWK --- END --- + _generate_sitemap_with_awk_inputs \ + "$sitemap" \ + "$file_index" \ + "$primary_pages_cache" \ + "$secondary_pages_cache" \ + "$tags_index" \ + "$authors_index" \ + "$latest_post_mod_date" \ + "$latest_tag_page_mod_date" \ + "$latest_author_page_mod_date" \ + "$sitemap_date_fmt" echo -e "${GREEN}Sitemap generated!${NC}" } # Export public functions -export -f generate_sitemap generate_rss \ No newline at end of file +export -f _normalize_relative_url_path +export -f _ram_strip_frontmatter_for_rss _ram_cache_full_content_for_file prepare_ram_rss_full_content_cache +export -f generate_sitemap generate_rss diff --git a/scripts/build/generate_index.sh b/scripts/build/generate_index.sh index c2dc92b..2886665 100755 --- a/scripts/build/generate_index.sh +++ b/scripts/build/generate_index.sh @@ -10,8 +10,298 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s # shellcheck source=cache.sh disable=SC1091 source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_index.sh"; exit 1; } +_generate_index_ram() { + echo -e "${YELLOW}Generating index pages...${NC}" + + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + echo -e "${YELLOW}No posts found in RAM file index. Skipping index generation.${NC}" + return 0 + fi + + local total_posts_orig + total_posts_orig=$(printf '%s\n' "$file_index_data" | awk 'NF { c++ } END { print c+0 }') + local total_pages=$(( (total_posts_orig + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE )) + [ "$total_pages" -eq 0 ] && total_pages=1 + + mapfile -t file_index_lines < <(printf '%s\n' "$file_index_data" | awk 'NF') + echo -e "Generating ${GREEN}$total_pages${NC} index pages for ${GREEN}$total_posts_orig${NC} posts" + + local current_page + for (( current_page = 1; current_page <= total_pages; current_page++ )); do + local output_file + if [ "$current_page" -eq 1 ]; then + output_file="$OUTPUT_DIR/index.html" + else + output_file="$OUTPUT_DIR/page/$current_page/index.html" + mkdir -p "$(dirname "$output_file")" + fi + + local page_header="$HEADER_TEMPLATE" + if [ "$current_page" -eq 1 ]; then + page_header=${page_header//\{\{site_title\}\}/"$SITE_TITLE"} + page_header=${page_header//\{\{page_title\}\}/"${MSG_HOME:-"Home"}"} + page_header=${page_header//\{\{og_type\}\}/"website"} + page_header=${page_header//\{\{page_url\}\}/"/"} + page_header=${page_header//\{\{site_url\}\}/"$SITE_URL"} + local home_schema + home_schema=$(cat < +{ + "@context": "https://schema.org", + "@type": "WebSite", + "name": "$SITE_TITLE", + "description": "$SITE_DESCRIPTION", + "url": "$SITE_URL/", + "potentialAction": { + "@type": "SearchAction", + "target": "$SITE_URL/search?q={search_term_string}", + "query-input": "required name=search_term_string" + }, + "publisher": { + "@type": "Organization", + "name": "$SITE_TITLE", + "url": "$SITE_URL" + } +} + +EOF +) + page_header=${page_header//\{\{schema_json_ld\}\}/"$home_schema"} + else + local pag_title + pag_title=$(printf "${MSG_PAGINATION_TITLE:-"%s - Page %d"}" "$SITE_TITLE" "$current_page") + page_header=${page_header//\{\{site_title\}\}/"$SITE_TITLE"} + page_header=${page_header//\{\{page_title\}\}/"$pag_title"} + page_header=${page_header//\{\{og_type\}\}/"website"} + local paginated_rel_url="/page/$current_page/" + page_header=${page_header//\{\{page_url\}\}/"$paginated_rel_url"} + page_header=${page_header//\{\{site_url\}\}/"$SITE_URL"} + local collection_schema + collection_schema=$(cat < +{ + "@context": "https://schema.org", + "@type": "CollectionPage", + "name": "$pag_title", + "description": "$SITE_DESCRIPTION", + "url": "$SITE_URL${paginated_rel_url}", + "isPartOf": { + "@type": "WebSite", + "name": "$SITE_TITLE", + "url": "$SITE_URL" + } +} + +EOF +) + page_header=${page_header//\{\{schema_json_ld\}\}/"$collection_schema"} + fi + page_header=${page_header//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + page_header=${page_header//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + page_header=${page_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + page_header=${page_header//\{\{og_image\}\}/""} + page_header=${page_header//\{\{twitter_image\}\}/""} + page_header=${page_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + + local page_footer="$FOOTER_TEMPLATE" + page_footer=${page_footer//\{\{current_year\}\}/$(date +%Y)} + page_footer=${page_footer//\{\{author_name\}\}/"$AUTHOR_NAME"} + + cat > "$output_file" < /dev/null && ram_mode_has_file "$index_file"; then + has_custom_index=true + elif [ -f "$index_file" ]; then + has_custom_index=true + fi + + if [ "$current_page" -eq 1 ] && [ "$has_custom_index" = true ]; then + local content="" html_content="" in_frontmatter=false found_frontmatter=false source_stream="" + if [ "${BSSG_RAM_MODE:-false}" = true ] && ram_mode_has_file "$index_file"; then + source_stream=$(ram_mode_get_content "$index_file") + else + source_stream=$(cat "$index_file") + fi + while IFS= read -r line; do + if [[ "$line" == "---" ]]; then + if ! $in_frontmatter && ! $found_frontmatter; then + in_frontmatter=true + found_frontmatter=true + continue + elif $in_frontmatter; then + in_frontmatter=false + continue + fi + fi + if ! $in_frontmatter && $found_frontmatter; then + content+="$line"$'\n' + fi + done <<< "$source_stream" + if ! $found_frontmatter; then + content="$source_stream" + fi + html_content=$(convert_markdown_to_html "$content") + echo "$html_content" >> "$output_file" + cat >> "$output_file" <> "$output_file" <${MSG_LATEST_POSTS:-"Latest Posts"} +
+EOF + local start_index=$(( (current_page - 1) * POSTS_PER_PAGE )) + local end_index=$(( start_index + POSTS_PER_PAGE - 1 )) + local i + for (( i = start_index; i <= end_index && i < total_posts_orig; i++ )); do + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "${file_index_lines[$i]}" + [ -z "$file" ] && continue + [ -z "$title" ] && continue + [ -z "$date" ] && continue + + local post_year post_month post_day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d) + fi + local formatted_path="${URL_SLUG_FORMAT//Year/$post_year}" + formatted_path="${formatted_path//Month/$post_month}" + formatted_path="${formatted_path//Day/$post_day}" + formatted_path="${formatted_path//slug/$slug}" + local post_link="/$formatted_path/" + + local display_date_format="$DATE_FORMAT" + if [ "${SHOW_TIMEZONE:-false}" = false ]; then + display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//') + fi + local formatted_date + formatted_date=$(format_date "$date" "$display_date_format") + + cat >> "$output_file" < +

$title

+
${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${author_name:+" ${MSG_BY:-"by"} ${author_name:-$AUTHOR_NAME}"}
+EOF + + if [ -n "$image" ]; then + local image_url="$image" + if [[ "$image" == /* ]]; then + image_url="${SITE_URL}${image}" + fi + cat >> "$output_file" < + + ${image_caption:-$title} + +
+EOF + fi + + if [ "${INDEX_SHOW_FULL_CONTENT:-false}" = "true" ]; then + local post_content="" html_content="" + if [ "${BSSG_RAM_MODE:-false}" = true ] && ram_mode_has_file "$file"; then + local source_stream + source_stream=$(ram_mode_get_content "$file") + post_content=$(printf '%s\n' "$source_stream" | awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { in_fm = 0; next; } + } + { if (!in_fm) print; } + ') + fi + if [ -n "$post_content" ]; then + if [[ "$file" == *.md ]]; then + html_content=$(convert_markdown_to_html "$post_content") + else + html_content="$post_content" + fi + fi + if [ -n "$html_content" ]; then + cat >> "$output_file" < + $html_content +
+EOF + fi + elif [ -n "$description" ]; then + cat >> "$output_file" < + $description + +EOF + fi + + cat >> "$output_file" < +EOF + done + + cat >> "$output_file" < +EOF + + if [ "$total_pages" -gt 1 ]; then + cat >> "$output_file" < + EOF fi - if [ -n "$description" ]; then + # Show either full content or just description based on config + if [ "${INDEX_SHOW_FULL_CONTENT:-false}" = "true" ]; then + # Show full post content + local post_content="" + local content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")" + + # Try RAM preload first + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then + local source_stream + source_stream=$(ram_mode_get_content "$file") + post_content=$(printf '%s\n' "$source_stream" | awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { in_fm = 0; next; } + } + { if (!in_fm) print; } + ') + # Try to get content from cache first + elif [ -f "$content_cache_file" ]; then + post_content=$(cat "$content_cache_file") + else + # Extract content from source file if cache doesn't exist + local in_frontmatter=false + local found_frontmatter=false + { + while IFS= read -r line; do + if [[ "$line" == "---" ]]; then + if ! $in_frontmatter && ! $found_frontmatter; then + in_frontmatter=true + found_frontmatter=true + continue + elif $in_frontmatter; then + in_frontmatter=false + continue + fi + fi + if ! $in_frontmatter && $found_frontmatter; then + post_content+="$line"$'\n' + fi + done + } < "$file" + + # If no frontmatter was found, use the whole file + if ! $found_frontmatter; then + post_content=$(cat "$file") + fi + fi + + # Convert to HTML if it's a markdown file + local html_content="" + if [[ "$file" == *.md ]]; then + html_content=$(convert_markdown_to_html "$post_content") + elif [[ "$file" == *.html ]]; then + # For HTML files, content is already HTML + html_content=$(sed -n '//,/<\/body>/p' "$file" | sed '1d;$d') + # If body extraction failed, use content as-is + if [ -z "$html_content" ]; then + html_content="$post_content" + fi + else + html_content="$post_content" + fi + + if [ -n "$html_content" ]; then + cat >> "$output_file" << EOF +
+ $html_content +
+EOF + fi + elif [ -n "$description" ]; then + # Show just the description/excerpt (default behavior) cat >> "$output_file" << EOF
$description @@ -294,9 +657,8 @@ EOF # Use GNU parallel if available and beneficial if [ "${HAS_PARALLEL:-false}" = true ] && [ "$total_pages" -gt 2 ] ; then echo -e "${GREEN}Using GNU parallel to process index pages${NC}" - local cores=1 - if command -v nproc > /dev/null 2>&1; then cores=$(nproc); - elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi + local cores + cores=$(get_parallel_jobs) # Use all detected cores local jobs=$cores @@ -305,10 +667,11 @@ EOF export OUTPUT_DIR URL_SLUG_FORMAT POSTS_PER_PAGE CACHE_DIR export SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME DATE_FORMAT SITE_URL export FORCE_REBUILD HEADER_TEMPLATE FOOTER_TEMPLATE SHOW_TIMEZONE - export MSG_LATEST_POSTS MSG_HOME MSG_PAGINATION_TITLE MSG_PUBLISHED_ON MSG_BY + export INDEX_SHOW_FULL_CONTENT + export MSG_LATEST_POSTS MSG_HOME MSG_PAGINATION_TITLE MSG_PUBLISHED_ON MSG_BY export MSG_NEWER_POSTS MSG_OLDER_POSTS MSG_PAGE_INFO_TEMPLATE # Note: total_posts_orig is NOT exported, passed as argument now - export -f process_index_page file_needs_rebuild get_file_mtime format_date generate_slug fix_url + export -f process_index_page file_needs_rebuild get_file_mtime format_date generate_slug fix_url convert_markdown_to_html # Ensure templates are exported if [ -z "$HEADER_TEMPLATE" ] || [ -z "$FOOTER_TEMPLATE" ]; then @@ -332,4 +695,4 @@ EOF } # Make the function available for sourcing -export -f generate_index \ No newline at end of file +export -f generate_index diff --git a/scripts/build/generate_pages.sh b/scripts/build/generate_pages.sh index cb4599a..2e4295f 100755 --- a/scripts/build/generate_pages.sh +++ b/scripts/build/generate_pages.sh @@ -24,9 +24,13 @@ convert_page() { # IMPORTANT: Assumes CACHE_DIR, FORCE_REBUILD, PAGES_DIR, SITE_TITLE, SITE_DESCRIPTION, SITE_URL, AUTHOR_NAME are exported/available local output_html_file="$output_base_path/index.html" + local ram_mode_active=false + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$input_file"; then + ram_mode_active=true + fi # Check if the source file exists - if [ ! -f "$input_file" ]; then + if ! $ram_mode_active && [ ! -f "$input_file" ]; then echo -e "${RED}Error: Source page '$input_file' not found${NC}" >&2 return 1 fi @@ -45,21 +49,31 @@ convert_page() { if [[ "$input_file" == *.html ]]; then # For HTML files, extract content between tags (simple approach) - html_content=$(sed -n '//,/<\/body>/p' "$input_file" | sed '1d;$d') + local html_source="" + if $ram_mode_active; then + html_source=$(ram_mode_get_content "$input_file") + else + html_source=$(cat "$input_file") + fi + html_content=$(printf '%s\n' "$html_source" | sed -n '//,/<\/body>/p' | sed '1d;$d') # We might not have raw content for reading time easily here content=$(echo "$html_content" | sed 's/<[^>]*>//g') # Basic text extraction for reading time else # For markdown files, extract content after frontmatter - local start_line=$(grep -n "^---$" "$input_file" | head -1 | cut -d: -f1) - local end_line=$(grep -n "^---$" "$input_file" | head -2 | tail -1 | cut -d: -f1) - - if [[ -z "$start_line" || -z "$end_line" || ! $start_line -lt $end_line ]]; then - # No valid frontmatter found, use the whole file - content=$(cat "$input_file") + local source_stream="" + if $ram_mode_active; then + source_stream=$(ram_mode_get_content "$input_file") else - # Extract content after the second --- line - content=$(tail -n +$((end_line + 1)) "$input_file") + source_stream=$(cat "$input_file") fi + content=$(printf '%s\n' "$source_stream" | awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { in_fm = 0; next; } + } + { if (!in_fm) print; } + ') # --- MODIFIED PART --- START --- # Convert markdown content to HTML using the function from content.sh @@ -111,6 +125,7 @@ convert_page() { # Handle image placeholders (remove for pages as they don't have featured images) header_content=${header_content//\{\{og_image\}\}/} header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Assemble the final HTML local final_html="${header_content}" @@ -178,10 +193,13 @@ process_all_pages() { return 0 fi - echo -e "Checking ${GREEN}${#page_files[@]}${NC} pages for changes" - # Use mapfile -t to read sorted files into array (newline-separated, trailing newline stripped) - mapfile -t page_files < <(find "${PAGES_DIR:-pages}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort) + local page_files=() + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_list_page_files > /dev/null; then + mapfile -t page_files < <(ram_mode_list_page_files) + else + mapfile -t page_files < <(find "${PAGES_DIR:-pages}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort) + fi local num_pages=${#page_files[@]} if [ "$num_pages" -eq 0 ]; then @@ -190,14 +208,40 @@ process_all_pages() { fi echo -e "Found ${GREEN}$num_pages${NC} potential pages." + local ram_mode_active=false + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_active=true + fi + + # RAM mode keeps source content only in-process (bash arrays). + # GNU parallel spawns fresh shells that cannot access those arrays. + if $ram_mode_active; then + if [ "$num_pages" -gt 1 ]; then + echo -e "${YELLOW}Using shell parallel workers for $num_pages RAM-mode pages${NC}" + local cores + cores=$(get_parallel_jobs) + + { + local file quoted_file + for file in "${page_files[@]}"; do + [[ -z "$file" ]] && continue + printf -v quoted_file '%q' "$file" + echo "process_single_page_file $quoted_file" + done + } | run_parallel "$cores" + else + echo -e "${YELLOW}Using sequential processing for RAM-mode pages${NC}" + if [[ -n "${page_files[0]}" ]]; then + process_single_page_file "${page_files[0]}" + fi + fi # Use GNU parallel if available, otherwise fallback # IMPORTANT: Assumes HAS_PARALLEL is exported/available - if [ "${HAS_PARALLEL:-false}" = true ]; then + elif [ "${HAS_PARALLEL:-false}" = true ]; then echo -e "${GREEN}Using GNU parallel to generate pages${NC}" # Determine number of cores - local cores=1 - if command -v nproc > /dev/null 2>&1; then cores=$(nproc); - elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi + local cores + cores=$(get_parallel_jobs) # Export functions needed by the parallel process and its children export -f convert_page process_single_page_file @@ -216,6 +260,7 @@ process_all_pages() { echo -e "${YELLOW}Using sequential processing for pages${NC}" local file for file in "${page_files[@]}"; do + [[ -z "$file" ]] && continue process_single_page_file "$file" done fi @@ -223,4 +268,4 @@ process_all_pages() { echo -e "${GREEN}Static page processing complete!${NC}" } -# --- Page Generation Functions --- END --- \ No newline at end of file +# --- Page Generation Functions --- END --- diff --git a/scripts/build/generate_posts.sh b/scripts/build/generate_posts.sh index 7a82233..2c962fb 100755 --- a/scripts/build/generate_posts.sh +++ b/scripts/build/generate_posts.sh @@ -11,9 +11,68 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s source "$(dirname "$0")/content.sh" || { echo >&2 "Error: Failed to source content.sh from generate_posts.sh"; exit 1; } # shellcheck source=cache.sh disable=SC1091 source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_posts.sh"; exit 1; } # For file_needs_rebuild checks etc. +# shellcheck source=related_posts.sh disable=SC1091 +source "$(dirname "$0")/related_posts.sh" || { echo >&2 "Error: Failed to source related_posts.sh from generate_posts.sh"; exit 1; } # For related posts functionality # --- Post Generation Functions --- START --- +declare -gA BSSG_POST_ISO8601_CACHE=() + +format_iso8601_post_date() { + local input_dt="$1" + local iso_dt="" + + if [ -z "$input_dt" ]; then + echo "" + return + fi + + local cache_key="${TIMEZONE:-local}|${input_dt}" + if [[ "$(declare -p BSSG_POST_ISO8601_CACHE 2>/dev/null || true)" != "declare -A"* ]]; then + unset BSSG_POST_ISO8601_CACHE 2>/dev/null || true + declare -gA BSSG_POST_ISO8601_CACHE=() + fi + if [[ -n "${BSSG_POST_ISO8601_CACHE[$cache_key]+_}" ]]; then + echo "${BSSG_POST_ISO8601_CACHE[$cache_key]}" + return + fi + + # Handle "now" separately + if [ "$input_dt" = "now" ]; then + iso_dt=$(LC_ALL=C date +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) + else + # Try parsing different formats based on OS + if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == *"bsd"* ]]; then + # Format 1: YYYY-MM-DD HH:MM:SS ZZZZ (e.g., +0200) + iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) + # Format 2: YYYY-MM-DD HH:MM:SS + [ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) + # Format 3: YYYY-MM-DD (assume T00:00:00) + [ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d" "$input_dt" +"%Y-%m-%dT00:00:00%z" 2>/dev/null) + # Format 4: RFC 2822 subset (e.g., 07 Sep 2023 08:10:00 +0200) + [ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%d %b %Y %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) + else + # GNU date -d handles many formats. + iso_dt=$(LC_ALL=C date -d "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) + fi + fi + + # Normalize timezone from +0000 to Z and +hhmm to +hh:mm. + if [ -n "$iso_dt" ] && [[ "$iso_dt" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then + local tz_offset="${BASH_REMATCH[0]}" + local tz_hh="${BASH_REMATCH[1]}" + local tz_mm="${BASH_REMATCH[2]}" + if [ "$tz_hh" = "+00" ] && [ "$tz_mm" = "00" ]; then + iso_dt="${iso_dt%$tz_offset}Z" + else + iso_dt="${iso_dt%$tz_offset}${tz_hh}:${tz_mm}" + fi + fi + + BSSG_POST_ISO8601_CACHE["$cache_key"]="$iso_dt" + echo "$iso_dt" +} + # Convert markdown to HTML convert_markdown() { local input_file="$1" @@ -26,61 +85,85 @@ convert_markdown() { local image="$8" local image_caption="$9" local description="${10}" + local author_name="${11}" + local author_email="${12}" + local skip_rebuild_check="${13:-false}" local content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$input_file")" local output_html_file="$output_base_path/index.html" + local ram_mode_active=false + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$input_file"; then + ram_mode_active=true + fi # Check if the source file exists - if [ ! -f "$input_file" ]; then + if ! $ram_mode_active && [ ! -f "$input_file" ]; then echo -e "${RED}Error: Source file '$input_file' not found${NC}" >&2 return 1 fi - # Skip if output file is newer than input file and no force rebuild - if ! file_needs_rebuild "$input_file" "$output_html_file"; then - echo -e "Skipping unchanged file: ${YELLOW}$(basename "$input_file")${NC}" - return 0 + # Skip if output file is newer than input file and no force rebuild. + # When callers already prefiltered rebuild candidates, this check can be skipped. + if [ "$skip_rebuild_check" != true ]; then + if ! file_needs_rebuild "$input_file" "$output_html_file"; then + echo -e "Skipping unchanged file: ${YELLOW}$(basename "$input_file")${NC}" + return 0 + fi fi - echo -e "Processing post: ${GREEN}$(basename "$input_file")${NC}" + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "Processing post: ${GREEN}$(basename "$input_file")${NC}" + fi - # IMPORTANT: Assumes lock_file/unlock_file are sourced/available - lock_file "$content_cache_file" - - # Try to get content from cache or file + # Extract body content (without frontmatter) in one awk pass. + # This is materially faster than line-by-line bash parsing on large markdown files. local content="" - local in_frontmatter=false - local found_frontmatter=false - { - while IFS= read -r line; do - if [[ "$line" == "---" ]]; then - if ! $in_frontmatter && ! $found_frontmatter; then - in_frontmatter=true - found_frontmatter=true - continue - elif $in_frontmatter; then - in_frontmatter=false - continue # Skip the closing --- line itself - fi - fi - if ! $in_frontmatter && $found_frontmatter; then - content+="$line"$'\n' - fi - done - } < "$input_file" - - # If no frontmatter was found, use the whole file as content - if ! $found_frontmatter; then - content=$(cat "$input_file") + local source_stream="" + local fediverse_creator_override="" + if $ram_mode_active; then + source_stream=$(ram_mode_get_content "$input_file") + else + source_stream=$(cat "$input_file") fi + if [[ "$input_file" == *.html ]]; then + fediverse_creator_override=$(printf '%s\n' "$source_stream" | grep -m 1 -o 'name="fediverse_creator" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + if [ -z "$fediverse_creator_override" ]; then + fediverse_creator_override=$(printf '%s\n' "$source_stream" | grep -m 1 -o 'name="fediverse:creator" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + fi + else + fediverse_creator_override=$(parse_metadata "$input_file" "fediverse_creator") + fi + content=$(printf '%s' "$source_stream" | awk ' + NR == 1 { + if ($0 == "---") { + has_frontmatter = 1 + in_frontmatter = 1 + next + } + } + + { + if (has_frontmatter) { + if (in_frontmatter) { + if ($0 == "---") { + in_frontmatter = 0 + } + next + } + print + } else { + print + } + } + ') # Cache the markdown content *without frontmatter* for potential use in RSS full content - if [ -n "$CACHE_DIR" ] && [ -d "${CACHE_DIR}/content" ]; then + if ! $ram_mode_active && [ -n "$CACHE_DIR" ] && [ -d "${CACHE_DIR}/content" ]; then # Write the $content variable (which has frontmatter removed) to the cache file + lock_file "$content_cache_file" printf '%s' "$content" > "$content_cache_file" + unlock_file "$content_cache_file" fi - - unlock_file "$content_cache_file" # Calculate reading time local reading_time @@ -118,7 +201,7 @@ convert_markdown() { [[ -z "$tag" ]] && continue local tag_slug=$(echo "$tag" | tr '[:upper:]' '[:lower:]' | sed -e 's/ /-/g' -e 's/[^a-z0-9-]//g') if [[ -n "$tag_slug" ]]; then # Ensure tag slug is not empty - tags_html+=$(printf ' %s' "${SITE_URL:-}" "$tag_slug" "$tag") + tags_html+=" ${tag}" fi done tags_html+="
" @@ -168,68 +251,34 @@ convert_markdown() { meta_desc=$(echo "${description:-$SITE_DESCRIPTION}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') header_content=${header_content//\{\{og_description\}\}/"$meta_desc"} header_content=${header_content//\{\{twitter_description\}\}/"$meta_desc"} + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + local fediverse_creator_meta_tag="" + fediverse_creator_meta_tag=$(build_fediverse_creator_meta_tag "$display_author_name" "$fediverse_creator_override") + if [[ "$header_content" == *"{{fediverse_creator_meta}}"* ]]; then + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"$fediverse_creator_meta_tag"} + elif [ -n "$fediverse_creator_meta_tag" ]; then + if [[ "$header_content" == *""* ]]; then + header_content=${header_content/<\/head>/$'\n'"$fediverse_creator_meta_tag"$'\n'''} + else + header_content+=$'\n'"$fediverse_creator_meta_tag" + fi + fi # Generate Schema.org JSON-LD for articles local schema_json_ld="" if [ -n "$date" ]; then local iso_date iso_lastmod_date - # Function to format date to ISO 8601 with corrected timezone - format_iso8601() { - local input_dt="$1" - local iso_dt="" - if [ -z "$input_dt" ]; then echo ""; return; fi - - # Handle "now" separately - if [ "$input_dt" = "now" ]; then - iso_dt=$(LC_ALL=C date +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - else - # Try parsing different formats based on OS - # Add LC_ALL=C for consistent parsing - if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == *"bsd"* ]]; then - # macOS/BSD: Try formats one by one with date -j -f - # Format 1: YYYY-MM-DD HH:MM:SS ZZZZ (e.g., +0200) - iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - # Format 2: YYYY-MM-DD HH:MM:SS - [ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - # Format 3: YYYY-MM-DD (assume T00:00:00) - [ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d" "$input_dt" +"%Y-%m-%dT00:00:00%z" 2>/dev/null) - # Format 4: RFC 2822 subset (e.g., 07 Sep 2023 08:10:00 +0200) - [ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%d %b %Y %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - else # Linux - # GNU date -d is more flexible and handles many formats automatically - iso_dt=$(LC_ALL=C date -d "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - fi - fi - - # If parsing succeeded, fix timezone format - if [ -n "$iso_dt" ]; then - # Fix timezone format from +0000 to +00:00 or Z - if [[ "$iso_dt" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then - local tz_offset="${BASH_REMATCH[0]}" - local tz_hh="${BASH_REMATCH[1]}" - local tz_mm="${BASH_REMATCH[2]}" - if [ "$tz_hh" == "+00" ] && [ "$tz_mm" == "00" ]; then - iso_dt="${iso_dt%$tz_offset}Z" - else - iso_dt="${iso_dt%$tz_offset}${tz_hh}:${tz_mm}" - fi - fi - echo "$iso_dt" - else - echo "" # Return empty if formatting failed - fi - } - - iso_date=$(format_iso8601 "$date") + iso_date=$(format_iso8601_post_date "$date") # Use date as fallback for lastmod, then format - iso_lastmod_date=$(format_iso8601 "${lastmod:-$date}") + iso_lastmod_date=$(format_iso8601_post_date "${lastmod:-$date}") # If lastmod still empty, use iso_date as fallback [ -z "$iso_lastmod_date" ] && iso_lastmod_date="$iso_date" # Fallback to build time if both are empty (should be rare) if [ -z "$iso_date" ]; then - local now_iso=$(format_iso8601 "now") + local now_iso + now_iso=$(format_iso8601_post_date "now") iso_date="$now_iso" iso_lastmod_date="$now_iso" fi @@ -239,13 +288,23 @@ convert_markdown() { image_url=$(fix_url "$image") fi - # Create JSON-LD - schema_json_ld=$(printf '' \ + # Create JSON-LD using post-specific author info + local post_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + local post_author_email="${author_email:-${AUTHOR_EMAIL:-anonymous@example.com}}" + + # Build author JSON - only include email if it's provided + local author_json + if [ -n "$author_email" ]; then + author_json=$(printf '{\n "@type": "Person",\n "name": "%s",\n "email": "%s"\n }' "$post_author_name" "$post_author_email") + else + author_json=$(printf '{\n "@type": "Person",\n "name": "%s"\n }' "$post_author_name") + fi + + schema_json_ld=$(printf '' \ "$(echo "$title" | sed 's/"/\"/g')" \ "$iso_date" \ "$iso_lastmod_date" \ - "${AUTHOR_NAME:-Anonymous}" \ - "${AUTHOR_EMAIL:-anonymous@example.com}" \ + "$author_json" \ "$SITE_TITLE" \ "$SITE_URL" \ "$(echo "$meta_desc" | sed 's/"/\"/g')" \ @@ -281,11 +340,18 @@ convert_markdown() { local formatted_lastmod=$(format_date "$lastmod" "$display_date_format") local post_meta_reading_time post_meta_reading_time=$(printf "${MSG_READING_TIME_TEMPLATE:-%d min read}" "$reading_time") - local post_meta="
${MSG_PUBLISHED_ON:-Published on}: $formatted_date" + local post_meta="
" + post_meta+="

" + post_meta+="${MSG_PUBLISHED_ON:-Published on}: ${MSG_BY:-by} $display_author_name" + post_meta+="

" if [ "$formatted_date" != "$formatted_lastmod" ]; then - post_meta+=" • ${MSG_UPDATED_ON:-Updated on}: $formatted_lastmod" + post_meta+="

" + post_meta+="${MSG_UPDATED_ON:-Updated on}: • $post_meta_reading_time" + post_meta+="

" + else + post_meta+="

$post_meta_reading_time

" fi - post_meta+=" • $post_meta_reading_time
" + post_meta+="
" # Construct featured image HTML local image_html="" @@ -294,14 +360,47 @@ convert_markdown() { image_html="
\"$alt_text\"
${image_caption:-$title}
" fi + # Generate related posts if enabled and tags exist + local related_posts_html="" + if [ "${ENABLE_RELATED_POSTS:-true}" = true ] && [ -n "$tags" ]; then + # RAM fast path: direct map lookup avoids per-post command-substitution/function overhead. + if [ "${BSSG_RAM_MODE:-false}" = true ] && \ + [ "${BSSG_RAM_RELATED_POSTS_READY:-false}" = true ] && \ + [ "${BSSG_RAM_RELATED_POSTS_LIMIT:-}" = "${RELATED_POSTS_COUNT:-3}" ]; then + related_posts_html="${BSSG_RAM_RELATED_POSTS_HTML[$slug]-}" + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${BLUE}DEBUG: Generating related posts for $slug with tags: $tags${NC}" + fi + else + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${BLUE}DEBUG: Generating related posts for $slug with tags: $tags${NC}" + fi + related_posts_html=$(generate_related_posts "$slug" "$tags" "$date" "${RELATED_POSTS_COUNT:-3}") + fi + else + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${BLUE}DEBUG: Skipping related posts for $slug - ENABLE_RELATED_POSTS=${ENABLE_RELATED_POSTS:-true}, tags=$tags${NC}" + fi + fi + # Construct article body local final_html="${header_content}" - final_html+=$(printf '
\n

%s

\n%s\n%s\n%s\n%s\n
\n' "$title" "$post_meta" "$image_html" "$html_content" "$tags_html") + final_html+='
'$'\n' + final_html+="

$title

"$'\n' + final_html+="$post_meta"$'\n' + final_html+="$image_html"$'\n' + final_html+="$html_content"$'\n' + final_html+="$tags_html"$'\n' + if [ -n "$related_posts_html" ]; then + final_html+="$related_posts_html"$'\n' + fi + final_html+='
'$'\n' # Replace placeholders in footer content local current_year=$(date +'%Y') + local post_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" footer_content=${footer_content//\{\{current_year\}\}/$current_year} - footer_content=${footer_content//\{\{author_name\}\}/${AUTHOR_NAME:-Anonymous}} + footer_content=${footer_content//\{\{author_name\}\}/$post_author_name} final_html+="${footer_content}" @@ -325,131 +424,300 @@ process_all_markdown_files() { local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" local modified_tags_list="${CACHE_DIR:-.bssg_cache}/modified_tags.list" # Define path for modified tags + local modified_authors_list="${CACHE_DIR:-.bssg_cache}/modified_authors.list" # Define path for modified authors local file_index_prev="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" # Path to previous index + local ram_mode_active=false + local file_index_data="" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_active=true + file_index_data=$(ram_mode_get_dataset "file_index") + fi - if [ ! -f "$file_index" ]; then + if ! $ram_mode_active && [ ! -f "$file_index" ]; then echo -e "${RED}Error: File index not found at '$file_index'. Run indexing first.${NC}" >&2 return 1 fi - local total_file_count=$(wc -l < "$file_index") + local total_file_count=0 + if $ram_mode_active; then + total_file_count=$(printf '%s\n' "$file_index_data" | awk 'NF { c++ } END { print c+0 }') + else + total_file_count=$(wc -l < "$file_index") + fi if [ "$total_file_count" -eq 0 ]; then echo -e "${YELLOW}No posts found in file index. Skipping post generation.${NC}" return 0 fi echo -e "Checking ${GREEN}$total_file_count${NC} potential posts listed in index." - # --- Start Change: Clear previous modified tags list --- - echo "Clearing previous modified tags list: $modified_tags_list" >&2 # Debug message - rm -f "$modified_tags_list" - touch "$modified_tags_list" # Ensure file exists even if empty - # --- End Change --- + # --- OPTIMIZATION: Quick check if any posts need rebuilding --- + local needs_pass1=false + local posts_needing_rebuild=0 + + # Only do expensive Pass 1 if related posts are enabled AND posts might need rebuilding + if [ "${ENABLE_RELATED_POSTS:-true}" = true ] && ! $ram_mode_active; then + echo -e "${BLUE}DEBUG: Related posts enabled, starting quick scan...${NC}" + # Quick scan to see if ANY posts need rebuilding before doing expensive Pass 1 + echo -e "${YELLOW}Quick scan: Checking if any posts need rebuilding...${NC}" + + while IFS= read -r line; do + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" + + # Basic check if it looks like a post + if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then + continue + fi + + # Calculate expected output path + local year month day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + year=$(date +%Y); month=$(date +%m); day=$(date +%d) + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; + url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" + local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html" + + # Quick rebuild check + common_rebuild_check "$output_html_file" + local common_result=$? + local needs_rebuild=false + + if [ $common_result -eq 0 ]; then + needs_rebuild=true + else + local input_time=$(get_file_mtime "$file") + local output_time=$(get_file_mtime "$output_html_file") + if (( input_time > output_time )); then + needs_rebuild=true + fi + fi + + if $needs_rebuild; then + posts_needing_rebuild=$((posts_needing_rebuild + 1)) + needs_pass1=true + # Early exit optimization: if we find posts needing rebuild, we need Pass 1 + break + fi + done < <( + if $ram_mode_active; then + printf '%s\n' "$file_index_data" | awk 'NF' + else + cat "$file_index" + fi + ) + + echo -e "Quick scan result: ${GREEN}$posts_needing_rebuild${NC} posts need rebuilding" + fi + + # --- PASS 1: Only run if needed (posts need rebuilding AND related posts enabled) --- + if [ "$needs_pass1" = true ] && [ "${ENABLE_RELATED_POSTS:-true}" = true ] && ! $ram_mode_active; then + echo -e "${BLUE}DEBUG: Both needs_pass1=true and ENABLE_RELATED_POSTS=true, running Pass 1...${NC}" + echo -e "${YELLOW}Pass 1: Identifying modified tags for related posts cache invalidation...${NC}" + + # Clear previous modified tags lists + rm -f "$modified_tags_list" + rm -f "$modified_authors_list" + touch "$modified_tags_list" # Ensure file exists even if empty + touch "$modified_authors_list" # Ensure file exists even if empty + + while IFS= read -r line; do + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" + + # Basic check if it looks like a post + if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then + continue + fi + + # Calculate expected output path + local year month day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + year=$(date +%Y); month=$(date +%m); day=$(date +%d) + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; + url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" + local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html" + + # Perform the rebuild check here + common_rebuild_check "$output_html_file" + local common_result=$? + local needs_rebuild=false + + if [ $common_result -eq 0 ]; then + needs_rebuild=true # Common checks failed (config changed, template newer, output missing) + else # common_result is 2 (output exists and newer than templates/locale) + local input_time=$(get_file_mtime "$file") + local output_time=$(get_file_mtime "$output_html_file") + if (( input_time > output_time )); then + needs_rebuild=true # Input file is newer + fi + fi + + # If post needs rebuilding, add its tags to the modified list + if $needs_rebuild; then + local new_tags="$tags" + local old_tags="" + # Try to get old tags from the previous index snapshot + if [ -f "$file_index_prev" ]; then + old_tags=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f6) + fi + + # Combine old and new tags + local combined_tags="${old_tags},${new_tags}" + + if [ -n "$combined_tags" ]; then + # Split by comma, trim, filter empty, sort unique, and add each tag on a new line + echo "$combined_tags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . | sort -u >> "$modified_tags_list" + fi + + # Track modified authors (similar logic to tags) + local new_author="$author_name" + local old_author="" + if [ -f "$file_index_prev" ]; then + old_author=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f11) + fi + + # Add both old and new authors to the modified list (if they exist) + if [ -n "$old_author" ] && [ "$old_author" != "" ]; then + echo "$old_author" >> "$modified_authors_list" + fi + if [ -n "$new_author" ] && [ "$new_author" != "" ]; then + echo "$new_author" >> "$modified_authors_list" + fi + fi + done < "$file_index" + + # Unique sort the modified tags and authors lists + if [ -f "$modified_tags_list" ]; then + local temp_tags_list=$(mktemp) + sort -u "$modified_tags_list" > "$temp_tags_list" + mv "$temp_tags_list" "$modified_tags_list" + fi + + if [ -f "$modified_authors_list" ]; then + local temp_authors_list=$(mktemp) + sort -u "$modified_authors_list" > "$temp_authors_list" + mv "$temp_authors_list" "$modified_authors_list" + fi + + # Invalidate related posts cache if there are modified tags + if [ -f "$modified_tags_list" ] && [ -s "$modified_tags_list" ]; then + # Source related posts functions if not already loaded + if ! command -v invalidate_related_posts_cache_for_tags > /dev/null 2>&1; then + # shellcheck source=related_posts.sh disable=SC1091 + source "$(dirname "$0")/related_posts.sh" || { echo -e "${RED}Error: Failed to source related_posts.sh${NC}"; exit 1; } + fi + + # Create a temporary file to capture the list of invalidated posts + RELATED_POSTS_INVALIDATED_LIST="${CACHE_DIR:-.bssg_cache}/related_posts_invalidated.list" + > "$RELATED_POSTS_INVALIDATED_LIST" # Create empty file + + # Call the invalidation function with the output file + invalidate_related_posts_cache_for_tags "$modified_tags_list" "$RELATED_POSTS_INVALIDATED_LIST" + + # Export the list for use in pass 2 + export RELATED_POSTS_INVALIDATED_LIST + fi + elif $ram_mode_active; then + echo -e "${BLUE}DEBUG: RAM mode active, skipping Pass 1 related-posts invalidation (in-memory computation).${NC}" + else + echo -e "${BLUE}DEBUG: Pass 1 skipped - needs_pass1=$needs_pass1, ENABLE_RELATED_POSTS=${ENABLE_RELATED_POSTS:-true}${NC}" + fi + + # --- PASS 2: Process posts with proper rebuild flags --- + echo -e "${YELLOW}Pass 2: Processing posts...${NC}" # Pre-filter files that need rebuilding local files_to_process_list=() local files_to_process_count=0 local skipped_count=0 - # Get template/locale mtimes once (requires utils.sh and cache.sh to be sourced) - # IMPORTANT: Assumes get_file_mtime, TEMPLATES_DIR, THEME, LOCALE_DIR, SITE_LANG are available - local template_dir="${TEMPLATES_DIR:-templates}" - if [ -d "$template_dir/${THEME:-default}" ]; then - template_dir="$template_dir/${THEME:-default}" - fi - local header_template="$template_dir/header.html" - local footer_template="$template_dir/footer.html" - local active_locale_file="" - if [ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]; then - active_locale_file="${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" - elif [ -f "${LOCALE_DIR:-locales}/en.sh" ]; then - active_locale_file="${LOCALE_DIR:-locales}/en.sh" - fi - local header_time=$(get_file_mtime "$header_template") - local footer_time=$(get_file_mtime "$footer_template") - local locale_time=$(get_file_mtime "$active_locale_file") - - while IFS= read -r line; do - local file filename title date lastmod tags slug image image_caption description - IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line" - - # Basic check if it looks like a post - if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then - # echo -e "Skipping non-post file listed in index (pre-check): ${YELLOW}$file${NC}" >&2 # Too verbose - continue - fi - - # Calculate expected output path (logic copied from process_single_file) - local output_path - local year month day - if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then - year="${BASH_REMATCH[1]}" - month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") - day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") - else - year=$(date +%Y); month=$(date +%m); day=$(date +%d) - fi - local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" - url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; - url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" - local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html" - - # Perform the rebuild check here - # IMPORTANT: Requires common_rebuild_check, get_file_mtime to be available - # Requires BSSG_CONFIG_CHANGED_STATUS to be exported by main.sh - common_rebuild_check "$output_html_file" - local common_result=$? - local needs_rebuild=false - - if [ $common_result -eq 0 ]; then - needs_rebuild=true # Common checks failed (config changed, template newer, output missing) - else # common_result is 2 (output exists and newer than templates/locale) - local input_time=$(get_file_mtime "$file") - local output_time=$(get_file_mtime "$output_html_file") - if (( input_time > output_time )); then - needs_rebuild=true # Input file is newer + if $ram_mode_active && [ "${FORCE_REBUILD:-false}" = true ]; then + echo -e "RAM mode force rebuild: skipping per-post rebuild checks." + while IFS= read -r line; do + local file filename title date + IFS='|' read -r file filename _ date _ <<< "$line" + if [ -n "$date" ] && [[ "$file" == "$SRC_DIR"* ]]; then + files_to_process_list+=("$line") + files_to_process_count=$((files_to_process_count + 1)) fi - fi + done < <(printf '%s\n' "$file_index_data" | awk 'NF') + else + while IFS= read -r line; do + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" - if $needs_rebuild; then - files_to_process_list+=("$line") - files_to_process_count=$((files_to_process_count + 1)) - # --- Start Change: Track ALL modified tags (old and new) --- - # 'tags' variable holds the NEW tags from the current file_index line - local new_tags="$tags" - local old_tags="" - # Try to get old tags from the previous index snapshot - if [ -f "$file_index_prev" ]; then - # Grep for the exact file path ($file), assuming it's the first field - # Extract the 6th field (tags) - old_tags=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f6) + # Basic check if it looks like a post + if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then + # echo -e "Skipping non-post file listed in index (pre-check): ${YELLOW}$file${NC}" >&2 # Too verbose + continue fi - - # Combine old and new tags - local combined_tags="${old_tags},${new_tags}" - - #echo "Tracking combined tags for modified file: $file -> Old: '$old_tags' New: '$new_tags' Combined: '$combined_tags'" >&2 # Debug message - if [ -n "$combined_tags" ]; then - # Split by comma, trim, filter empty, sort unique, and add each tag on a new line - echo "$combined_tags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . | sort -u >> "$modified_tags_list" + # Calculate expected output path (logic copied from process_single_file) + local output_path + local year month day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + year=$(date +%Y); month=$(date +%m); day=$(date +%d) fi - # --- End Change --- - else - # Only print skip message if not rebuilding - echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}" - skipped_count=$((skipped_count + 1)) - fi - done < "$file_index" + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; + url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" + local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html" - # --- Start Change: Unique sort the modified tags list (redundant now but safe) --- - if [ -f "$modified_tags_list" ]; then - echo "Sorting and making modified tags list unique: $modified_tags_list" >&2 # Debug message - local temp_tags_list=$(mktemp) - # Sort unique again just in case duplicates were added somehow - sort -u "$modified_tags_list" > "$temp_tags_list" - mv "$temp_tags_list" "$modified_tags_list" + # Perform the rebuild check here + common_rebuild_check "$output_html_file" + local common_result=$? + local needs_rebuild=false + + if [ $common_result -eq 0 ]; then + needs_rebuild=true # Common checks failed (config changed, template newer, output missing) + else # common_result is 2 (output exists and newer than templates/locale) + local input_time=$(get_file_mtime "$file") + local output_time=$(get_file_mtime "$output_html_file") + if (( input_time > output_time )); then + needs_rebuild=true # Input file is newer + fi + fi + + # Check if this post needs rebuilding due to related posts cache invalidation + if ! $ram_mode_active && [ "$needs_rebuild" = false ] && [ -n "${RELATED_POSTS_INVALIDATED_LIST:-}" ] && [ -f "$RELATED_POSTS_INVALIDATED_LIST" ]; then + if grep -Fxq "$slug" "$RELATED_POSTS_INVALIDATED_LIST" 2>/dev/null; then + needs_rebuild=true # Related posts cache was invalidated + echo -e "Rebuilding ${GREEN}$(basename "$file")${NC} due to related posts cache invalidation" + fi + fi + + if $needs_rebuild; then + files_to_process_list+=("$line") + files_to_process_count=$((files_to_process_count + 1)) + else + # Only print skip message if not rebuilding + echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}" + skipped_count=$((skipped_count + 1)) + fi + done < <( + if $ram_mode_active; then + printf '%s\n' "$file_index_data" | awk 'NF' + else + cat "$file_index" + fi + ) fi - # --- End Change --- # Check if any files need processing if [ $files_to_process_count -eq 0 ]; then @@ -460,14 +728,17 @@ process_all_markdown_files() { echo -e "Found ${GREEN}$files_to_process_count${NC} posts needing processing out of $total_file_count (Skipped: $skipped_count)." + if $ram_mode_active && [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then + prepare_related_posts_ram_cache "${RELATED_POSTS_COUNT:-3}" + fi + # Define a function for processing a single file line from the *filtered* list - # Note: This function now assumes the file *needs* processing. process_single_file_for_rebuild() { local line="$1" # Read the line from the argument variable - local file filename title date lastmod tags slug image image_caption description - IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line" + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" # No need for the basic check here, already done in pre-filter @@ -486,21 +757,59 @@ process_all_markdown_files() { url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" output_path="${OUTPUT_DIR:-output}/$url_path" - # Call the main conversion function - # We no longer rely on its internal file_needs_rebuild check - # TODO: Consider modifying convert_markdown to accept a force flag or skip its check - if ! convert_markdown "$file" "$output_path" "$title" "$date" "$lastmod" "$tags" "$slug" "$image" "$image_caption" "$description"; then + # Call the conversion function, skipping internal rebuild checks because this + # function only receives files pre-selected for rebuild. + if ! convert_markdown "$file" "$output_path" "$title" "$date" "$lastmod" "$tags" "$slug" "$image" "$image_caption" "$description" "$author_name" "$author_email" true; then local exit_code=$? echo -e "${RED}ERROR:${NC} convert_markdown failed for '$file' with exit code $exit_code. Output HTML may be missing or incomplete." >&2 fi } # Use GNU parallel if available - if [ "${HAS_PARALLEL:-false}" = true ]; then + if $ram_mode_active; then + local cores + cores=$(get_parallel_jobs) + if [ "$cores" -gt "$files_to_process_count" ]; then + cores="$files_to_process_count" + fi + + if [ "$files_to_process_count" -gt 1 ] && [ "$cores" -gt 1 ]; then + echo -e "${YELLOW}Using shell parallel workers for $files_to_process_count RAM-mode posts${NC}" + + local worker_pids=() + local worker_idx + for ((worker_idx = 0; worker_idx < cores; worker_idx++)); do + ( + local idx + for ((idx = worker_idx; idx < files_to_process_count; idx += cores)); do + process_single_file_for_rebuild "${files_to_process_list[$idx]}" + done + ) & + worker_pids+=("$!") + done + + local pid + local worker_failed=false + for pid in "${worker_pids[@]}"; do + if ! wait "$pid"; then + worker_failed=true + fi + done + if $worker_failed; then + echo -e "${RED}Parallel RAM-mode post processing failed.${NC}" + exit 1 + fi + else + echo -e "${YELLOW}Using sequential processing for $files_to_process_count RAM-mode posts${NC}" + local line + for line in "${files_to_process_list[@]}"; do + process_single_file_for_rebuild "$line" + done + fi + elif [ "${HAS_PARALLEL:-false}" = true ]; then echo -e "${GREEN}Using GNU parallel to process $files_to_process_count posts${NC}" - local cores=1 - if command -v nproc > /dev/null 2>&1; then cores=$(nproc); - elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi + local cores + cores=$(get_parallel_jobs) # Export functions and variables needed by parallel tasks # Note: We export the new process function @@ -508,11 +817,15 @@ process_all_markdown_files() { # Export dependencies of convert_markdown and its helpers export -f file_needs_rebuild get_file_mtime common_rebuild_check config_has_changed # Still needed by convert_markdown *internally* for now export -f calculate_reading_time generate_slug format_date fix_url parse_metadata extract_metadata convert_markdown_to_html + export -f trim_whitespace resolve_fediverse_creator build_fediverse_creator_meta_tag + export -f format_iso8601_post_date export -f portable_md5sum # Used by cache funcs export CACHE_DIR FORCE_REBUILD OUTPUT_DIR SITE_URL URL_SLUG_FORMAT HEADER_TEMPLATE FOOTER_TEMPLATE export SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME MARKDOWN_PROCESSOR MARKDOWN_PL_PATH DATE_FORMAT TIMEZONE SHOW_TIMEZONE + export FEDIVERSE_CREATOR AUTHOR_FEDIVERSE_CREATORS_SERIALIZED export MSG_PUBLISHED_ON MSG_UPDATED_ON MSG_READING_TIME_TEMPLATE # Export needed locale messages export CONFIG_HASH_FILE BSSG_CONFIG_CHANGED_STATUS # Export status for common_rebuild_check + export ENABLE_RELATED_POSTS RELATED_POSTS_COUNT # Export related posts configuration # Process filtered lines in parallel printf "%s\n" "${files_to_process_list[@]}" | parallel --jobs "$cores" --will-cite process_single_file_for_rebuild {} || { echo -e "${RED}Parallel post processing failed.${NC}"; exit 1; } diff --git a/scripts/build/generate_secondary_pages.sh b/scripts/build/generate_secondary_pages.sh index 7aefa4f..efffb6b 100755 --- a/scripts/build/generate_secondary_pages.sh +++ b/scripts/build/generate_secondary_pages.sh @@ -14,6 +14,10 @@ generate_pages_index() { # --- Define Target File --- local pages_index="$OUTPUT_DIR/pages.html" local secondary_pages_list_file="${CACHE_DIR:-.bssg_cache}/secondary_pages.list" + local ram_mode_active=false + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_active=true + fi # --- Cache Check --- START --- # Rebuild if force flag is set OR if list file exists and output is older than list file @@ -22,13 +26,13 @@ generate_pages_index() { if [[ "${FORCE_REBUILD:-false}" == true ]]; then should_rebuild=true echo -e "${YELLOW}Forcing pages index rebuild (--force-rebuild).${NC}" - elif [ ! -f "$secondary_pages_list_file" ]; then + elif ! $ram_mode_active && [ ! -f "$secondary_pages_list_file" ]; then # If list file doesn't exist, we need to generate pages.html (or handle absence) # This case might mean 0 secondary pages after a clean build. # Let the existing logic handle the case of 0 pages later. should_rebuild=true echo -e "${YELLOW}Secondary pages list file not found, rebuilding pages index.${NC}" - elif [ ! -f "$pages_index" ] || [ "$pages_index" -ot "$secondary_pages_list_file" ]; then + elif ! $ram_mode_active && { [ ! -f "$pages_index" ] || [ "$pages_index" -ot "$secondary_pages_list_file" ]; }; then should_rebuild=true echo -e "${YELLOW}Pages index is older than secondary pages list, rebuilding.${NC}" # Add checks for template file changes? More complex, rely on overall rebuild for now. @@ -47,7 +51,9 @@ generate_pages_index() { # --- Read secondary pages from cache file --- START --- local temp_secondary_pages=() - if [ -f "$secondary_pages_list_file" ]; then + if $ram_mode_active; then + mapfile -t temp_secondary_pages < <(printf '%s\n' "$(ram_mode_get_dataset "secondary_pages")" | awk 'NF') + elif [ -f "$secondary_pages_list_file" ]; then # Use mapfile (readarray) to read lines into the array mapfile -t temp_secondary_pages < "$secondary_pages_list_file" # Optional: Trim whitespace from each element if necessary (mapfile usually handles newlines) @@ -81,15 +87,13 @@ generate_pages_index() { header_content=${header_content//\{\{og_type\}\}/"website"} # Set proper URL in og:url - header_content=${header_content//\{\{page_url\}\}/"pages.html"} + header_content=${header_content//\{\{page_url\}\}/"/pages.html"} header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} # Generate CollectionPage schema local schema_json_ld="" - local tmp_schema=$(mktemp) - # Create CollectionPage schema - cat > "$tmp_schema" << EOF + schema_json_ld=$(cat << EOF EOF - - # Read the schema from the temporary file - schema_json_ld=$(cat "$tmp_schema") - - # Remove the temporary file - rm "$tmp_schema" +) # Add schema markup to header header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} @@ -118,6 +117,7 @@ EOF # Remove image placeholders header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Replace placeholders in the footer footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} @@ -135,7 +135,7 @@ EOF IFS='|' read -r title url _ <<< "$page" # Ignore date for menu cat >> "$pages_index" << EOF EOF done @@ -150,4 +150,4 @@ EOF } # Make function available for sourcing -export -f generate_pages_index \ No newline at end of file +export -f generate_pages_index diff --git a/scripts/build/generate_tags.sh b/scripts/build/generate_tags.sh index d738196..798f9be 100755 --- a/scripts/build/generate_tags.sh +++ b/scripts/build/generate_tags.sh @@ -13,8 +13,604 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s # shellcheck source=generate_feeds.sh disable=SC1091 source "$(dirname "$0")/generate_feeds.sh" || { echo >&2 "Error: Failed to source generate_feeds.sh from generate_tags.sh"; exit 1; } +declare -gA BSSG_RAM_TAG_POST_SLUGS_BY_SLUG=() +declare -gA BSSG_RAM_TAG_POST_COUNT_BY_SLUG=() +declare -gA BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG=() +declare -gA BSSG_RAM_RSS_TEMPLATE_BY_SLUG=() +declare -g BSSG_RAM_TAG_DISPLAY_DATE_FORMAT="" +declare -g BSSG_RAM_TAG_HEADER_BASE="" +declare -g BSSG_RAM_TAG_FOOTER_CONTENT="" + +_bssg_tags_now_ms() { + if declare -F _bssg_ram_timing_now_ms > /dev/null; then + _bssg_ram_timing_now_ms + return + fi + + if [ -n "${EPOCHREALTIME:-}" ]; then + local epoch_norm sec frac ms_part + # Some locales expose EPOCHREALTIME with ',' instead of '.' as decimal separator. + epoch_norm="${EPOCHREALTIME/,/.}" + if [[ "$epoch_norm" =~ ^([0-9]+)([.][0-9]+)?$ ]]; then + sec="${BASH_REMATCH[1]}" + frac="${BASH_REMATCH[2]#.}" + frac="${frac}000" + ms_part="${frac:0:3}" + printf '%s\n' $(( 10#$sec * 1000 + 10#$ms_part )) + return + fi + fi + + if command -v perl >/dev/null 2>&1; then + perl -MTime::HiRes=time -e 'printf("%.0f\n", time()*1000)' + else + printf '%s\n' $(( $(date +%s) * 1000 )) + fi +} + +_bssg_tags_format_ms() { + local ms="${1:-0}" + printf '%d.%03ds' $((ms / 1000)) $((ms % 1000)) +} + +_write_tag_rss_from_cached_items_ram() { + local output_file="$1" + local feed_link_rel="$2" + local feed_atom_link_rel="$3" + local tag="$4" + local rss_items_xml="$5" + + local feed_title="${SITE_TITLE} - ${MSG_TAG_PAGE_TITLE:-"Posts tagged with"}: $tag" + local feed_description="${MSG_POSTS_TAGGED_WITH:-"Posts tagged with"}: $tag" + local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z" + + local escaped_feed_title escaped_feed_description feed_link feed_atom_link channel_last_build_date + escaped_feed_title=$(html_escape "$feed_title") + escaped_feed_description=$(html_escape "$feed_description") + feed_link=$(fix_url "$feed_link_rel") + feed_atom_link=$(fix_url "$feed_atom_link_rel") + channel_last_build_date=$(format_date "now" "$rss_date_fmt") + + exec 4> "$output_file" || return 1 + printf '%s\n' \ + '' \ + '' \ + '' \ + " ${escaped_feed_title}" \ + " ${feed_link}" \ + " ${escaped_feed_description}" \ + " ${SITE_LANG:-en}" \ + " ${channel_last_build_date}" \ + " " >&4 + + if [ -n "$rss_items_xml" ]; then + printf '%s' "$rss_items_xml" >&4 + fi + + printf '%s\n' '' '' >&4 + exec 4>&- + + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${GREEN}RSS feed generated at $output_file${NC}" + fi +} + +_process_single_tag_page_ram() { + local tag_url="$1" + local tag="$2" + local tag_page_html_file="$OUTPUT_DIR/tags/$tag_url/index.html" + local tag_rss_file="$OUTPUT_DIR/tags/$tag_url/${RSS_FILENAME:-rss.xml}" + local tag_page_rel_url="/tags/${tag_url}/" + local tag_rss_rel_url="/tags/${tag_url}/${RSS_FILENAME:-rss.xml}" + mkdir -p "$(dirname "$tag_page_html_file")" + + local header_content="$BSSG_RAM_TAG_HEADER_BASE" + header_content=${header_content//\{\{page_title\}\}/"${MSG_TAG_PAGE_TITLE:-"Posts tagged with"}: $tag"} + header_content=${header_content//\{\{page_url\}\}/"$tag_page_rel_url"} + if [ "${ENABLE_TAG_RSS:-false}" = true ]; then + header_content=${header_content///} + else + header_content=${header_content///} + fi + local schema_json_ld + schema_json_ld=$(cat < +{ + "@context": "https://schema.org", + "@type": "CollectionPage", + "name": "Posts tagged with: $tag", + "description": "Posts with tag: $tag", + "url": "$SITE_URL${tag_page_rel_url}", + "isPartOf": { + "@type": "WebSite", + "name": "$SITE_TITLE", + "url": "$SITE_URL" + } +} + +EOF +) + header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} + local footer_content="$BSSG_RAM_TAG_FOOTER_CONTENT" + + exec 3> "$tag_page_html_file" + printf '%s\n' "$header_content" >&3 + printf '

%s: %s

\n' "${MSG_TAG_PAGE_TITLE:-Posts tagged with}" "$tag" >&3 + printf '
\n' >&3 + + local rss_item_limit=${RSS_ITEM_LIMIT:-15} + local rss_count=0 + local cached_rss_items="" + local rss_all_items_cached=true + local -a selected_rss_templates=() + local tag_post_slugs="" + if [[ -n "${BSSG_RAM_TAG_POST_SLUGS_BY_SLUG[$tag_url]+_}" ]]; then + tag_post_slugs="${BSSG_RAM_TAG_POST_SLUGS_BY_SLUG[$tag_url]}" + fi + + local slug cached_article_html rss_template + while IFS= read -r slug; do + [ -z "$slug" ] && continue + cached_article_html="${BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG[$slug]}" + if [ -n "$cached_article_html" ]; then + printf '%s' "$cached_article_html" >&3 + fi + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "$rss_count" -lt "$rss_item_limit" ]; then + rss_template="${BSSG_RAM_RSS_TEMPLATE_BY_SLUG[$slug]}" + if [ -n "$rss_template" ]; then + selected_rss_templates+=("$rss_template") + if $rss_all_items_cached; then + local rss_file rss_filename rss_title rss_date rss_lastmod rss_tags rss_slug rss_image rss_image_caption rss_description rss_author_name rss_author_email + IFS='|' read -r rss_file rss_filename rss_title rss_date rss_lastmod rss_tags rss_slug rss_image rss_image_caption rss_description rss_author_name rss_author_email <<< "$rss_template" + local rss_item_cache_key="${RSS_INCLUDE_FULL_CONTENT:-false}|${rss_file}|${rss_date}|${rss_lastmod}|${rss_slug}|${rss_title}" + local rss_item_xml="${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]-}" + if [ -n "$rss_item_xml" ]; then + cached_rss_items+="$rss_item_xml" + else + rss_all_items_cached=false + fi + fi + rss_count=$((rss_count + 1)) + fi + fi + done <<< "$tag_post_slugs" + + printf '
\n' >&3 + printf '

%s

\n' "$SITE_URL" "${MSG_ALL_TAGS:-All Tags}" >&3 + printf '%s\n' "$footer_content" >&3 + exec 3>&- + + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "${#selected_rss_templates[@]}" -gt 0 ]; then + if $rss_all_items_cached; then + _write_tag_rss_from_cached_items_ram "$tag_rss_file" "$tag_page_rel_url" "$tag_rss_rel_url" "$tag" "$cached_rss_items" + else + local tag_post_data="" + local rss_template_entry + for rss_template_entry in "${selected_rss_templates[@]}"; do + tag_post_data+="${rss_template_entry//%TAG%/$tag}"$'\n' + done + _generate_rss_feed "$tag_rss_file" "${SITE_TITLE} - ${MSG_TAG_PAGE_TITLE:-"Posts tagged with"}: $tag" "${MSG_POSTS_TAGGED_WITH:-"Posts tagged with"}: $tag" "$tag_page_rel_url" "$tag_rss_rel_url" "$tag_post_data" + fi + fi +} + +_generate_tag_pages_ram() { + echo -e "${YELLOW}Processing tag pages${NC}${ENABLE_TAG_RSS:+" and RSS feeds"}...${NC}" + local ram_tags_timing_enabled=false + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + ram_tags_timing_enabled=true + fi + local tags_total_start_ms=0 + local tags_phase_start_ms=0 + local tags_prep_ms=0 + local tags_render_ms=0 + local tags_index_ms=0 + local tags_total_ms=0 + if [ "$ram_tags_timing_enabled" = true ]; then + tags_total_start_ms="$(_bssg_tags_now_ms)" + tags_phase_start_ms="$tags_total_start_ms" + fi + + local tags_index_data + tags_index_data=$(ram_mode_get_dataset "tags_index") + local main_tags_index_output="$OUTPUT_DIR/tags/index.html" + + mkdir -p "$OUTPUT_DIR/tags" + + if [ -z "$tags_index_data" ]; then + echo -e "${YELLOW}No tags found in RAM index. Skipping tag page generation.${NC}" + return 0 + fi + + BSSG_RAM_TAG_POST_SLUGS_BY_SLUG=() + BSSG_RAM_TAG_POST_COUNT_BY_SLUG=() + BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG=() + BSSG_RAM_RSS_TEMPLATE_BY_SLUG=() + declare -A tag_name_by_slug=() + local sorted_tag_urls=() + declare -A rss_prefill_slug_set=() + declare -A rss_prefill_slug_hits=() + local rss_prefill_slugs=() + local rss_prefill_occurrences=0 + local rss_item_limit="${RSS_ITEM_LIMIT:-15}" + local rss_prefill_min_hits="${RAM_RSS_PREFILL_MIN_HITS:-2}" + local rss_prefill_max_posts="${RAM_RSS_PREFILL_MAX_POSTS:-24}" + if ! [[ "$rss_prefill_min_hits" =~ ^[0-9]+$ ]] || [ "$rss_prefill_min_hits" -lt 1 ]; then + rss_prefill_min_hits=1 + fi + if ! [[ "$rss_prefill_max_posts" =~ ^[0-9]+$ ]]; then + rss_prefill_max_posts=24 + fi + declare -A seen_post_slugs=() + local display_date_format="$DATE_FORMAT" + if [ "${SHOW_TIMEZONE:-false}" = false ]; then + display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//') + fi + BSSG_RAM_TAG_DISPLAY_DATE_FORMAT="$display_date_format" + + # Prime per-post caches once from file_index (one row per post), then build + # lightweight tag->post mappings from tags_index (many rows per post). + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + + local can_prime_rss_metadata=false + local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z" + local build_timestamp_iso="" + if [ "${ENABLE_TAG_RSS:-false}" = true ] && declare -F _ram_prime_rss_metadata_entry > /dev/null; then + can_prime_rss_metadata=true + build_timestamp_iso=$(format_date "now" "%Y-%m-%dT%H:%M:%S%z") + if [[ "$build_timestamp_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then + build_timestamp_iso="${build_timestamp_iso::${#build_timestamp_iso}-2}:${BASH_REMATCH[2]}" + fi + fi + + local file filename title date lastmod tags slug image image_caption description author_name author_email + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + [ -z "$file" ] && continue + [ -z "$slug" ] && continue + [[ -n "${seen_post_slugs[$slug]+_}" ]] && continue + seen_post_slugs["$slug"]=1 + + local post_year post_month post_day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d) + fi + + local formatted_path="${URL_SLUG_FORMAT//Year/$post_year}" + formatted_path="${formatted_path//Month/$post_month}" + formatted_path="${formatted_path//Day/$post_day}" + formatted_path="${formatted_path//slug/$slug}" + local post_link="/${formatted_path}/" + local formatted_date + formatted_date=$(format_date "$date" "$display_date_format") + + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + local article_html="" + article_html+='
'$'\n' + article_html+="

${title}

"$'\n' + article_html+="
${MSG_PUBLISHED_ON:-Published on} ${formatted_date} ${MSG_BY:-by} ${display_author_name}
"$'\n' + if [ -n "$image" ]; then + local image_url alt_text figcaption_content + image_url=$(fix_url "$image") + alt_text="${image_caption:-$title}" + figcaption_content="${image_caption:-$title}" + article_html+=' '$'\n' + fi + if [ -n "$description" ]; then + article_html+='
'$'\n' + article_html+=" ${description}"$'\n' + article_html+='
'$'\n' + fi + article_html+='
'$'\n' + BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG["$slug"]="$article_html" + BSSG_RAM_RSS_TEMPLATE_BY_SLUG["$slug"]="${filename}|${filename}|${title}|${date}|${lastmod}|%TAG%|${slug}|${image}|${image_caption}|${description}|${author_name}|${author_email}" + + if $can_prime_rss_metadata; then + _ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" >/dev/null || true + fi + done <<< "$file_index_data" + + if $can_prime_rss_metadata; then + BSSG_RAM_RSS_METADATA_CACHE_READY=true + fi + + # Sort once globally by tag slug, then by publish date/lastmod descending. + # Aggregate per-tag rows in awk to reduce per-line bash map churn. + local aggregated_tags_data + aggregated_tags_data=$(printf '%s\n' "$tags_index_data" | awk 'NF' | LC_ALL=C sort -t'|' -k2,2 -k4,4r -k5,5r | awk -F'|' -v OFS='|' ' + { + tag = $1 + tag_slug = $2 + post_slug = $7 + if (tag == "" || tag_slug == "") next + + if (current_tag_slug != "" && tag_slug != current_tag_slug) { + print current_tag_slug, current_tag_name, current_count, current_post_slugs + current_count = 0 + current_post_slugs = "" + } + + if (tag_slug != current_tag_slug) { + current_tag_slug = tag_slug + current_tag_name = tag + } + + if (post_slug != "") { + if (current_post_slugs == "") { + current_post_slugs = post_slug + } else { + current_post_slugs = current_post_slugs "," post_slug + } + } + current_count++ + } + END { + if (current_tag_slug != "") { + print current_tag_slug, current_tag_name, current_count, current_post_slugs + } + }') + + local tag_slug tag_name tag_count_value tag_post_slugs_csv + while IFS='|' read -r tag_slug tag_name tag_count_value tag_post_slugs_csv; do + [ -z "$tag_slug" ] && continue + tag_name_by_slug["$tag_slug"]="$tag_name" + BSSG_RAM_TAG_POST_COUNT_BY_SLUG["$tag_slug"]="$tag_count_value" + local tag_post_slugs_newline="" + if [ -n "$tag_post_slugs_csv" ]; then + tag_post_slugs_newline="${tag_post_slugs_csv//,/$'\n'}" + fi + BSSG_RAM_TAG_POST_SLUGS_BY_SLUG["$tag_slug"]="$tag_post_slugs_newline" + sorted_tag_urls+=("$tag_slug") + + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ -n "$tag_post_slugs_newline" ]; then + local rss_prefill_count=0 + local rss_prefill_slug="" + while IFS= read -r rss_prefill_slug; do + [ -z "$rss_prefill_slug" ] && continue + rss_prefill_occurrences=$((rss_prefill_occurrences + 1)) + rss_prefill_slug_hits["$rss_prefill_slug"]=$(( ${rss_prefill_slug_hits[$rss_prefill_slug]:-0} + 1 )) + if [[ -z "${rss_prefill_slug_set[$rss_prefill_slug]+_}" ]]; then + rss_prefill_slug_set["$rss_prefill_slug"]=1 + rss_prefill_slugs+=("$rss_prefill_slug") + fi + rss_prefill_count=$((rss_prefill_count + 1)) + if [ "$rss_prefill_count" -ge "$rss_item_limit" ]; then + break + fi + done <<< "$tag_post_slugs_newline" + fi + done <<< "$aggregated_tags_data" + + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "$rss_prefill_min_hits" -gt 1 ] && [ "${#rss_prefill_slugs[@]}" -gt 0 ]; then + local -a rss_prefill_filtered_slugs=() + local rss_prefill_slug + for rss_prefill_slug in "${rss_prefill_slugs[@]}"; do + if [ "${rss_prefill_slug_hits[$rss_prefill_slug]:-0}" -ge "$rss_prefill_min_hits" ]; then + rss_prefill_filtered_slugs+=("$rss_prefill_slug") + fi + done + if [ "${#rss_prefill_filtered_slugs[@]}" -gt 0 ]; then + rss_prefill_slugs=("${rss_prefill_filtered_slugs[@]}") + fi + fi + + local rss_prefill_pool_count="${#rss_prefill_slugs[@]}" + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "$rss_prefill_max_posts" -gt 0 ] && [ "${#rss_prefill_slugs[@]}" -gt "$rss_prefill_max_posts" ]; then + local -a rss_prefill_ranked_lines=() + local rss_prefill_slug + for rss_prefill_slug in "${rss_prefill_slugs[@]}"; do + rss_prefill_ranked_lines+=("${rss_prefill_slug_hits[$rss_prefill_slug]:-0}|$rss_prefill_slug") + done + + local -a rss_prefill_capped_slugs=() + local rss_prefill_rank_line + while IFS= read -r rss_prefill_rank_line; do + [ -z "$rss_prefill_rank_line" ] && continue + rss_prefill_capped_slugs+=("${rss_prefill_rank_line#*|}") + done < <( + printf '%s\n' "${rss_prefill_ranked_lines[@]}" \ + | LC_ALL=C sort -t'|' -k1,1nr -k2,2 \ + | head -n "$rss_prefill_max_posts" + ) + + if [ "${#rss_prefill_capped_slugs[@]}" -gt 0 ]; then + rss_prefill_slugs=("${rss_prefill_capped_slugs[@]}") + fi + fi + + local footer_base="$FOOTER_TEMPLATE" + footer_base=${footer_base//\{\{current_year\}\}/$(date +%Y)} + footer_base=${footer_base//\{\{author_name\}\}/"$AUTHOR_NAME"} + BSSG_RAM_TAG_FOOTER_CONTENT="$footer_base" + + local header_base="$HEADER_TEMPLATE" + header_base=${header_base//\{\{site_title\}\}/"$SITE_TITLE"} + header_base=${header_base//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_base=${header_base//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + header_base=${header_base//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + header_base=${header_base//\{\{og_type\}\}/"website"} + header_base=${header_base//\{\{site_url\}\}/"$SITE_URL"} + header_base=${header_base//\{\{og_image\}\}/""} + header_base=${header_base//\{\{twitter_image\}\}/""} + header_base=${header_base//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + BSSG_RAM_TAG_HEADER_BASE="$header_base" + + local tag_count="${#sorted_tag_urls[@]}" + echo -e "Generating ${GREEN}$tag_count${NC} tag pages from RAM index." + + if [ "${ENABLE_TAG_RSS:-false}" = true ]; then + if declare -F prepare_ram_rss_metadata_cache > /dev/null; then + prepare_ram_rss_metadata_cache + fi + if [ "${RSS_INCLUDE_FULL_CONTENT:-false}" = true ] && declare -F prepare_ram_rss_full_content_cache > /dev/null; then + prepare_ram_rss_full_content_cache + fi + + # Pre-warm RAM RSS item XML cache once in parent process so worker + # subshells inherit it read-only and avoid rebuilding duplicate items. + if declare -F _generate_rss_feed > /dev/null; then + local rss_prefill_post_data="" + local rss_prefill_slug rss_template_entry + for rss_prefill_slug in "${rss_prefill_slugs[@]}"; do + rss_template_entry="${BSSG_RAM_RSS_TEMPLATE_BY_SLUG[$rss_prefill_slug]}" + [ -z "$rss_template_entry" ] && continue + rss_prefill_post_data+="${rss_template_entry//%TAG%/__prefill__}"$'\n' + done + if [ -n "$rss_prefill_post_data" ]; then + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + local max_posts_label="unlimited" + if [ "$rss_prefill_max_posts" -gt 0 ]; then + max_posts_label="$rss_prefill_max_posts" + fi + echo -e "DEBUG: Pre-warming RAM RSS item cache for ${#rss_prefill_slugs[@]} posts (${rss_prefill_occurrences} tag-RSS slots, min hits: ${rss_prefill_min_hits}, max posts: ${max_posts_label}, pool: ${rss_prefill_pool_count})." + fi + _generate_rss_feed "/dev/null" "__prefill__" "__prefill__" "/" "/rss.xml" "$rss_prefill_post_data" >/dev/null || true + fi + fi + fi + + if [ "$ram_tags_timing_enabled" = true ]; then + local now_ms + now_ms="$(_bssg_tags_now_ms)" + tags_prep_ms=$((now_ms - tags_phase_start_ms)) + tags_phase_start_ms="$now_ms" + fi + + local tag_url + local cores + cores=$(get_parallel_jobs) + if [ "$cores" -gt "$tag_count" ]; then + cores="$tag_count" + fi + + if [ "$tag_count" -gt 1 ] && [ "$cores" -gt 1 ]; then + local worker_pids=() + local worker_idx + for ((worker_idx = 0; worker_idx < cores; worker_idx++)); do + ( + local idx local_tag_url local_tag + for ((idx = worker_idx; idx < tag_count; idx += cores)); do + local_tag_url="${sorted_tag_urls[$idx]}" + local_tag="${tag_name_by_slug[$local_tag_url]}" + _process_single_tag_page_ram "$local_tag_url" "$local_tag" + done + ) & + worker_pids+=("$!") + done + + local pid + local worker_failed=false + for pid in "${worker_pids[@]}"; do + if ! wait "$pid"; then + worker_failed=true + fi + done + if $worker_failed; then + echo -e "${RED}Parallel RAM-mode tag processing failed.${NC}" + exit 1 + fi + else + for tag_url in "${sorted_tag_urls[@]}"; do + tag="${tag_name_by_slug[$tag_url]}" + _process_single_tag_page_ram "$tag_url" "$tag" + done + fi + + if [ "$ram_tags_timing_enabled" = true ]; then + local now_ms + now_ms="$(_bssg_tags_now_ms)" + tags_render_ms=$((now_ms - tags_phase_start_ms)) + tags_phase_start_ms="$now_ms" + fi + + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"${MSG_ALL_TAGS:-"All Tags"}"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"/tags/"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + header_content=${header_content///} + local tags_schema_json + tags_schema_json=$(cat < +{ + "@context": "https://schema.org", + "@type": "CollectionPage", + "name": "${MSG_ALL_TAGS:-"All Tags"}", + "description": "List of all tags on $SITE_TITLE", + "url": "$SITE_URL/tags/", + "isPartOf": { + "@type": "WebSite", + "name": "$SITE_TITLE", + "url": "$SITE_URL" + } +} + +EOF +) + header_content=${header_content//\{\{schema_json_ld\}\}/"$tags_schema_json"} + header_content=${header_content//\{\{og_image\}\}/""} + header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} + footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + + exec 5> "$main_tags_index_output" + printf '%s\n' "$header_content" >&5 + printf '

%s

\n' "${MSG_ALL_TAGS:-All Tags}" >&5 + printf '
\n' >&5 + for tag_url in "${sorted_tag_urls[@]}"; do + tag="${tag_name_by_slug[$tag_url]}" + local post_count="${BSSG_RAM_TAG_POST_COUNT_BY_SLUG[$tag_url]:-0}" + printf ' %s (%s)\n' "$SITE_URL" "$tag_url" "$tag" "$post_count" >&5 + done + printf '
\n' >&5 + printf '%s\n' "$footer_content" >&5 + exec 5>&- + + if [ "$ram_tags_timing_enabled" = true ]; then + local now_ms + now_ms="$(_bssg_tags_now_ms)" + tags_index_ms=$((now_ms - tags_phase_start_ms)) + tags_total_ms=$((now_ms - tags_total_start_ms)) + echo -e "${BLUE}RAM tags sub-timing:${NC}" + echo -e " Prepare maps/cache: $(_bssg_tags_format_ms "$tags_prep_ms")" + echo -e " Tag pages+RSS: $(_bssg_tags_format_ms "$tags_render_ms")" + echo -e " tags/index.html: $(_bssg_tags_format_ms "$tags_index_ms")" + echo -e " Total tags stage: $(_bssg_tags_format_ms "$tags_total_ms")" + fi + + BSSG_RAM_TAG_POST_SLUGS_BY_SLUG=() + BSSG_RAM_TAG_POST_COUNT_BY_SLUG=() + BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG=() + BSSG_RAM_RSS_TEMPLATE_BY_SLUG=() + BSSG_RAM_TAG_HEADER_BASE="" + BSSG_RAM_TAG_FOOTER_CONTENT="" + BSSG_RAM_TAG_DISPLAY_DATE_FORMAT="" + + echo -e "${GREEN}Tag pages processed!${NC}" +} + # Generate tag pages generate_tag_pages() { + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + _generate_tag_pages_ram + return $? + fi + echo -e "${YELLOW}Processing tag pages${NC}${ENABLE_TAG_RSS:+" and RSS feeds"}...${NC}" local tags_index_file="$CACHE_DIR/tags_index.txt" @@ -285,6 +881,7 @@ EOF # Remove image placeholders header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Replace placeholders in the footer footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} @@ -307,8 +904,8 @@ EOF if [ -z "$post_line" ]; then continue; fi # echo "DEBUG (process_tag for '$tag'): Processing post_line: $post_line" >&2 # Removed - local _ _ title date lastmod filename slug image image_caption description - IFS='|' read -r _ _ title date lastmod filename slug image image_caption description <<< "$post_line" + local _ _ title date lastmod filename slug image image_caption description author_name author_email + IFS='|' read -r _ _ title date lastmod filename slug image image_caption description author_name author_email <<< "$post_line" # Create slug-based URL path local post_year post_month post_day @@ -333,14 +930,17 @@ EOF fi local formatted_date=$(format_date "$date" "$display_date_format") + # Determine author for display (with fallback) + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + # --- Start Debug: Check variables before appending article --- #echo "DEBUGAPPEND (tag='$tag', title='$title'): Appending article HTML with link='$post_link', date='$formatted_date'" >&2 # --- End Debug --- cat >> "$tag_page_html_file" << EOF
-

$title

-
${MSG_PUBLISHED_ON:-"Published on"} $formatted_date
+

$title

+
${MSG_PUBLISHED_ON:-"Published on"} $formatted_date ${MSG_BY:-"by"} $display_author_name
EOF if [ -n "$image" ]; then @@ -393,9 +993,9 @@ EOF # Get post data for this tag from the tags index # Sort by post date (field 4), then lastmod (field 5) reverse, limit - # IMPORTANT: tags_index.txt has format: Tag|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|Image|ImageCaption|PostDescription|OriginalFilePath + # IMPORTANT: tags_index.txt has format: Tag|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|Image|ImageCaption|PostDescription|AuthorName|AuthorEmail # We need to map this to the format expected by _generate_rss_feed: - # file|filename|title|date|lastmod|tags|slug|image|image_caption|description + # file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email # We lack the original 'file' path and 'tags' string here. We can approximate. local tag_post_data_tmp=$(mktemp) @@ -404,8 +1004,8 @@ EOF head -n "$rss_item_limit" | \ awk -F'|' -v tag_val="$tag" 'BEGIN {OFS="|"} { # Reconstruct needed fields. Use filename ($6) as placeholder for first field. - # file (placeholder) | filename | title | date | lastmod | tags | slug | image | image_caption | description - print $6 "|" $6 "|" $3 "|" $4 "|" $5 "|" tag_val "|" $7 "|" $8 "|" $9 "|" $10 + # file (placeholder) | filename | title | date | lastmod | tags | slug | image | image_caption | description | author_name | author_email + print $6 "|" $6 "|" $3 "|" $4 "|" $5 "|" tag_val "|" $7 "|" $8 "|" $9 "|" $10 "|" $11 "|" $12 }' > "$tag_post_data_tmp" local tag_post_data=$(cat "$tag_post_data_tmp") @@ -490,9 +1090,8 @@ EOF # Use parallel if [ "${HAS_PARALLEL:-false}" = true ] ; then echo -e "${GREEN}Using GNU parallel to process tag pages${NC}${ENABLE_TAG_RSS:+/feeds}" - local cores=1 - if command -v nproc > /dev/null 2>&1; then cores=$(nproc); - elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi + local cores + cores=$(get_parallel_jobs) local jobs=$cores # Use all cores for tags by default if parallel # Export necessary functions and variables @@ -619,6 +1218,7 @@ EOF header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Replace placeholders in the footer footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} diff --git a/scripts/build/indexing.sh b/scripts/build/indexing.sh index 69064dc..d5087bf 100755 --- a/scripts/build/indexing.sh +++ b/scripts/build/indexing.sh @@ -32,6 +32,7 @@ _build_raw_file_index() { vars["title"] = ""; vars["date"] = ""; vars["lastmod"] = ""; vars["tags"] = ""; vars["slug"] = ""; vars["image"] = ""; vars["image_caption"] = ""; vars["description"] = ""; + vars["author_name"] = ""; vars["author_email"] = ""; in_fm = 0; found_fm = 0; is_html = (FILENAME ~ /\.html$/); is_md = (FILENAME ~ /\.md$/); @@ -40,7 +41,8 @@ _build_raw_file_index() { if (NR > 1) { # Print previous file raw data print current_filename, current_basename, vars["title"], vars["date"], vars["lastmod"], \ - vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"]; + vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"], \ + vars["author_name"], vars["author_email"]; } reset_vars(); current_filename = FILENAME; @@ -101,7 +103,8 @@ _build_raw_file_index() { if (NR > 0) { # Print last file raw data print current_filename, current_basename, vars["title"], vars["date"], vars["lastmod"], \ - vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"]; + vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"], \ + vars["author_name"], vars["author_email"]; } } EOF @@ -119,9 +122,9 @@ _process_raw_file_index() { > "$output_processed_index" # Ensure output file is empty - local file filename title date lastmod tags slug image image_caption description + local file filename title date lastmod tags slug image image_caption description author_name author_email local file_mtime - while IFS='|' read -r file filename title date lastmod tags slug image image_caption description || [[ -n "$file" ]]; do + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email || [[ -n "$file" ]]; do # Fallback for Title (use filename without extension) if [ -z "$title" ]; then title="${filename%.*}" @@ -155,19 +158,50 @@ _process_raw_file_index() { description=$(generate_excerpt "$file") fi + # Apply fallback logic for author fields + if [ -z "$author_name" ]; then + author_name="${AUTHOR_NAME:-Anonymous}" + fi + if [ -z "$author_email" ] && [ -n "$author_name" ] && [ "$author_name" = "${AUTHOR_NAME:-Anonymous}" ]; then + # Only use default email if using default name + author_email="${AUTHOR_EMAIL:-}" + fi + # If author_name is specified but author_email is empty, leave email empty + # Output the fully processed line to the final index file - echo "$file|$filename|$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description" >> "$output_processed_index" + echo "$file|$filename|$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description|$author_name|$author_email" >> "$output_processed_index" done < "$input_raw_index" wait # Ensure background processes from potential subshells (like generate_excerpt) finish } # Optimized file index building - orchestrates raw build and processing +_build_file_index_from_ram() { + while IFS= read -r file; do + [[ -z "$file" ]] && continue + local metadata + metadata=$(extract_metadata "$file") || continue + local filename + filename=$(basename "$file") + echo "$file|$filename|$metadata" + done < <(ram_mode_list_src_files) | sort -t '|' -k 4,4r -k 1,1 +} + optimized_build_file_index() { echo -e "${YELLOW}Building file index...${NC}" local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" local index_marker="${CACHE_DIR:-.bssg_cache}/index_marker" local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" + + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_list_src_files > /dev/null; then + local file_index_data + file_index_data=$(_build_file_index_from_ram) + ram_mode_set_dataset "file_index" "$file_index_data" + ram_mode_clear_dataset "file_index_prev" + ram_mode_set_dataset "frontmatter_changes_marker" "1" + echo -e "${GREEN}File index built from RAM preload with $(ram_mode_dataset_line_count "file_index") complete entries!${NC}" + return 0 + fi # Check if rebuild is needed if [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$file_index" ] && [ -f "$index_marker" ]; then @@ -280,6 +314,44 @@ build_tags_index() { local tags_index_file="${CACHE_DIR:-.bssg_cache}/tags_index.txt" local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local file_index_data tags_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + ram_mode_set_dataset "tags_index" "" + ram_mode_clear_dataset "has_tags" + echo -e "${GREEN}Tags index built!${NC}" + return 0 + fi + + tags_index_data=$(printf '%s\n' "$file_index_data" | awk -F'|' -v OFS='|' ' + { + if (length($6) > 0) { + split($6, tags_array, ","); + for (i in tags_array) { + tag = tags_array[i]; + gsub(/^[[:space:]]+|[[:space:]]+$/, "", tag); + if (length(tag) == 0) continue; + + tag_slug = tolower(tag); + gsub(/[^a-z0-9]+/, "-", tag_slug); + gsub(/^-+|-+$/, "", tag_slug); + if (length(tag_slug) == 0) tag_slug = "-"; + + print tag, tag_slug, $3, $4, $5, $2, $7, $8, $9, $10, $11, $12; + } + } + }') + ram_mode_set_dataset "tags_index" "$tags_index_data" + if [ -n "$tags_index_data" ]; then + ram_mode_set_dataset "has_tags" "1" + else + ram_mode_clear_dataset "has_tags" + fi + echo -e "${GREEN}Tags index built!${NC}" + return 0 + fi + # --- Optimized Rebuild Check --- START --- local rebuild_needed=false local reason="" @@ -323,7 +395,7 @@ build_tags_index() { # Use awk for efficient processing and slug generation awk -F'|' -v OFS='|' '{ # $1=file, $2=filename, $3=title, $4=date, $5=lastmod, - # $6=tags, $7=slug, $8=image, $9=image_caption, $10=description + # $6=tags, $7=slug, $8=image, $9=image_caption, $10=description, $11=author_name, $12=author_email if (length($6) > 0) { # Check if tags field is not empty split($6, tags_array, ","); # Split tags by comma for (i in tags_array) { @@ -337,8 +409,8 @@ build_tags_index() { gsub(/^-+|-+$/, "", tag_slug); # Trim leading/trailing hyphens if (length(tag_slug) == 0) tag_slug = "-"; # Handle empty slugs - # Print: TagName|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription - print tag, tag_slug, $3, $4, $5, $2, $7, $8, $9, $10; + # Print: TagName|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription|AuthorName|AuthorEmail + print tag, tag_slug, $3, $4, $5, $2, $7, $8, $9, $10, $11, $12; } } }' "$file_index" > "$tags_index_file" @@ -356,6 +428,194 @@ build_tags_index() { echo -e "${GREEN}Tags index built!${NC}" } +# Build authors index from the file index +build_authors_index() { + echo -e "${YELLOW}Building authors index...${NC}" + + local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" + local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" + + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local file_index_data authors_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + ram_mode_set_dataset "authors_index" "" + ram_mode_clear_dataset "has_authors" + echo -e "${GREEN}Authors index built!${NC}" + return 0 + fi + + authors_index_data=$(printf '%s\n' "$file_index_data" | awk -F'|' -v OFS='|' ' + { + author_name = $11; + author_email = $12; + if (length(author_name) == 0) next; + + author_slug = tolower(author_name); + gsub(/[^a-z0-9]+/, "-", author_slug); + gsub(/^-+|-+$/, "", author_slug); + if (length(author_slug) == 0) author_slug = "anonymous"; + + print author_name, author_slug, author_email, $3, $4, $5, $2, $7, $8, $9, $10; + }') + ram_mode_set_dataset "authors_index" "$authors_index_data" + if [ -n "$authors_index_data" ]; then + ram_mode_set_dataset "has_authors" "1" + else + ram_mode_clear_dataset "has_authors" + fi + echo -e "${GREEN}Authors index built!${NC}" + return 0 + fi + + # Check if rebuild is needed: missing cache or input/dependencies changed + local rebuild_needed=false + if [ ! -f "$authors_index_file" ]; then + rebuild_needed=true + elif file_needs_rebuild "$file_index" "$authors_index_file"; then + echo -e "${YELLOW}Authors index is outdated or dependencies changed, rebuilding authors...${NC}" + rebuild_needed=true + fi + + if [ "$rebuild_needed" = false ]; then + echo -e "${GREEN}Authors index is up to date, skipping...${NC}" + return 0 + fi + + if [ ! -f "$file_index" ]; then + echo -e "${RED}Error: File index '$file_index' not found. Cannot build authors index.${NC}" + return 1 + fi + + lock_file "$authors_index_file" + + > "$authors_index_file" # Clear the file + + # Read from file index and extract author info + # Use awk for efficient processing and slug generation + awk -F'|' -v OFS='|' '{ + # $1=file, $2=filename, $3=title, $4=date, $5=lastmod, + # $6=tags, $7=slug, $8=image, $9=image_caption, $10=description, $11=author_name, $12=author_email + author_name = $11; + author_email = $12; + + # Skip if author_name is empty + if (length(author_name) == 0) next; + + # Generate slug within awk (replicating generate_slug logic) + author_slug = tolower(author_name); + gsub(/[^a-z0-9]+/, "-", author_slug); # Replace non-alphanumeric with hyphens + gsub(/^-+|-+$/, "", author_slug); # Trim leading/trailing hyphens + if (length(author_slug) == 0) author_slug = "anonymous"; # Handle empty slugs + + # Print: AuthorName|AuthorSlug|AuthorEmail|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription + print author_name, author_slug, author_email, $3, $4, $5, $2, $7, $8, $9, $10; + }' "$file_index" > "$authors_index_file" + + unlock_file "$authors_index_file" + + # Check if the generated index is not empty and create/remove flag file + local authors_flag_file="${CACHE_DIR:-.bssg_cache}/has_authors.flag" + if [ -s "$authors_index_file" ]; then + touch "$authors_flag_file" + else + rm -f "$authors_flag_file" + fi + + echo -e "${GREEN}Authors index built!${NC}" +} + +# Compare current and previous authors index to find affected authors and check if index needs rebuild +# Exports: AFFECTED_AUTHORS (space-separated list of author names) +# AUTHORS_INDEX_NEEDS_REBUILD ("true" or "false") +identify_affected_authors() { + local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" + local authors_index_prev_file="${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt" + + export AFFECTED_AUTHORS="" + export AUTHORS_INDEX_NEEDS_REBUILD="false" + + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local authors_index_data + authors_index_data=$(ram_mode_get_dataset "authors_index") + if [ -n "$authors_index_data" ]; then + AFFECTED_AUTHORS=$(printf '%s\n' "$authors_index_data" | awk -F'|' 'NF { print $1 }' | sort -u | tr '\n' ' ') + AUTHORS_INDEX_NEEDS_REBUILD="true" + fi + export AFFECTED_AUTHORS + export AUTHORS_INDEX_NEEDS_REBUILD + return 0 + fi + + # If previous index doesn't exist, all authors in the current index are affected, + # and the main index needs rebuilding. + if [ ! -f "$authors_index_prev_file" ]; then + if [ -s "$authors_index_file" ]; then # Check if current index has content + echo "Previous authors index not found. Marking all authors as affected." >&2 # Debug + AFFECTED_AUTHORS=$(cut -d'|' -f1 "$authors_index_file" | sort -u | tr '\n' ' ') + AUTHORS_INDEX_NEEDS_REBUILD="true" + else + echo "Both previous and current authors indexes are missing or empty. No authors affected." >&2 # Debug + fi + export AFFECTED_AUTHORS + export AUTHORS_INDEX_NEEDS_REBUILD + return 0 + fi + + # If current index doesn't exist (but previous did), means all posts were deleted? + # Mark authors from previous index as affected, index needs rebuild. + if [ ! -f "$authors_index_file" ] || [ ! -s "$authors_index_file" ]; then + echo "Current authors index not found or empty. Marking all previous authors as affected." >&2 # Debug + AFFECTED_AUTHORS=$(cut -d'|' -f1 "$authors_index_prev_file" | sort -u | tr '\n' ' ') + AUTHORS_INDEX_NEEDS_REBUILD="true" + export AFFECTED_AUTHORS + export AUTHORS_INDEX_NEEDS_REBUILD + return 0 + fi + + # Extract AuthorName|Filename from both files for precise comparison + local current_entries="${CACHE_DIR:-.bssg_cache}/authors_curr_af.$$" + local prev_entries="${CACHE_DIR:-.bssg_cache}/authors_prev_af.$$" + trap 'rm -f "$current_entries" "$prev_entries"' RETURN + + cut -d'|' -f1,7 "$authors_index_file" | sort > "$current_entries" + cut -d'|' -f1,7 "$authors_index_prev_file" | sort > "$prev_entries" + + # Find differences (lines unique to current or previous) + local diff_output + diff_output=$(comm -3 "$current_entries" "$prev_entries") + + # Extract unique author names from the differences + if [ -n "$diff_output" ]; then + AFFECTED_AUTHORS=$(echo "$diff_output" | sed 's/^[[:space:]]*//' | cut -d'|' -f1 | sort -u | tr '\n' ' ') + echo "Affected authors identified: $AFFECTED_AUTHORS" >&2 # Debug + else + echo "No difference in posts per author found." >&2 # Debug + AFFECTED_AUTHORS="" + fi + + # Compare author counts (AuthorName|Count) to see if the main index needs rebuilding + local current_counts="${CACHE_DIR:-.bssg_cache}/authors_curr_counts.$$" + local prev_counts="${CACHE_DIR:-.bssg_cache}/authors_prev_counts.$$" + trap 'rm -f "$current_entries" "$prev_entries" "$current_counts" "$prev_counts"' RETURN + + cut -d'|' -f1 "$authors_index_file" | sort | uniq -c | awk '{print $2"|"$1}' | sort > "$current_counts" + cut -d'|' -f1 "$authors_index_prev_file" | sort | uniq -c | awk '{print $2"|"$1}' | sort > "$prev_counts" + + if ! cmp -s "$current_counts" "$prev_counts"; then + echo "Author counts differ. Main authors index needs rebuild." >&2 # Debug + AUTHORS_INDEX_NEEDS_REBUILD="true" + else + echo "Author counts are the same." >&2 # Debug + AUTHORS_INDEX_NEEDS_REBUILD="false" + fi + + export AFFECTED_AUTHORS + export AUTHORS_INDEX_NEEDS_REBUILD + rm -f "$current_entries" "$prev_entries" "$current_counts" "$prev_counts" + trap - RETURN # Remove trap upon successful completion +} + # Build archive index by year and month from the file index build_archive_index() { echo -e "${YELLOW}Building archive index...${NC}" @@ -363,6 +623,43 @@ build_archive_index() { local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" local archive_index_file="${CACHE_DIR:-.bssg_cache}/archive_index.txt" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local file_index_data archive_index_data="" + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + ram_mode_set_dataset "archive_index" "" + echo -e "${GREEN}Archive index built!${NC}" + return 0 + fi + + local line file filename title date lastmod tags slug image image_caption description author_name author_email + while IFS= read -r line; do + [ -z "$line" ] && continue + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" + [ -z "$date" ] && continue + + local year month month_name + if [[ "$date" =~ ^([0-9]{4})[-/]([0-9]{1,2})[-/]([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + else + continue + fi + + local month_name_var="MSG_MONTH_${month}" + month_name="${!month_name_var}" + if [[ -z "$month_name" ]]; then + month_name="$month" + fi + + archive_index_data+="$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description|$author_name|$author_email"$'\n' + done <<< "$file_index_data" + + ram_mode_set_dataset "archive_index" "$archive_index_data" + echo -e "${GREEN}Archive index built!${NC}" + return 0 + fi + # Check if rebuild is needed: missing cache or input/dependencies changed local rebuild_needed=false if [ ! -f "$archive_index_file" ]; then @@ -387,9 +684,9 @@ build_archive_index() { > "$archive_index_file" # Clear the file # Read from file index and extract date info - local line file filename title date lastmod tags slug image image_caption description + local line file filename title date lastmod tags slug image image_caption description author_name author_email while IFS= read -r line || [[ -n "$line" ]]; do - IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line" + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" if [ -n "$date" ]; then local year month month_name @@ -428,8 +725,8 @@ build_archive_index() { [[ -z "$month_name" ]] && month_name="Unknown" fi - # Output: Year|MonthNum|MonthName|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription - echo "$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description" >> "$archive_index_file" + # Output: Year|MonthNum|MonthName|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription|AuthorName|AuthorEmail + echo "$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description|$author_name|$author_email" >> "$archive_index_file" fi done < "$file_index" @@ -448,6 +745,18 @@ identify_affected_archive_months() { export AFFECTED_ARCHIVE_MONTHS="" export ARCHIVE_INDEX_NEEDS_REBUILD="false" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local archive_index_data + archive_index_data=$(ram_mode_get_dataset "archive_index") + if [ -n "$archive_index_data" ]; then + AFFECTED_ARCHIVE_MONTHS=$(printf '%s\n' "$archive_index_data" | awk -F'|' 'NF { print $1 "|" $2 }' | sort -u | tr '\n' ' ') + ARCHIVE_INDEX_NEEDS_REBUILD="true" + fi + export AFFECTED_ARCHIVE_MONTHS + export ARCHIVE_INDEX_NEEDS_REBUILD + return 0 + fi + # If previous index doesn't exist, all months in the current index are affected, # and the main index needs rebuilding. if [ ! -f "$archive_index_prev_file" ]; then @@ -517,4 +826,4 @@ identify_affected_archive_months() { trap - RETURN # Remove trap upon successful completion } -# --- Indexing Functions --- END --- \ No newline at end of file +# --- Indexing Functions --- END --- diff --git a/scripts/build/main.sh b/scripts/build/main.sh index 8add6f4..678c316 100755 --- a/scripts/build/main.sh +++ b/scripts/build/main.sh @@ -13,6 +13,7 @@ BUILD_START_TIME=$(date +%s) SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" # Determine the project root (one level up from the SCRIPT_DIR's parent) PROJECT_ROOT="$( dirname "$( dirname "$SCRIPT_DIR" )" )" +export BSSG_PROJECT_ROOT="$PROJECT_ROOT" # Check if PROJECT_ROOT is already the current directory to avoid unnecessary cd if [ "$PWD" != "$PROJECT_ROOT" ]; then echo "Changing directory to project root: $PROJECT_ROOT" @@ -81,25 +82,180 @@ fi # shellcheck source=utils.sh source "${SCRIPT_DIR}/utils.sh" || { echo -e "\033[0;31mError: Failed to source utils.sh\033[0m"; exit 1; } +# Build mode validation and setup +BUILD_MODE="${BUILD_MODE:-normal}" +case "$BUILD_MODE" in + normal|ram) ;; + *) + echo -e "${RED}Error: Invalid BUILD_MODE '$BUILD_MODE'. Use 'normal' or 'ram'.${NC}" >&2 + exit 1 + ;; +esac +export BUILD_MODE +export BSSG_RAM_MODE=false + # Print the theme being used for this build (final value after potential random selection) echo -e "${GREEN}Using theme: ${THEME}${NC}" echo "Loaded utilities." +# --- RAM Mode Stage Timing --- START --- +BSSG_RAM_TIMING_ENABLED=false +if [ "$BUILD_MODE" = "ram" ]; then + BSSG_RAM_TIMING_ENABLED=true +fi +declare -ga BSSG_RAM_TIMING_STAGE_KEYS=() +declare -ga BSSG_RAM_TIMING_STAGE_LABELS=() +declare -ga BSSG_RAM_TIMING_STAGE_MS=() +BSSG_RAM_TIMING_STAGE_ACTIVE=false +BSSG_RAM_TIMING_CURRENT_STAGE_KEY="" +BSSG_RAM_TIMING_CURRENT_STAGE_LABEL="" +BSSG_RAM_TIMING_CURRENT_STAGE_START_MS=0 + +_bssg_ram_timing_now_ms() { + if [ -n "${EPOCHREALTIME:-}" ]; then + local epoch_norm sec frac ms_part + # Some locales expose EPOCHREALTIME with ',' instead of '.' as decimal separator. + epoch_norm="${EPOCHREALTIME/,/.}" + if [[ "$epoch_norm" =~ ^([0-9]+)([.][0-9]+)?$ ]]; then + sec="${BASH_REMATCH[1]}" + frac="${BASH_REMATCH[2]#.}" + frac="${frac}000" + ms_part="${frac:0:3}" + printf '%s\n' $(( 10#$sec * 1000 + 10#$ms_part )) + return + fi + fi + + if command -v perl >/dev/null 2>&1; then + perl -MTime::HiRes=time -e 'printf("%.0f\n", time()*1000)' + else + printf '%s\n' $(( $(date +%s) * 1000 )) + fi +} + +_bssg_ram_timing_format_ms() { + local ms="$1" + printf '%d.%03ds' $((ms / 1000)) $((ms % 1000)) +} + +bssg_ram_timing_start() { + if [ "$BSSG_RAM_TIMING_ENABLED" != true ]; then + return + fi + + if [ "$BSSG_RAM_TIMING_STAGE_ACTIVE" = true ]; then + bssg_ram_timing_end + fi + + BSSG_RAM_TIMING_CURRENT_STAGE_KEY="$1" + BSSG_RAM_TIMING_CURRENT_STAGE_LABEL="$2" + BSSG_RAM_TIMING_CURRENT_STAGE_START_MS="$(_bssg_ram_timing_now_ms)" + BSSG_RAM_TIMING_STAGE_ACTIVE=true +} + +bssg_ram_timing_end() { + if [ "$BSSG_RAM_TIMING_ENABLED" != true ] || [ "$BSSG_RAM_TIMING_STAGE_ACTIVE" != true ]; then + return + fi + + local end_ms elapsed_ms + end_ms="$(_bssg_ram_timing_now_ms)" + elapsed_ms=$((end_ms - BSSG_RAM_TIMING_CURRENT_STAGE_START_MS)) + if [ "$elapsed_ms" -lt 0 ]; then + elapsed_ms=0 + fi + + BSSG_RAM_TIMING_STAGE_KEYS+=("$BSSG_RAM_TIMING_CURRENT_STAGE_KEY") + BSSG_RAM_TIMING_STAGE_LABELS+=("$BSSG_RAM_TIMING_CURRENT_STAGE_LABEL") + BSSG_RAM_TIMING_STAGE_MS+=("$elapsed_ms") + + BSSG_RAM_TIMING_STAGE_ACTIVE=false + BSSG_RAM_TIMING_CURRENT_STAGE_KEY="" + BSSG_RAM_TIMING_CURRENT_STAGE_LABEL="" + BSSG_RAM_TIMING_CURRENT_STAGE_START_MS=0 +} + +bssg_ram_timing_print_summary() { + if [ "$BSSG_RAM_TIMING_ENABLED" != true ]; then + return + fi + + # Close any open stage (defensive; build flow should end stages explicitly). + if [ "$BSSG_RAM_TIMING_STAGE_ACTIVE" = true ]; then + bssg_ram_timing_end + fi + + local count="${#BSSG_RAM_TIMING_STAGE_MS[@]}" + if [ "$count" -eq 0 ]; then + return + fi + + local total_ms=0 + local max_ms=0 + local max_label="" + local i + for ((i = 0; i < count; i++)); do + local stage_ms="${BSSG_RAM_TIMING_STAGE_MS[$i]}" + total_ms=$((total_ms + stage_ms)) + if [ "$stage_ms" -gt "$max_ms" ]; then + max_ms="$stage_ms" + max_label="${BSSG_RAM_TIMING_STAGE_LABELS[$i]}" + fi + done + + echo "------------------------------------------------------" + echo -e "${GREEN}RAM mode timing summary:${NC}" + printf " %-26s %12s %10s\n" "Stage" "Duration" "Share" + for ((i = 0; i < count; i++)); do + local stage_label="${BSSG_RAM_TIMING_STAGE_LABELS[$i]}" + local stage_ms="${BSSG_RAM_TIMING_STAGE_MS[$i]}" + local pct_tenths=0 + if [ "$total_ms" -gt 0 ]; then + pct_tenths=$(( (stage_ms * 1000 + total_ms / 2) / total_ms )) + fi + local formatted_ms + formatted_ms="$(_bssg_ram_timing_format_ms "$stage_ms")" + printf " %-26s %12s %6d.%d%%\n" "$stage_label" "$formatted_ms" $((pct_tenths / 10)) $((pct_tenths % 10)) + done + echo -e " ${GREEN}Total (timed stages):$(_bssg_ram_timing_format_ms "$total_ms")${NC}" + if [ -n "$max_label" ]; then + echo -e " ${YELLOW}Slowest stage:${NC} ${max_label} ($(_bssg_ram_timing_format_ms "$max_ms"))" + fi +} +# --- RAM Mode Stage Timing --- END --- + # Check Dependencies # shellcheck source=deps.sh +bssg_ram_timing_start "dependencies" "Dependencies" source "${SCRIPT_DIR}/deps.sh" || { echo -e "${RED}Error: Failed to source deps.sh${NC}"; exit 1; } check_dependencies # Call the function to perform checks and export HAS_PARALLEL +bssg_ram_timing_end + +if [ "$BUILD_MODE" = "ram" ]; then + export BSSG_RAM_MODE=true + export FORCE_REBUILD=true + + # shellcheck source=ram_mode.sh + source "${SCRIPT_DIR}/ram_mode.sh" || { echo -e "${RED}Error: Failed to source ram_mode.sh${NC}"; exit 1; } + print_info "RAM mode enabled: source/template files and build indexes are held in memory." + print_info "RAM mode parallel worker cap: ${RAM_MODE_MAX_JOBS:-6} (set RAM_MODE_MAX_JOBS to tune)." +fi + echo "Checked dependencies. Parallel available: ${HAS_PARALLEL:-false}" # Source Cache Manager (defines cache functions) # shellcheck source=cache.sh +bssg_ram_timing_start "cache_setup" "Cache Setup/Clean" source "${SCRIPT_DIR}/cache.sh" || { echo -e "${RED}Error: Failed to source cache.sh${NC}"; exit 1; } echo "Loaded cache manager." # Check if config changed BEFORE updating the hash file, store status for later use BSSG_CONFIG_CHANGED_STATUS=1 # Default to 1 (not changed) -if config_has_changed; then +if [ "${BSSG_RAM_MODE:-false}" = true ]; then + # RAM mode is intentionally ephemeral, always rebuild from preloaded inputs. + BSSG_CONFIG_CHANGED_STATUS=0 +elif config_has_changed; then BSSG_CONFIG_CHANGED_STATUS=0 # Set to 0 (changed) fi export BSSG_CONFIG_CHANGED_STATUS @@ -118,17 +274,21 @@ fi # --- Add check for CLEAN_OUTPUT influencing FORCE_REBUILD --- END --- # Handle --force-rebuild first -if [ "${FORCE_REBUILD:-false}" = true ]; then +if [ "${BSSG_RAM_MODE:-false}" != true ] && [ "${FORCE_REBUILD:-false}" = true ]; then echo -e "${YELLOW}Force rebuild enabled, deleting entire cache directory (${CACHE_DIR:-.bssg_cache})...${NC}" rm -rf "${CACHE_DIR:-.bssg_cache}" echo -e "${GREEN}Cache deleted!${NC}" fi -echo "Ensuring cache directory structure exists... (${CACHE_DIR:-.bssg_cache})" -mkdir -p "${CACHE_DIR:-.bssg_cache}/meta" "${CACHE_DIR:-.bssg_cache}/content" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + echo "Ensuring cache directory structure exists... (${CACHE_DIR:-.bssg_cache})" + mkdir -p "${CACHE_DIR:-.bssg_cache}/meta" "${CACHE_DIR:-.bssg_cache}/content" -# Create initial config hash *after* ensuring cache dir exists -create_config_hash + # Create initial config hash *after* ensuring cache dir exists + create_config_hash +else + echo "RAM mode: skipping cache directory creation and config hash persistence." +fi # --- Initial Cache Setup & Cleaning --- END # Handle --clean-output flag (using logic moved from original main/clean_output_directory) @@ -148,10 +308,12 @@ if [ "${CLEAN_OUTPUT:-false}" = true ]; then echo -e "${YELLOW}Output directory (${OUTPUT_DIR:-output}) does not exist, no need to clean.${NC}" fi fi +bssg_ram_timing_end # Source Content Processor (defines functions like extract_metadata, convert_markdown_to_html) # Moved up before indexing as indexing uses some content functions (e.g., generate_slug) # shellcheck source=content.sh +bssg_ram_timing_start "index_build" "Index/Data Build" source "${SCRIPT_DIR}/content.sh" || { echo -e "${RED}Error: Failed to source content.sh${NC}"; exit 1; } echo "Loaded content processing functions." @@ -161,17 +323,23 @@ echo "Loaded content processing functions." source "${SCRIPT_DIR}/indexing.sh" || { echo -e "${RED}Error: Failed to source indexing.sh${NC}"; exit 1; } echo "Loaded indexing functions." +if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_preload_inputs || { echo -e "${RED}Error: RAM preload failed.${NC}"; exit 1; } +fi + # --- Build Intermediate Indexes --- # Moved up before preload_templates # --- Start Change: Snapshot previous file index --- file_index_file="${CACHE_DIR:-.bssg_cache}/file_index.txt" file_index_prev_file="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" -if [ -f "$file_index_file" ]; then - echo "Snapshotting previous file index to $file_index_prev_file" >&2 # Debug - cp "$file_index_file" "$file_index_prev_file" -else - # Ensure previous file doesn't exist if current doesn't - rm -f "$file_index_prev_file" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + if [ -f "$file_index_file" ]; then + echo "Snapshotting previous file index to $file_index_prev_file" >&2 # Debug + cp "$file_index_file" "$file_index_prev_file" + else + # Ensure previous file doesn't exist if current doesn't + rm -f "$file_index_prev_file" + fi fi # --- End Change --- optimized_build_file_index || { echo -e "${RED}Error: Failed to build file index.${NC}"; exit 1; } @@ -179,12 +347,14 @@ optimized_build_file_index || { echo -e "${RED}Error: Failed to build file index # --- Start Change: Snapshot previous tags index --- tags_index_file="${CACHE_DIR:-.bssg_cache}/tags_index.txt" tags_index_prev_file="${CACHE_DIR:-.bssg_cache}/tags_index_prev.txt" -if [ -f "$tags_index_file" ]; then - echo "Snapshotting previous tags index to $tags_index_prev_file" >&2 # Debug - cp "$tags_index_file" "$tags_index_prev_file" -else - # Ensure previous file doesn't exist if current doesn't - rm -f "$tags_index_prev_file" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + if [ -f "$tags_index_file" ]; then + echo "Snapshotting previous tags index to $tags_index_prev_file" >&2 # Debug + cp "$tags_index_file" "$tags_index_prev_file" + else + # Ensure previous file doesn't exist if current doesn't + rm -f "$tags_index_prev_file" + fi fi # --- End Change --- @@ -196,16 +366,38 @@ build_tags_index || { echo -e "${RED}Error: Failed to build tags index.${NC}"; e # echo "--- End $tags_index_file DEBUG ---" >&2 # --- End Debug --- +# --- Start Change: Snapshot previous authors index --- +authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" +authors_index_prev_file="${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + if [ -f "$authors_index_file" ]; then + echo "Snapshotting previous authors index to $authors_index_prev_file" >&2 # Debug + cp "$authors_index_file" "$authors_index_prev_file" + else + # Ensure previous file doesn't exist if current doesn't + rm -f "$authors_index_prev_file" + fi +fi +# --- End Change --- + +build_authors_index || { echo -e "${RED}Error: Failed to build authors index.${NC}"; exit 1; } + +# --- Start Change: Identify affected authors --- +identify_affected_authors || { echo -e "${RED}Error: Failed to identify affected authors.${NC}"; exit 1; } +# --- End Change --- + if [ "${ENABLE_ARCHIVES:-false}" = true ]; then # --- Start Change: Snapshot previous archive index --- archive_index_file="${CACHE_DIR:-.bssg_cache}/archive_index.txt" archive_index_prev_file="${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt" - if [ -f "$archive_index_file" ]; then - echo "Snapshotting previous archive index to $archive_index_prev_file" >&2 # Debug - cp "$archive_index_file" "$archive_index_prev_file" - else - # Ensure previous file doesn't exist if current doesn't - rm -f "$archive_index_prev_file" + if [ "${BSSG_RAM_MODE:-false}" != true ]; then + if [ -f "$archive_index_file" ]; then + echo "Snapshotting previous archive index to $archive_index_prev_file" >&2 # Debug + cp "$archive_index_file" "$archive_index_prev_file" + else + # Ensure previous file doesn't exist if current doesn't + rm -f "$archive_index_prev_file" + fi fi # --- End Change --- build_archive_index || { echo -e "${RED}Error: Failed to build archive index.${NC}"; exit 1; } @@ -214,10 +406,12 @@ if [ "${ENABLE_ARCHIVES:-false}" = true ]; then # --- End Change --- fi echo "Built intermediate cache indexes." +bssg_ram_timing_end # Load Templates (and generate dynamic menus, exports vars like HEADER_TEMPLATE) # Moved down after indexing # shellcheck source=templates.sh +bssg_ram_timing_start "templates" "Template Prep" source "${SCRIPT_DIR}/templates.sh" || { echo -e "${RED}Error: Failed to source templates.sh${NC}"; exit 1; } preload_templates # Call the function echo "Loaded and processed templates." @@ -261,6 +455,7 @@ fi export BSSG_MAX_TEMPLATE_LOCALE_TIME=$latest_template_locale_time echo "Latest template/locale time: $BSSG_MAX_TEMPLATE_LOCALE_TIME (Header: $header_time, Footer: $footer_time, Locale: $locale_time)" # --- Pre-calculate Max Template/Locale Time --- END --- +bssg_ram_timing_end # --- Prepare for Parallel Processing --- if [ "${HAS_PARALLEL:-false}" = true ]; then @@ -281,35 +476,66 @@ if [ "${HAS_PARALLEL:-false}" = true ]; then echo "Core parallel exports complete." fi +# --- Related Posts Cache Invalidation --- START --- +# This will be handled during post processing for better timing +RELATED_POSTS_INVALIDATED_LIST="" +if [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then + # Export the variable for use by generate_posts.sh + export RELATED_POSTS_INVALIDATED_LIST +fi +# --- Related Posts Cache Invalidation --- END --- + # --- Generate Content HTML --- # Source and run Post Generator # shellcheck source=generate_posts.sh +bssg_ram_timing_start "posts" "Posts" source "${SCRIPT_DIR}/generate_posts.sh" || { echo -e "${RED}Error: Failed to source generate_posts.sh${NC}"; exit 1; } process_all_markdown_files || { echo -e "${RED}Error: Post processing failed.${NC}"; exit 1; } echo "Generated post HTML files." +bssg_ram_timing_end # --- Post Generation --- END --- # --- Page Generation --- START -- # Source the page generation script # shellcheck source=generate_pages.sh disable=SC1091 +bssg_ram_timing_start "pages" "Static Pages" source "$SCRIPT_DIR/generate_pages.sh" || { echo -e "${RED}Error: Failed to source generate_pages.sh${NC}"; exit 1; } # Call the main page processing function process_all_pages || { echo -e "${RED}Error: Page processing failed.${NC}"; exit 1; } +bssg_ram_timing_end # --- Page Generation --- END --- # --- Tag Page Generation --- START --- # Source and run Tag Page Generator # shellcheck source=generate_tags.sh disable=SC1091 +bssg_ram_timing_start "tags" "Tags" source "$SCRIPT_DIR/generate_tags.sh" || { echo -e "${RED}Error: Failed to source generate_tags.sh${NC}"; exit 1; } # Call the main function from the sourced script generate_tag_pages || { echo -e "${RED}Error: Tag page generation failed.${NC}"; exit 1; } echo "Generated tag list pages." +bssg_ram_timing_end # --- Tag Page Generation --- END --- +# --- Author Page Generation --- START --- +# Source and run Author Page Generator (if enabled) +if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ]; then + bssg_ram_timing_start "authors" "Authors" + # shellcheck source=generate_authors.sh disable=SC1091 + source "$SCRIPT_DIR/generate_authors.sh" || { echo -e "${RED}Error: Failed to source generate_authors.sh${NC}"; exit 1; } + + # Call the main generation function + # It will internally use AFFECTED_AUTHORS and AUTHORS_INDEX_NEEDS_REBUILD + generate_author_pages || { echo -e "${RED}Error: Author page generation failed.${NC}"; exit 1; } + echo "Generated author pages." + bssg_ram_timing_end +fi +# --- Author Page Generation --- END --- + # --- Archive Page Generation --- START --- # Source and run Archive Page Generator (if enabled) if [ "${ENABLE_ARCHIVES:-false}" = true ]; then + bssg_ram_timing_start "archives" "Archives" # Source the script (loads functions) # shellcheck source=generate_archives.sh disable=SC1091 source "$SCRIPT_DIR/generate_archives.sh" || { echo -e "${RED}Error: Failed to source generate_archives.sh${NC}"; exit 1; } @@ -318,28 +544,68 @@ if [ "${ENABLE_ARCHIVES:-false}" = true ]; then # It will internally use AFFECTED_ARCHIVE_MONTHS and ARCHIVE_INDEX_NEEDS_REBUILD generate_archive_pages || { echo -e "${RED}Error: Archive page generation failed.${NC}"; exit 1; } echo "Generated archive pages." + bssg_ram_timing_end fi # --- Archive Page Generation --- END --- # --- Main Index Page Generation --- START --- # Source and run Main Index Page Generator # shellcheck source=generate_index.sh disable=SC1091 +bssg_ram_timing_start "main_index" "Main Index" source "$SCRIPT_DIR/generate_index.sh" || { echo -e "${RED}Error: Failed to source generate_index.sh${NC}"; exit 1; } # Call the main function from the sourced script generate_index || { echo -e "${RED}Error: Index page generation failed.${NC}"; exit 1; } echo "Generated main index/pagination pages." +bssg_ram_timing_end # --- Main Index Page Generation --- END --- # --- Feed Generation --- START --- # Source and run Feed Generator # shellcheck source=generate_feeds.sh disable=SC1091 +bssg_ram_timing_start "feeds" "Sitemap/RSS" source "$SCRIPT_DIR/generate_feeds.sh" || { echo -e "${RED}Error: Failed to source generate_feeds.sh${NC}"; exit 1; } # Call the functions from the sourced script echo "Timing sitemap generation..." -generate_sitemap || echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" # Allow failure -echo "Timing RSS feed generation..." -generate_rss || echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" # Allow failure +if [ "${BSSG_RAM_MODE:-false}" = true ]; then + echo "Timing RSS feed generation..." + feed_jobs=0 + feed_jobs=$(get_parallel_jobs) + if [ "$feed_jobs" -gt 1 ]; then + echo "RAM mode: generating sitemap and RSS in parallel..." + + sitemap_failed=false + rss_failed=false + + generate_sitemap & + sitemap_pid=$! + + generate_rss & + rss_pid=$! + + if ! wait "$sitemap_pid"; then + sitemap_failed=true + fi + if ! wait "$rss_pid"; then + rss_failed=true + fi + + if $sitemap_failed; then + echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" + fi + if $rss_failed; then + echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" + fi + else + generate_sitemap || echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" # Allow failure + generate_rss || echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" # Allow failure + fi +else + generate_sitemap || echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" # Allow failure + echo "Timing RSS feed generation..." + generate_rss || echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" # Allow failure +fi echo "Generated RSS feed and sitemap." +bssg_ram_timing_end # --- Feed Generation --- END --- # --- Secondary Pages Index Generation --- START --- @@ -349,10 +615,12 @@ echo "Generated RSS feed and sitemap." # We attempt to reconstruct the array from the exported string. # shellcheck disable=SC2154 # SECONDARY_PAGES is exported by templates.sh if [ -n "$SECONDARY_PAGES" ] && [ "$SECONDARY_PAGES" != "()" ]; then + bssg_ram_timing_start "secondary_index" "Secondary Index" # shellcheck source=generate_secondary_pages.sh disable=SC1091 source "$SCRIPT_DIR/generate_secondary_pages.sh" || { echo -e "${RED}Error: Failed to source generate_secondary_pages.sh${NC}"; exit 1; } generate_pages_index || echo -e "${YELLOW}Secondary pages index generation failed, continuing build...${NC}" # Allow failure echo "Generated secondary pages index." + bssg_ram_timing_end else echo "No secondary pages defined, skipping secondary index generation." fi @@ -361,6 +629,7 @@ fi # --- Asset Handling --- START --- # Source the asset handling script # shellcheck source=assets.sh disable=SC1091 +bssg_ram_timing_start "assets" "Assets/CSS" source "$SCRIPT_DIR/assets.sh" || { echo -e "${RED}Error: Failed to source assets.sh${NC}"; exit 1; } # Copy static assets echo "Timing static files copy..." @@ -369,35 +638,143 @@ copy_static_files || { echo -e "${RED}Error: Failed to copy static assets.${NC}" echo "Timing CSS/Theme processing..." create_css "$OUTPUT_DIR" "$THEME" || { echo -e "${RED}Error: Failed to process CSS.${NC}"; exit 1; } # Pass OUTPUT_DIR and THEME echo "Handled static assets and CSS." +bssg_ram_timing_end # --- Asset Handling --- END --- # --- Post Processing --- START --- # Source and run Post Processor # shellcheck source=post_process.sh disable=SC1091 +bssg_ram_timing_start "post_process" "Post Processing" source "$SCRIPT_DIR/post_process.sh" || { echo -e "${RED}Error: Failed to source post_process.sh${NC}"; exit 1; } echo "Timing URL post-processing..." post_process_urls || echo -e "${YELLOW}URL post-processing failed, continuing...${NC}" # Allow failure echo "Timing output permissions fix..." fix_output_permissions || echo -e "${YELLOW}Fixing output permissions failed, continuing...${NC}" # Allow failure echo "Completed post-processing." +bssg_ram_timing_end # --- Post Processing --- END --- # --- Final Cache Update --- START --- -create_config_hash +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + create_config_hash +fi # --- Final Cache Update --- END --- # --- Final Cleanup --- START --- -echo "Cleaning up previous index files..." -rm -f "${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" -rm -f "${CACHE_DIR:-.bssg_cache}/tags_index_prev.txt" -rm -f "${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + echo "Cleaning up previous index files..." + rm -f "${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" + rm -f "${CACHE_DIR:-.bssg_cache}/tags_index_prev.txt" + rm -f "${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt" + rm -f "${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt" -# Remove the frontmatter changes marker if it exists -rm -f "${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" + # Remove the frontmatter changes marker if it exists + rm -f "${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" + + # Clean up related posts temporary files to prevent unnecessary cache invalidation on next build + rm -f "${CACHE_DIR:-.bssg_cache}/modified_tags.list" + rm -f "${CACHE_DIR:-.bssg_cache}/modified_authors.list" + rm -f "${CACHE_DIR:-.bssg_cache}/related_posts_invalidated.list" +fi # --- Final Cleanup --- END --- +# --- Pre-compress Assets --- START --- +_precompress_single_file() { + local file="$1" + local gzfile="$2" + local compression_level="$3" + local verbose_logs="$4" + + if [ "$verbose_logs" = "true" ]; then + echo "Compressing: $file" + fi + gzip -c "-${compression_level}" -- "$file" > "$gzfile" +} + +precompress_assets() { + # Check if pre-compression is enabled in the config. + if [ ! "${PRECOMPRESS_ASSETS:-false}" = "true" ]; then + return + fi + + echo "Starting pre-compression of assets..." + local compression_level="${PRECOMPRESS_GZIP_LEVEL:-9}" + if ! [[ "$compression_level" =~ ^[1-9]$ ]]; then + compression_level=9 + fi + local verbose_logs="${PRECOMPRESS_VERBOSE:-${RAM_MODE_VERBOSE:-false}}" + + # 1. Cleanup: Remove any .gz file that does not have a corresponding original file. + # This handles cases where original files were deleted. + # Using -print0 and read -d '' to safely handle filenames with spaces or special chars. + find "${OUTPUT_DIR}" -type f -name "*.gz" -print0 | while IFS= read -r -d '' gzfile; do + original_file="${gzfile%.gz}" + if [ ! -f "$original_file" ]; then + echo "Removing stale compressed file: $gzfile" + rm -- "$gzfile" + fi + done + + # 2. Compression: Compress text files if they are new or have been updated. + # We target .html, .css, .xml and .js files. + local changed_files=() + while IFS= read -r -d '' file; do + local gzfile="${file}.gz" + # Compress if the .gz file doesn't exist, or if the original file is newer. + if [ ! -f "$gzfile" ] || [ "$file" -nt "$gzfile" ]; then + changed_files+=("$file") + fi + done < <(find "${OUTPUT_DIR}" -type f \( -name "*.html" -o -name "*.css" -o -name "*.xml" -o -name "*.js" \) -print0) + + if [ "${#changed_files[@]}" -eq 0 ]; then + echo "No changed assets to pre-compress." + echo "Asset pre-compression finished." + return + fi + + local compress_jobs + compress_jobs=$(get_parallel_jobs "${PRECOMPRESS_MAX_JOBS:-0}") + if [ "$compress_jobs" -gt "${#changed_files[@]}" ]; then + compress_jobs="${#changed_files[@]}" + fi + + if [ "$compress_jobs" -gt 1 ]; then + local file gzfile q_file q_gzfile q_level q_verbose + q_level=$(printf '%q' "$compression_level") + q_verbose=$(printf '%q' "$verbose_logs") + run_parallel "$compress_jobs" < <( + for file in "${changed_files[@]}"; do + gzfile="${file}.gz" + q_file=$(printf '%q' "$file") + q_gzfile=$(printf '%q' "$gzfile") + printf "_precompress_single_file %s %s %s %s\n" "$q_file" "$q_gzfile" "$q_level" "$q_verbose" + done + ) || { echo -e "${RED}Asset pre-compression failed.${NC}"; return 1; } + else + local file gzfile + for file in "${changed_files[@]}"; do + gzfile="${file}.gz" + _precompress_single_file "$file" "$gzfile" "$compression_level" "$verbose_logs" || { + echo -e "${RED}Asset pre-compression failed for ${file}.${NC}" + return 1 + } + done + fi + + echo "Pre-compressed ${#changed_files[@]} assets using ${compress_jobs} worker(s) (gzip -${compression_level})." + + echo "Asset pre-compression finished." +} + +# Execute the asset compression. +bssg_ram_timing_start "precompress" "Pre-compress" +precompress_assets +bssg_ram_timing_end +# --- Pre-compress Assets --- END --- + # --- Deployment --- START --- +bssg_ram_timing_start "deployment" "Deployment Decision/Run" deploy_now="false" if [[ "${CMD_DEPLOY_OVERRIDE:-unset}" == "true" ]]; then # Use default value for safety deploy_now="true" @@ -458,12 +835,15 @@ if [[ "$deploy_now" == "true" ]]; then echo -e "${YELLOW}Warning: Deployment was requested, but DEPLOY_SCRIPT is not set in configuration.${NC}" fi fi +bssg_ram_timing_end # --- Deployment --- END --- # --- End of execution --- BUILD_END_TIME=$(date +%s) BUILD_DURATION=$((BUILD_END_TIME - BUILD_START_TIME)) +bssg_ram_timing_print_summary echo "------------------------------------------------------" echo -e "${GREEN}Build process completed in ${BUILD_DURATION} seconds.${NC}" + exit 0 diff --git a/scripts/build/ram_mode.sh b/scripts/build/ram_mode.sh new file mode 100644 index 0000000..a659766 --- /dev/null +++ b/scripts/build/ram_mode.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# +# BSSG - RAM Build Helpers +# Preloads input content in memory and provides lookup helpers. +# + +# Guard against duplicate sourcing +if [[ -n "${BSSG_RAM_MODE_SCRIPT_LOADED:-}" ]]; then + return 0 +fi +export BSSG_RAM_MODE_SCRIPT_LOADED=1 + +# In-memory stores +declare -gA BSSG_RAM_FILE_CONTENT=() +declare -gA BSSG_RAM_FILE_MTIME=() +declare -gA BSSG_RAM_DATASET=() +declare -gA BSSG_RAM_BASENAME_KEY=() +declare -ga BSSG_RAM_SRC_FILES=() +declare -ga BSSG_RAM_PAGE_FILES=() +declare -ga BSSG_RAM_TEMPLATE_FILES=() + +ram_mode_enabled() { + [[ "${BSSG_RAM_MODE:-false}" == "true" ]] +} + +_ram_mode_disk_mtime() { + local file="$1" + local kernel_name + kernel_name=$(uname -s) + if [[ "$kernel_name" == "Darwin" ]] || [[ "$kernel_name" == *"BSD" ]]; then + stat -f "%m" "$file" 2>/dev/null || echo "0" + else + stat -c "%Y" "$file" 2>/dev/null || echo "0" + fi +} + +ram_mode_resolve_key() { + local file="$1" + if [[ -z "$file" ]]; then + return 1 + fi + + if [[ -n "${BSSG_RAM_FILE_CONTENT[$file]+_}" || -n "${BSSG_RAM_FILE_MTIME[$file]+_}" ]]; then + echo "$file" + return 0 + fi + + if [[ "$file" == /* && -n "${BSSG_PROJECT_ROOT:-}" ]]; then + local prefix="${BSSG_PROJECT_ROOT%/}/" + if [[ "$file" == "$prefix"* ]]; then + local rel="${file#"$prefix"}" + if [[ -n "${BSSG_RAM_FILE_CONTENT[$rel]+_}" || -n "${BSSG_RAM_FILE_MTIME[$rel]+_}" ]]; then + echo "$rel" + return 0 + fi + fi + fi + + if [[ "$file" != */* && -n "${BSSG_RAM_BASENAME_KEY[$file]+_}" ]]; then + local mapped="${BSSG_RAM_BASENAME_KEY[$file]}" + if [[ "$mapped" != "__AMBIGUOUS__" ]]; then + echo "$mapped" + return 0 + fi + fi + + echo "$file" + return 0 +} + +ram_mode_has_file() { + local key + if ! key=$(ram_mode_resolve_key "$1"); then + return 1 + fi + [[ -n "${BSSG_RAM_FILE_CONTENT[$key]+_}" || -n "${BSSG_RAM_FILE_MTIME[$key]+_}" ]] +} + +ram_mode_get_content() { + local key + if ! key=$(ram_mode_resolve_key "$1"); then + return 0 + fi + if [[ -n "${BSSG_RAM_FILE_CONTENT[$key]+_}" ]]; then + printf '%s' "${BSSG_RAM_FILE_CONTENT[$key]}" + fi +} + +ram_mode_get_mtime() { + local key + if ! key=$(ram_mode_resolve_key "$1"); then + printf '0\n' + return 0 + fi + if [[ -n "${BSSG_RAM_FILE_MTIME[$key]+_}" ]]; then + printf '%s\n' "${BSSG_RAM_FILE_MTIME[$key]}" + else + printf '0\n' + fi +} + +ram_mode_list_src_files() { + if [[ ${#BSSG_RAM_SRC_FILES[@]} -eq 0 ]]; then + return 0 + fi + printf '%s\n' "${BSSG_RAM_SRC_FILES[@]}" +} + +ram_mode_list_page_files() { + if [[ ${#BSSG_RAM_PAGE_FILES[@]} -eq 0 ]]; then + return 0 + fi + printf '%s\n' "${BSSG_RAM_PAGE_FILES[@]}" +} + +ram_mode_set_dataset() { + local key="$1" + local value="$2" + BSSG_RAM_DATASET["$key"]="$value" +} + +ram_mode_get_dataset() { + local key="$1" + if [[ -n "${BSSG_RAM_DATASET[$key]+_}" ]]; then + printf '%s' "${BSSG_RAM_DATASET[$key]}" + fi +} + +ram_mode_clear_dataset() { + local key="$1" + unset 'BSSG_RAM_DATASET[$key]' +} + +ram_mode_dataset_line_count() { + local key="$1" + local data + data=$(ram_mode_get_dataset "$key") + if [[ -z "$data" ]]; then + echo "0" + return 0 + fi + printf '%s\n' "$data" | awk 'NF { c++ } END { print c+0 }' +} + +_ram_mode_store_file() { + local file="$1" + [[ -f "$file" ]] || return 0 + + local file_content + file_content=$(cat "$file") + BSSG_RAM_FILE_CONTENT["$file"]="$file_content" + BSSG_RAM_FILE_MTIME["$file"]="$(_ram_mode_disk_mtime "$file")" + + local base + base=$(basename "$file") + if [[ -z "${BSSG_RAM_BASENAME_KEY[$base]+_}" ]]; then + BSSG_RAM_BASENAME_KEY["$base"]="$file" + elif [[ "${BSSG_RAM_BASENAME_KEY[$base]}" != "$file" ]]; then + BSSG_RAM_BASENAME_KEY["$base"]="__AMBIGUOUS__" + fi +} + +_ram_mode_collect_content_files() { + local dir="$1" + [[ -d "$dir" ]] || return 0 + find "$dir" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort +} + +_ram_mode_collect_template_files() { + local dir="$1" + [[ -d "$dir" ]] || return 0 + find "$dir" -type f -name "*.html" -not -path "*/.*" | sort +} + +ram_mode_preload_inputs() { + if ! ram_mode_enabled; then + return 0 + fi + + BSSG_RAM_FILE_CONTENT=() + BSSG_RAM_FILE_MTIME=() + BSSG_RAM_DATASET=() + BSSG_RAM_BASENAME_KEY=() + BSSG_RAM_SRC_FILES=() + BSSG_RAM_PAGE_FILES=() + BSSG_RAM_TEMPLATE_FILES=() + + local file + while IFS= read -r file; do + [[ -z "$file" ]] && continue + BSSG_RAM_SRC_FILES+=("$file") + _ram_mode_store_file "$file" + done < <(_ram_mode_collect_content_files "${SRC_DIR:-src}") + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + BSSG_RAM_PAGE_FILES+=("$file") + _ram_mode_store_file "$file" + done < <(_ram_mode_collect_content_files "${PAGES_DIR:-pages}") + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + BSSG_RAM_TEMPLATE_FILES+=("$file") + _ram_mode_store_file "$file" + done < <(_ram_mode_collect_template_files "${TEMPLATES_DIR:-templates}") + + # Preload active locale (and fallback locale) so date/menu rendering avoids disk reads. + if [[ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]]; then + _ram_mode_store_file "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" + fi + if [[ -f "${LOCALE_DIR:-locales}/en.sh" ]]; then + _ram_mode_store_file "${LOCALE_DIR:-locales}/en.sh" + fi + + print_info "RAM mode preloaded ${#BSSG_RAM_FILE_CONTENT[@]} text files (${#BSSG_RAM_SRC_FILES[@]} posts, ${#BSSG_RAM_PAGE_FILES[@]} pages)." +} + +export -f ram_mode_enabled ram_mode_resolve_key ram_mode_has_file ram_mode_get_content ram_mode_get_mtime +export -f ram_mode_list_src_files ram_mode_list_page_files ram_mode_preload_inputs +export -f ram_mode_set_dataset ram_mode_get_dataset ram_mode_clear_dataset +export -f ram_mode_dataset_line_count diff --git a/scripts/build/related_posts.sh b/scripts/build/related_posts.sh new file mode 100644 index 0000000..e071499 --- /dev/null +++ b/scripts/build/related_posts.sh @@ -0,0 +1,441 @@ +#!/usr/bin/env bash +# +# BSSG - Related Posts Module +# Functions for generating related posts based on shared tags +# + +# Source dependencies +# shellcheck source=utils.sh disable=SC1091 +source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from related_posts.sh"; exit 1; } +# shellcheck source=cache.sh disable=SC1091 +source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from related_posts.sh"; exit 1; } + +# --- Related Posts Functions --- START --- + +declare -gA BSSG_RAM_RELATED_POSTS_HTML=() +declare -g BSSG_RAM_RELATED_POSTS_READY=false +declare -g BSSG_RAM_RELATED_POSTS_LIMIT="" + +_build_post_url_from_date_slug() { + local post_date="$1" + local post_slug="$2" + local post_year post_month post_day + + if [[ "$post_date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y) + post_month=$(date +%m) + post_day=$(date +%d) + fi + + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$post_year}" + url_path="${url_path//Month/$post_month}" + url_path="${url_path//Day/$post_day}" + url_path="${url_path//slug/$post_slug}" + printf '/%s/\n' "$url_path" +} + +_build_ram_related_posts_cache() { + local max_results="${1:-3}" + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + + BSSG_RAM_RELATED_POSTS_HTML=() + BSSG_RAM_RELATED_POSTS_READY=true + BSSG_RAM_RELATED_POSTS_LIMIT="$max_results" + + [ -z "$file_index_data" ] && return 0 + + local scored_results="" + scored_results=$(printf '%s\n' "$file_index_data" | awk -F'|' ' + function trim(s) { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", s) + return s + } + + { + n++ + title[n] = $3 + date[n] = $4 + tags_raw[n] = $6 + slug[n] = $7 + desc[n] = $10 + + split(tags_raw[n], tag_arr, ",") + for (k in tag_arr) { + t = trim(tag_arr[k]) + if (t != "") { + tags[n SUBSEP t] = 1 + } + } + } + + END { + for (i = 1; i <= n; i++) { + if (slug[i] == "" || tags_raw[i] == "") { + continue + } + + split(tags_raw[i], i_tags, ",") + for (j = 1; j <= n; j++) { + if (i == j || slug[j] == "" || date[j] == "" || tags_raw[j] == "") { + continue + } + + score = 0 + delete seen + for (k in i_tags) { + t = trim(i_tags[k]) + if (t == "" || seen[t]) { + continue + } + seen[t] = 1 + if (tags[j SUBSEP t]) { + score++ + } + } + + if (score > 0) { + printf "%s|%d|%s|%s|%s|%s\n", slug[i], score, date[j], title[j], slug[j], desc[j] + } + } + } + } + ' | sort -t'|' -k1,1 -k2,2nr -k3,3r) + + [ -z "$scored_results" ] && return 0 + + local current_slug="" current_count=0 + local html_output="" + local slug score date title related_slug description + + while IFS='|' read -r slug score date title related_slug description; do + [ -z "$slug" ] && continue + + if [ "$slug" != "$current_slug" ]; then + if [ -n "$current_slug" ] && [ "$current_count" -gt 0 ]; then + html_output+=''$'\n' + html_output+=''$'\n' + BSSG_RAM_RELATED_POSTS_HTML["$current_slug"]="$html_output" + fi + current_slug="$slug" + current_count=0 + html_output="" + fi + + if [ "$current_count" -ge "$max_results" ]; then + continue + fi + + local post_url + post_url=$(_build_post_url_from_date_slug "$date" "$related_slug") + + local short_desc="$description" + if [[ ${#short_desc} -gt 120 ]]; then + short_desc="${short_desc:0:117}..." + fi + + if [ "$current_count" -eq 0 ]; then + html_output+=''$'\n' + BSSG_RAM_RELATED_POSTS_HTML["$current_slug"]="$html_output" + fi +} + +prepare_related_posts_ram_cache() { + local max_results="${1:-3}" + if [ "${BSSG_RAM_MODE:-false}" != true ]; then + return 0 + fi + + if [ "$BSSG_RAM_RELATED_POSTS_READY" = true ] && [ "$BSSG_RAM_RELATED_POSTS_LIMIT" = "$max_results" ]; then + return 0 + fi + + _build_ram_related_posts_cache "$max_results" +} + +# Generate related posts for a given post based on shared tags +# Args: $1=current_post_slug $2=current_post_tags $3=current_post_date $4=max_results (optional, default=3) +# Returns: HTML snippet with related posts +generate_related_posts() { + local current_slug="$1" + local current_tags="$2" + local current_date="$3" + local max_results="${4:-3}" + + # Validate inputs + if [[ -z "$current_slug" || -z "$current_tags" ]]; then + return 0 # No related posts if missing essential data + fi + + # RAM mode uses a precomputed in-memory map to avoid repeated O(n^2) scans. + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + if [ "$BSSG_RAM_RELATED_POSTS_READY" != true ] || [ "$BSSG_RAM_RELATED_POSTS_LIMIT" != "$max_results" ]; then + _build_ram_related_posts_cache "$max_results" + fi + if [[ -n "${BSSG_RAM_RELATED_POSTS_HTML[$current_slug]+_}" ]]; then + printf '%s' "${BSSG_RAM_RELATED_POSTS_HTML[$current_slug]}" + fi + return 0 + fi + + # Check cache first + local cache_file="${CACHE_DIR:-.bssg_cache}/related_posts/${current_slug}.html" + local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" + + # Create cache directory if it doesn't exist + mkdir -p "$(dirname "$cache_file")" + + # Check if cache is valid (newer than file index) + if [[ -f "$cache_file" && -f "$file_index" ]]; then + if [[ "$cache_file" -nt "$file_index" ]] && [[ "${FORCE_REBUILD:-false}" != true ]]; then + cat "$cache_file" + return 0 + fi + fi + + # Generate related posts + local related_posts_html="" + related_posts_html=$(compute_related_posts "$current_slug" "$current_tags" "$current_date" "$max_results") + + # Cache the result + echo "$related_posts_html" > "$cache_file" + + # Output the result + echo "$related_posts_html" +} + +# Core algorithm to compute related posts +compute_related_posts() { + local current_slug="$1" + local current_tags="$2" + local current_date="$3" + local max_results="$4" + + local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" + local file_index_data="" + local ram_mode_active=false + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_active=true + file_index_data=$(ram_mode_get_dataset "file_index") + fi + + if $ram_mode_active; then + if [[ -z "$file_index_data" ]]; then + return 0 + fi + elif [[ ! -f "$file_index" ]]; then + return 0 # No posts to compare against + fi + + # Convert current tags to array for comparison + IFS=',' read -ra current_tags_array <<< "$current_tags" + local current_tags_clean=() + for tag in "${current_tags_array[@]}"; do + tag=$(echo "$tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # Trim whitespace + if [[ -n "$tag" ]]; then + current_tags_clean+=("$tag") + fi + done + + # If no valid tags, return empty + if [[ ${#current_tags_clean[@]} -eq 0 ]]; then + return 0 + fi + + # Process all posts and calculate similarity scores + local temp_results="" + + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + # Skip current post + if [[ "$slug" == "$current_slug" ]]; then + continue + fi + + # Skip posts without tags or date + if [[ -z "$tags" || -z "$date" ]]; then + continue + fi + + # Calculate similarity score based on shared tags + local score=0 + IFS=',' read -ra post_tags_array <<< "$tags" + + for post_tag in "${post_tags_array[@]}"; do + post_tag=$(echo "$post_tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # Trim whitespace + if [[ -n "$post_tag" ]]; then + for current_tag in "${current_tags_clean[@]}"; do + if [[ "$post_tag" == "$current_tag" ]]; then + score=$((score + 1)) + break + fi + done + fi + done + + # Only consider posts with at least one shared tag + if [[ $score -gt 0 ]]; then + # Store: score|date|title|slug|description + temp_results+="${score}|${date}|${title}|${slug}|${description}"$'\n' + fi + + done < <( + if $ram_mode_active; then + printf '%s\n' "$file_index_data" | awk 'NF' + else + cat "$file_index" + fi + ) + + # Sort by score (descending), then by date (descending), limit results + local sorted_results="" + if [[ -n "$temp_results" ]]; then + sorted_results=$(printf '%s\n' "$temp_results" | awk 'NF' | sort -t'|' -k1,1nr -k2,2r | head -n "$max_results") + fi + + # Generate HTML output + if [[ -z "$sorted_results" ]]; then + return 0 # No related posts found + fi + + local html_output="" + html_output+=''$'\n' + + echo "$html_output" +} + +# Clean related posts cache (called when posts are modified) +clean_related_posts_cache() { + local cache_dir="${CACHE_DIR:-.bssg_cache}/related_posts" + if [[ -d "$cache_dir" ]]; then + echo -e "${YELLOW}Cleaning related posts cache...${NC}" + rm -rf "$cache_dir" + mkdir -p "$cache_dir" + fi +} + +# Invalidate related posts cache for posts that share tags with modified posts +# Args: $1=path to modified tags list file, $2=optional output file for invalidated post slugs +invalidate_related_posts_cache_for_tags() { + local modified_tags_file="$1" + local invalidated_output_file="$2" + local cache_dir="${CACHE_DIR:-.bssg_cache}/related_posts" + local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" + + if [[ ! -f "$modified_tags_file" || ! -d "$cache_dir" || ! -f "$file_index" ]]; then + return 0 + fi + + # Read modified tags into array + local modified_tags=() + while IFS= read -r tag; do + if [[ -n "$tag" ]]; then + modified_tags+=("$tag") + fi + done < "$modified_tags_file" + + if [[ ${#modified_tags[@]} -eq 0 ]]; then + return 0 + fi + + echo -e "${YELLOW}Invalidating related posts cache for posts with modified tags...${NC}" + + # Find posts that have any of the modified tags and remove their cache + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + if [[ -n "$tags" && -n "$slug" ]]; then + IFS=',' read -ra post_tags_array <<< "$tags" + local should_invalidate=false + + for post_tag in "${post_tags_array[@]}"; do + post_tag=$(echo "$post_tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [[ -n "$post_tag" ]]; then + for modified_tag in "${modified_tags[@]}"; do + if [[ "$post_tag" == "$modified_tag" ]]; then + should_invalidate=true + break 2 + fi + done + fi + done + + if [[ "$should_invalidate" == true ]]; then + local cache_file="$cache_dir/${slug}.html" + if [[ -f "$cache_file" ]]; then + rm -f "$cache_file" + echo -e " Invalidated cache for post: ${GREEN}$slug${NC}" + fi + + # Write the slug to the output file if provided + if [[ -n "$invalidated_output_file" ]]; then + echo "$slug" >> "$invalidated_output_file" + fi + fi + fi + done < "$file_index" +} + +# --- Related Posts Functions --- END --- + +# Export functions for use by other scripts +export -f generate_related_posts compute_related_posts clean_related_posts_cache invalidate_related_posts_cache_for_tags +export -f prepare_related_posts_ram_cache diff --git a/scripts/build/templates.sh b/scripts/build/templates.sh index d640ab3..293cf45 100755 --- a/scripts/build/templates.sh +++ b/scripts/build/templates.sh @@ -63,7 +63,9 @@ load_template() { # Function to pre-load all templates and process menus/placeholders preload_templates() { # Create template cache directory if it doesn't exist - mkdir -p "$TEMPLATE_CACHE_DIR" + if [ "${BSSG_RAM_MODE:-false}" != true ]; then + mkdir -p "$TEMPLATE_CACHE_DIR" + fi local template_dir local templates_to_load=("header.html" "footer.html" "post.html" "page.html" "index.html" "tag.html" "archive.html") @@ -131,10 +133,16 @@ preload_templates() { # Scan pages directory for markdown and HTML files if [ -d "${PAGES_DIR:-pages}" ]; then - local page_files - page_files=($(find "${PAGES_DIR:-pages}" -type f \( -name "*.md" -o -name "*.html" \) | sort)) + local page_files=() + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_list_page_files > /dev/null; then + mapfile -t page_files < <(ram_mode_list_page_files) + else + page_files=($(find "${PAGES_DIR:-pages}" -type f \( -name "*.md" -o -name "*.html" \) | sort)) + fi for file in "${page_files[@]}"; do + [[ -z "$file" ]] && continue + # Skip if file is hidden if [[ $(basename "$file") == .* ]]; then continue @@ -144,10 +152,19 @@ preload_templates() { local title slug date secondary if [[ "$file" == *.html ]]; then # Crude HTML parsing - assumes specific meta tags exist - title=$(grep -m 1 '' "$file" 2>/dev/null | sed 's/<[^>]*>//g') - slug=$(grep -m 1 'meta name="slug"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - date=$(grep -m 1 'meta name="date"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') # Extract date from meta - secondary=$(grep -m 1 'meta name="secondary"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + local html_source="" + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then + html_source=$(ram_mode_get_content "$file") + title=$(printf '%s\n' "$html_source" | grep -m 1 '<title>' 2>/dev/null | sed 's/<[^>]*>//g') + slug=$(printf '%s\n' "$html_source" | grep -m 1 'meta name="slug"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + date=$(printf '%s\n' "$html_source" | grep -m 1 'meta name="date"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + secondary=$(printf '%s\n' "$html_source" | grep -m 1 'meta name="secondary"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + else + title=$(grep -m 1 '<title>' "$file" 2>/dev/null | sed 's/<[^>]*>//g') + slug=$(grep -m 1 'meta name="slug"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + date=$(grep -m 1 'meta name="date"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') # Extract date from meta + secondary=$(grep -m 1 'meta name="secondary"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + fi else # Assumes parse_metadata is available title=$(parse_metadata "$file" "title") @@ -206,11 +223,40 @@ preload_templates() { # Add standard menu items local tags_flag_file="${CACHE_DIR:-.bssg_cache}/has_tags.flag" - # Add tags link only if the flag file exists (meaning tags were found in the last indexing run) - if [ -f "$tags_flag_file" ]; then + local has_tags=false + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + [ -n "$(ram_mode_get_dataset "has_tags")" ] && has_tags=true + elif [ -f "$tags_flag_file" ]; then + has_tags=true + fi + # Add tags link only if tags are present. + if [ "$has_tags" = true ]; then menu_items+=" <a href=\"${SITE_URL}/tags/\">${MSG_TAGS:-"Tags"}</a>" fi + # Add Authors link if enabled and multiple authors exist + local authors_flag_file="${CACHE_DIR:-.bssg_cache}/has_authors.flag" + if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ]; then + # Check if we have multiple authors (more than the threshold) + local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" + local unique_author_count=0 + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local authors_index_data + authors_index_data=$(ram_mode_get_dataset "authors_index") + if [ -n "$authors_index_data" ]; then + unique_author_count=$(printf '%s\n' "$authors_index_data" | awk -F'|' 'NF { print $1 }' | sort -u | wc -l | tr -d ' ') + fi + elif [ -f "$authors_index_file" ] && [ -f "$authors_flag_file" ]; then + unique_author_count=$(awk -F'|' '{print $1}' "$authors_index_file" | sort -u | wc -l) + fi + if [ "$unique_author_count" -gt 0 ]; then + local threshold="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" + if [ "$unique_author_count" -ge "$threshold" ]; then + menu_items+=" <a href=\"${SITE_URL}/authors/\">${MSG_AUTHORS:-"Authors"}</a>" + fi + fi + fi + # Only add Archives link if enabled if [ "${ENABLE_ARCHIVES:-true}" = true ]; then menu_items+=" <a href=\"${SITE_URL}/archives/\">${MSG_ARCHIVES:-"Archives"}</a>" @@ -219,9 +265,30 @@ preload_templates() { menu_items+=" <a href=\"${SITE_URL}/${RSS_FILENAME:-rss.xml}\">${MSG_RSS:-"RSS"}</a>" # Add tags link to footer only if the flag file exists - if [ -f "$tags_flag_file" ]; then + if [ "$has_tags" = true ]; then footer_items+=" <a href=\"${SITE_URL}/tags/\">${MSG_TAGS:-"Tags"}</a> ·" fi + + # Add Authors link to footer if enabled and multiple authors exist + if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ]; then + local unique_author_count_footer=0 + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local authors_index_data_footer + authors_index_data_footer=$(ram_mode_get_dataset "authors_index") + if [ -n "$authors_index_data_footer" ]; then + unique_author_count_footer=$(printf '%s\n' "$authors_index_data_footer" | awk -F'|' 'NF { print $1 }' | sort -u | wc -l | tr -d ' ') + fi + elif [ -f "$authors_index_file" ] && [ -f "$authors_flag_file" ]; then + unique_author_count_footer=$(awk -F'|' '{print $1}' "$authors_index_file" | sort -u | wc -l) + fi + if [ "$unique_author_count_footer" -gt 0 ]; then + local threshold_footer="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" + if [ "$unique_author_count_footer" -ge "$threshold_footer" ]; then + footer_items+=" <a href=\"${SITE_URL}/authors/\">${MSG_AUTHORS:-"Authors"}</a> ·" + fi + fi + fi + footer_items+=" <a href=\"${SITE_URL}/${RSS_FILENAME:-rss.xml}\">${MSG_SUBSCRIBE_RSS:-"Subscribe via RSS"}</a>" # Replace menu placeholders in templates @@ -253,6 +320,24 @@ preload_templates() { HEADER_TEMPLATE=$(echo "$HEADER_TEMPLATE" | sed "s|{{[[:space:]]*rss_filename[[:space:]]*}}|${RSS_FILENAME:-rss.xml}|g") # --- Add RSS Filename Placeholder --- END --- + # --- Handle rel="me" Verification Link --- START --- + local rel_me_tags="" + if [ -n "${REL_ME_URLS_SERIALIZED:-}" ]; then + local rel_me_link_url rel_me_href + while IFS= read -r rel_me_link_url; do + [ -n "$rel_me_link_url" ] || continue + rel_me_href=$(html_escape "$rel_me_link_url") + rel_me_tags+="<link rel=\"me\" href=\"${rel_me_href}\">"$'\n' + done <<< "$REL_ME_URLS_SERIALIZED" + rel_me_tags="${rel_me_tags%$'\n'}" + print_info "Adding rel=\"me\" verification links from REL_ME_URL/REL_ME_URLS." + else + print_info "No REL_ME_URL or REL_ME_URLS specified, skipping rel=\"me\" links." + fi + HEADER_TEMPLATE=$(echo "$HEADER_TEMPLATE" | sed "s|{{[[:space:]]*rel_me_link[[:space:]]*}}|__BSSG_REL_ME_LINK__|g") + HEADER_TEMPLATE=${HEADER_TEMPLATE//__BSSG_REL_ME_LINK__/$rel_me_tags} + # --- Handle rel="me" Verification Link --- END --- + # --- Handle Custom CSS --- START --- local custom_css_tag="" if [ -n "$CUSTOM_CSS" ]; then @@ -273,50 +358,55 @@ preload_templates() { HEADER_TEMPLATE=$(echo "$HEADER_TEMPLATE" | sed "s|{{[[:space:]]*custom_css_link[[:space:]]*}}|${custom_css_tag}|") # --- Handle Custom CSS --- END --- - # Write primary and secondary page lists to cache files only if changed - local primary_pages_cache="$CACHE_DIR/primary_pages.tmp" - local secondary_pages_cache="$CACHE_DIR/secondary_pages.tmp" - local secondary_pages_list_file="$CACHE_DIR/secondary_pages.list" # <-- Define list file path - - # Prepare content in temporary files - local primary_tmp=$(mktemp) - local secondary_tmp=$(mktemp) - local secondary_list_tmp=$(mktemp) # <-- Temp file for the list - - # Write current content to temporary files - # Use printf for safer writing - for page in "${primary_pages[@]}"; do - printf "%s\n" "$page" >> "$primary_tmp" - done - for page in "${SECONDARY_PAGES[@]}"; do - # Write to the temp file for comparison - printf "%s\n" "$page" >> "$secondary_tmp" - # Also write to the list temp file, one per line - printf "%s\n" "$page" >> "$secondary_list_tmp" - done + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_set_dataset "primary_pages" "$(printf '%s\n' "${primary_pages[@]}")" + ram_mode_set_dataset "secondary_pages" "$(printf '%s\n' "${SECONDARY_PAGES[@]}")" + else + # Write primary and secondary page lists to cache files only if changed + local primary_pages_cache="$CACHE_DIR/primary_pages.tmp" + local secondary_pages_cache="$CACHE_DIR/secondary_pages.tmp" + local secondary_pages_list_file="$CACHE_DIR/secondary_pages.list" # <-- Define list file path + + # Prepare content in temporary files + local primary_tmp=$(mktemp) + local secondary_tmp=$(mktemp) + local secondary_list_tmp=$(mktemp) # <-- Temp file for the list + + # Write current content to temporary files + # Use printf for safer writing + for page in "${primary_pages[@]}"; do + printf "%s\n" "$page" >> "$primary_tmp" + done + for page in "${SECONDARY_PAGES[@]}"; do + # Write to the temp file for comparison + printf "%s\n" "$page" >> "$secondary_tmp" + # Also write to the list temp file, one per line + printf "%s\n" "$page" >> "$secondary_list_tmp" + done - # Function to compare and update cache file - update_cache_if_changed() { - local temp_file="$1" - local cache_file="$2" - local file_desc="$3" + # Function to compare and update cache file + update_cache_if_changed() { + local temp_file="$1" + local cache_file="$2" + local file_desc="$3" - if [ ! -f "$cache_file" ] || ! cmp -s "$temp_file" "$cache_file"; then - mv "$temp_file" "$cache_file" - # echo "DEBUG: Updated $file_desc cache file." # Optional debug - else - rm "$temp_file" - # echo "DEBUG: $file_desc cache file unchanged." # Optional debug - fi - } + if [ ! -f "$cache_file" ] || ! cmp -s "$temp_file" "$cache_file"; then + mv "$temp_file" "$cache_file" + # echo "DEBUG: Updated $file_desc cache file." # Optional debug + else + rm "$temp_file" + # echo "DEBUG: $file_desc cache file unchanged." # Optional debug + fi + } - # Compare and update cache files - update_cache_if_changed "$primary_tmp" "$primary_pages_cache" - update_cache_if_changed "$secondary_tmp" "$secondary_pages_cache" - update_cache_if_changed "$secondary_list_tmp" "$secondary_pages_list_file" # <-- Update the list file + # Compare and update cache files + update_cache_if_changed "$primary_tmp" "$primary_pages_cache" + update_cache_if_changed "$secondary_tmp" "$secondary_pages_cache" + update_cache_if_changed "$secondary_list_tmp" "$secondary_pages_list_file" # <-- Update the list file - # Clean up temporary files - rm -f "$primary_tmp" "$secondary_tmp" "$secondary_list_tmp" # <-- Cleanup list temp file + # Clean up temporary files + rm -f "$primary_tmp" "$secondary_tmp" "$secondary_list_tmp" # <-- Cleanup list temp file + fi echo -e "${GREEN}Templates pre-processed (menus, locale placeholders).${NC}" } @@ -338,4 +428,4 @@ export FOOTER_TEMPLATE # Export functions - Do not export the SECONDARY_PAGES array itself anymore export -f preload_templates -# export SECONDARY_PAGES # <-- Remove this export \ No newline at end of file +# export SECONDARY_PAGES # <-- Remove this export diff --git a/scripts/build/utils.sh b/scripts/build/utils.sh index 15a0cd8..b4c3b9d 100755 --- a/scripts/build/utils.sh +++ b/scripts/build/utils.sh @@ -5,11 +5,44 @@ # # Colors for output messages -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -NC='\033[0m' +if [[ -t 1 ]] && [[ -z $NO_COLOR ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + BLUE='\033[0;34m' + NC='\033[0m' +else + RED="" + GREEN="" + YELLOW="" + BLUE="" + NC="" +fi + +# Cache kernel name once to avoid repeated `uname` calls in hot paths. +if [ -z "${BSSG_KERNEL_NAME:-}" ]; then + BSSG_KERNEL_NAME="$(uname -s 2>/dev/null || echo "")" +fi + +# Cache repeated date formatting work across stages in the same process. +declare -gA BSSG_FORMAT_DATE_CACHE=() +declare -gA BSSG_FORMAT_DATE_TS_CACHE=() + +# GNU parallel workers import functions, but array declarations may not carry over. +# Keep date caches associative in every process to avoid bad-subscript errors. +_bssg_ensure_assoc_cache() { + local var_name="$1" + local var_decl + + var_decl=$(declare -p "$var_name" 2>/dev/null || true) + if [[ "$var_decl" == declare\ -A* ]]; then + return 0 + fi + + unset "$var_name" 2>/dev/null || true + declare -gA "$var_name" + eval "$var_name=()" +} # --- Printing Functions --- START --- print_error() { @@ -61,7 +94,10 @@ format_date() { local format_override="$2" # Optional format string local target_format=${format_override:-"$DATE_FORMAT"} # Use override or global DATE_FORMAT local formatted_date - local kernel_name=$(uname -s) # Get kernel name (e.g., Linux, Darwin, FreeBSD) + local kernel_name="${BSSG_KERNEL_NAME:-}" + if [ -z "$kernel_name" ]; then + kernel_name="$(uname -s)" + fi # Skip formatting if date is empty if [ -z "$input_date" ]; then @@ -83,27 +119,46 @@ format_date() { return fi + _bssg_ensure_assoc_cache "BSSG_FORMAT_DATE_CACHE" + + # Use cached values for stable (non-"now") inputs. + local cache_tz="${TIMEZONE:-local}" + local cache_key="${cache_tz}|${target_format}|${input_date}" + if [[ -n "${BSSG_FORMAT_DATE_CACHE[$cache_key]+_}" ]]; then + echo "${BSSG_FORMAT_DATE_CACHE[$cache_key]}" + return + fi + # Try to format the date using the configured format # IMPORTANT: DATE_FORMAT must be exported or sourced *before* calling this if [[ "$kernel_name" == "Darwin" ]] || [[ "$kernel_name" == *"BSD" ]]; then # macOS/BSD date formatting (uses date -j -f) - # IMPORTANT: Using ISO 8601 format (YYYY-MM-DD HH:MM:SS) in source - # files is strongly recommended for portability. - - # Try parsing full ISO date-time first - formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date\" +\"$target_format\"" 2>/dev/null) - - # If failed, try RFC2822 format - if [ -z "$formatted_date" ]; then + # Fast-path common stable inputs to avoid multiple failed parse attempts. + if [[ "$input_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d\" \"$input_date\" +\"$target_format\"" 2>/dev/null) + elif [[ "$input_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]][0-9]{2}:[0-9]{2}:[0-9]{2}$ ]]; then + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date\" +\"$target_format\"" 2>/dev/null) + elif [[ "$input_date" =~ ^[A-Za-z]{3},[[:space:]][0-9]{2}[[:space:]][A-Za-z]{3}[[:space:]][0-9]{4}[[:space:]][0-9]{2}:[0-9]{2}:[0-9]{2}[[:space:]][+-][0-9]{4}$ ]]; then formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%a, %d %b %Y %H:%M:%S %z\" \"$input_date\" +\"$target_format\"" 2>/dev/null) fi - # If still failed, try parsing date-only (YYYY-MM-DD) and assume midnight + # Fallback parser chain for uncommon/legacy input variants. if [ -z "$formatted_date" ]; then - # Check if input looks like YYYY-MM-DD using shell pattern matching - if [[ "$input_date" == [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then - # Try parsing by appending midnight time - formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date 00:00:00\" +\"$target_format\"" 2>/dev/null) + # Try parsing full ISO date-time first + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date\" +\"$target_format\"" 2>/dev/null) + + # If failed, try RFC2822 format + if [ -z "$formatted_date" ]; then + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%a, %d %b %Y %H:%M:%S %z\" \"$input_date\" +\"$target_format\"" 2>/dev/null) + fi + + # If still failed, try parsing date-only (YYYY-MM-DD) and assume midnight + if [ -z "$formatted_date" ]; then + # Check if input looks like YYYY-MM-DD using shell pattern matching + if [[ "$input_date" == [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then + # Try parsing by appending midnight time + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date 00:00:00\" +\"$target_format\"" 2>/dev/null) + fi fi fi @@ -117,6 +172,7 @@ format_date() { formatted_date=$(eval "${tz_prefix}LC_ALL=C date -d \"$input_date\" +\"$target_format\"" 2>/dev/null || echo "$input_date") fi + BSSG_FORMAT_DATE_CACHE["$cache_key"]="$formatted_date" echo "$formatted_date" } @@ -133,6 +189,16 @@ format_date_from_timestamp() { return fi + _bssg_ensure_assoc_cache "BSSG_FORMAT_DATE_TS_CACHE" + + # Cache by timestamp/format/timezone. + local cache_tz="${TIMEZONE:-local}" + local cache_key="${cache_tz}|${target_format}|${timestamp}" + if [[ -n "${BSSG_FORMAT_DATE_TS_CACHE[$cache_key]+_}" ]]; then + echo "${BSSG_FORMAT_DATE_TS_CACHE[$cache_key]}" + return + fi + # Set TZ environment variable if TIMEZONE is set and not "local" local tz_prefix="" if [ -n "${TIMEZONE:-}" ] && [ "${TIMEZONE:-local}" != "local" ]; then @@ -151,6 +217,7 @@ format_date_from_timestamp() { formatted_date=$(eval "${tz_prefix}LC_ALL=C date -d \"@$timestamp\" +\"$target_format\"" 2>/dev/null || echo "") fi + BSSG_FORMAT_DATE_TS_CACHE["$cache_key"]="$formatted_date" echo "$formatted_date" } @@ -218,7 +285,21 @@ unlock_file() { # Get file modification time in a portable way get_file_mtime() { local file="$1" - local kernel_name=$(uname -s) + local kernel_name="${BSSG_KERNEL_NAME:-}" + + # In RAM mode, prefer preloaded input timestamps. + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_get_mtime > /dev/null; then + local ram_mtime + ram_mtime=$(ram_mode_get_mtime "$file") + if [ -n "$ram_mtime" ] && [ "$ram_mtime" != "0" ]; then + echo "$ram_mtime" + return 0 + fi + fi + + if [ -z "$kernel_name" ]; then + kernel_name="$(uname -s)" + fi # Use specific stat flags based on kernel name # %m for BSD/macOS (seconds since Epoch) @@ -234,58 +315,108 @@ get_file_mtime() { # Fallback parallel implementation using background processes # Used when GNU parallel is not available +detect_cpu_cores() { + if command -v nproc > /dev/null 2>&1; then + nproc + elif command -v sysctl > /dev/null 2>&1; then + sysctl -n hw.ncpu 2>/dev/null || echo 1 + else + echo 2 + fi +} + +# Determine worker count. +# In RAM mode we cap concurrency by default to reduce memory pressure from +# large inherited in-memory arrays in each worker process. +get_parallel_jobs() { + local requested_jobs="$1" + local jobs=0 + + if [[ "$requested_jobs" =~ ^[0-9]+$ ]] && [ "$requested_jobs" -gt 0 ]; then + jobs="$requested_jobs" + else + jobs=$(detect_cpu_cores) + fi + + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local ram_cap="${RAM_MODE_MAX_JOBS:-6}" + if ! [[ "$ram_cap" =~ ^[0-9]+$ ]] || [ "$ram_cap" -lt 1 ]; then + ram_cap=6 + fi + if [ "$jobs" -gt "$ram_cap" ]; then + jobs="$ram_cap" + fi + fi + + if [ "$jobs" -lt 1 ]; then + jobs=1 + fi + + echo "$jobs" +} + run_parallel() { local max_jobs="$1" shift - if [ -z "$max_jobs" ] || [ "$max_jobs" -lt 1 ]; then - # Determine number of CPU cores if not specified - if command -v nproc > /dev/null 2>&1; then - # Linux - max_jobs=$(nproc) - elif command -v sysctl > /dev/null 2>&1; then - # macOS, BSD - max_jobs=$(sysctl -n hw.ncpu 2>/dev/null || echo 1) - else - # Default to 2 jobs if we can't determine - max_jobs=2 - fi + max_jobs=$(get_parallel_jobs "$max_jobs") + + local had_error=0 + local wait_n_supported=0 + if [[ ${BASH_VERSINFO[0]:-0} -gt 4 ]] || { [[ ${BASH_VERSINFO[0]:-0} -eq 4 ]] && [[ ${BASH_VERSINFO[1]:-0} -ge 3 ]]; }; then + wait_n_supported=1 fi - local job_count=0 - local pids=() + if [ "$wait_n_supported" -eq 1 ]; then + local running_jobs=0 - # Read commands from stdin - while read -r cmd; do - # Skip empty lines - [ -z "$cmd" ] && continue + while read -r cmd; do + [ -z "$cmd" ] && continue - # If we've reached max jobs, wait for one to finish - if [ $job_count -ge $max_jobs ]; then - # Wait for any child process to finish - wait -n 2>/dev/null || true - - # Cleanup finished jobs from pids array - local new_pids=() - for pid in "${pids[@]}"; do - if kill -0 $pid 2>/dev/null; then - new_pids+=($pid) + while [ "$running_jobs" -ge "$max_jobs" ]; do + if ! wait -n 2>/dev/null; then + had_error=1 fi + running_jobs=$((running_jobs - 1)) done - pids=("${new_pids[@]}") - # Update job count - job_count=${#pids[@]} - fi + (eval "$cmd") & + running_jobs=$((running_jobs + 1)) + done - # Run the command in the background - (eval "$cmd") & - pids+=($!) - job_count=$((job_count + 1)) - done + while [ "$running_jobs" -gt 0 ]; do + if ! wait -n 2>/dev/null; then + had_error=1 + fi + running_jobs=$((running_jobs - 1)) + done + else + # Portable fallback for older bash without wait -n. + local pids=() + while read -r cmd; do + [ -z "$cmd" ] && continue - # Wait for all remaining jobs to finish - wait + while [ "${#pids[@]}" -ge "$max_jobs" ]; do + local oldest_pid="${pids[0]}" + if ! wait "$oldest_pid"; then + had_error=1 + fi + pids=("${pids[@]:1}") + done + + (eval "$cmd") & + pids+=($!) + done + + local pid + for pid in "${pids[@]}"; do + if ! wait "$pid"; then + had_error=1 + fi + done + fi + + return "$had_error" } # Add a reading time calculation function @@ -319,17 +450,65 @@ html_escape() { fi } +trim_whitespace() { + printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' +} + +resolve_fediverse_creator() { + local author_name="$1" + local post_fediverse_creator="$2" + local resolved_creator="" + + resolved_creator=$(trim_whitespace "$post_fediverse_creator") + if [ -n "$resolved_creator" ]; then + printf '%s' "$resolved_creator" + return 0 + fi + + if [ -n "$author_name" ] && [ -n "${AUTHOR_FEDIVERSE_CREATORS_SERIALIZED:-}" ]; then + local configured_author configured_creator + while IFS=$'\t' read -r configured_author configured_creator; do + [ "$configured_author" = "$author_name" ] || continue + resolved_creator=$(trim_whitespace "$configured_creator") + if [ -n "$resolved_creator" ]; then + printf '%s' "$resolved_creator" + return 0 + fi + done <<< "${AUTHOR_FEDIVERSE_CREATORS_SERIALIZED}" + fi + + trim_whitespace "${FEDIVERSE_CREATOR:-}" +} + +build_fediverse_creator_meta_tag() { + local fediverse_creator + fediverse_creator=$(resolve_fediverse_creator "$1" "$2") + + if [ -z "$fediverse_creator" ]; then + printf '%s' "" + return 0 + fi + + printf '<meta name="fediverse:creator" content="%s">' "$(html_escape "$fediverse_creator")" +} + # Export the functions export -f format_date_from_timestamp export -f generate_slug export -f lock_file export -f unlock_file export -f get_file_mtime +export -f detect_cpu_cores +export -f get_parallel_jobs export -f run_parallel export -f calculate_reading_time export -f html_escape +export -f trim_whitespace +export -f resolve_fediverse_creator +export -f build_fediverse_creator_meta_tag # Export the new print functions export -f print_error export -f print_warning export -f print_success -export -f print_info \ No newline at end of file +export -f print_info +export -f _bssg_ensure_assoc_cache diff --git a/scripts/edit.sh b/scripts/edit.sh index 871c6af..9fbbd10 100755 --- a/scripts/edit.sh +++ b/scripts/edit.sh @@ -60,10 +60,34 @@ else sed_requires_backup_cleanup=true fi -# Function to generate a slug from a title +# Generate a URL-friendly slug from a title +# This implementation matches the one in scripts/build/utils.sh generate_slug() { local title="$1" - echo "$title" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//' + + # Convert to lowercase + local slug=$(echo "$title" | tr '[:upper:]' '[:lower:]') + + # First use iconv to transliterate if available + if command -v iconv >/dev/null 2>&1; then + slug=$(echo "$slug" | iconv -f utf-8 -t ascii//TRANSLIT 2>/dev/null || echo "$slug") + fi + + # Replace all non-alphanumeric characters with hyphens + slug=$(echo "$slug" | sed -e 's/[^a-z0-9]/-/g') + + # Replace multiple consecutive hyphens with a single one + slug=$(echo "$slug" | sed -e 's/--*/-/g') + + # Remove leading and trailing hyphens + slug=$(echo "$slug" | sed -e 's/^-//' -e 's/-$//') + + # If slug is empty, use 'untitled' as fallback + if [ -z "$slug" ]; then + slug="untitled" + fi + + echo "$slug" } # Function to edit a post @@ -166,7 +190,11 @@ edit_post() { # If no date found, use current date if [ -z "$new_date" ]; then - new_date=$(date +"%Y-%m-%d %H:%M:%S %z") + new_date=$(date +"%Y-%m-%d") + else + # Extract only the date portion (YYYY-MM-DD) from the date field + # This handles cases where the date field contains time and timezone + new_date=$(echo "$new_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/') fi # If title found, rename the file diff --git a/scripts/page.sh b/scripts/page.sh index 7c880a0..ca5c862 100755 --- a/scripts/page.sh +++ b/scripts/page.sh @@ -49,10 +49,34 @@ if [ -z "$EDITOR" ]; then fi fi -# Function to generate a slug from a title +# Generate a URL-friendly slug from a title +# This implementation matches the one in scripts/build/utils.sh generate_slug() { local title="$1" - echo "$title" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//' + + # Convert to lowercase + local slug=$(echo "$title" | tr '[:upper:]' '[:lower:]') + + # First use iconv to transliterate if available + if command -v iconv >/dev/null 2>&1; then + slug=$(echo "$slug" | iconv -f utf-8 -t ascii//TRANSLIT 2>/dev/null || echo "$slug") + fi + + # Replace all non-alphanumeric characters with hyphens + slug=$(echo "$slug" | sed -e 's/[^a-z0-9]/-/g') + + # Replace multiple consecutive hyphens with a single one + slug=$(echo "$slug" | sed -e 's/--*/-/g') + + # Remove leading and trailing hyphens + slug=$(echo "$slug" | sed -e 's/^-//' -e 's/-$//') + + # If slug is empty, use 'untitled' as fallback + if [ -z "$slug" ]; then + slug="untitled" + fi + + echo "$slug" } # Function to create a new page diff --git a/scripts/post.sh b/scripts/post.sh index 14ba030..f6cb964 100755 --- a/scripts/post.sh +++ b/scripts/post.sh @@ -49,11 +49,34 @@ if [ -z "$EDITOR" ]; then fi fi -# Function to generate a slug from a title +# Generate a URL-friendly slug from a title +# This implementation matches the one in scripts/build/utils.sh generate_slug() { local title="$1" - # POSIX compliant slug generation - echo "$title" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9]/-/g' -e 's/-\{1,\}/-/g' -e 's/^-//' -e 's/-$//' + + # Convert to lowercase + local slug=$(echo "$title" | tr '[:upper:]' '[:lower:]') + + # First use iconv to transliterate if available + if command -v iconv >/dev/null 2>&1; then + slug=$(echo "$slug" | iconv -f utf-8 -t ascii//TRANSLIT 2>/dev/null || echo "$slug") + fi + + # Replace all non-alphanumeric characters with hyphens + slug=$(echo "$slug" | sed -e 's/[^a-z0-9]/-/g') + + # Replace multiple consecutive hyphens with a single one + slug=$(echo "$slug" | sed -e 's/--*/-/g') + + # Remove leading and trailing hyphens + slug=$(echo "$slug" | sed -e 's/^-//' -e 's/-$//') + + # If slug is empty, use 'untitled' as fallback + if [ -z "$slug" ]; then + slug="untitled" + fi + + echo "$slug" } # Function to display usage information @@ -199,6 +222,9 @@ EOM <meta name="date" content="$display_date"> <meta name="lastmod" content="$display_date"> <meta name="slug" content="$slug"> + <meta name="author_name" content=""> + <meta name="author_email" content=""> + <meta name="fediverse_creator" content=""> </head> <body> <h1>$title</h1> @@ -222,6 +248,9 @@ slug: $slug image: image_caption: description: +author_name: +author_email: +fediverse_creator: --- $initial_content @@ -446,6 +475,9 @@ else <meta name="date" content="$display_date"> <meta name="lastmod" content="$display_date"> <meta name="slug" content="$POST_SLUG"> + <meta name="author_name" content=""> + <meta name="author_email" content=""> + <meta name="fediverse_creator" content=""> </head> <body> <h1>$POST_TITLE</h1> @@ -465,6 +497,9 @@ slug: $POST_SLUG image: image_caption: description: +author_name: +author_email: +fediverse_creator: --- $POST_CONTENT diff --git a/src/2025-04-01-bssg-features-and-examples.md b/src/2025-04-01-bssg-features-and-examples.md index f33b14e..853959e 100644 --- a/src/2025-04-01-bssg-features-and-examples.md +++ b/src/2025-04-01-bssg-features-and-examples.md @@ -6,6 +6,7 @@ slug: bssg-features-examples description: A detailed overview of BSSG's key features with practical examples showing how to get the most out of this Bash Static Site Generator. image: https://picsum.photos/537/354 image_caption: Sample, random pic from picsum +fediverse_creator: @author@example.social --- BSSG (Bash Static Site Generator) offers a powerful yet simple approach to creating static websites. This post demonstrates some of its key features with practical examples. @@ -23,6 +24,7 @@ slug: custom-url-slug image: /path/to/featured-image.jpg image_caption: A caption for your featured image description: A brief summary of your post for previews and SEO +fediverse_creator: @author@example.social --- ``` @@ -123,6 +125,7 @@ When you build your BSSG site, it generates clean HTML with excellent accessibil <meta property="og:title" content="Post Title"> <meta property="og:description" content="Post description"> <meta property="og:url" content="https://example.com/post-slug"> + <meta name="fediverse:creator" content="@author@example.social"> <link rel="stylesheet" href="/css/style.css"> </head> <body> diff --git a/templates/header.html b/templates/header.html index ef24012..c15ae55 100644 --- a/templates/header.html +++ b/templates/header.html @@ -5,22 +5,20 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{page_title}} | {{site_title}} - + - + {{og_image}} - + {{twitter_image}} + {{fediverse_creator_meta}} - - - - + {{rel_me_link}} {{schema_json_ld}} {{custom_css_link}} diff --git a/themes/amiga500/style.css b/themes/amiga500/style.css index bb5ab1c..0d4d584 100644 --- a/themes/amiga500/style.css +++ b/themes/amiga500/style.css @@ -1,3 +1,25 @@ +/* + * Amiga 500 Workbench Theme for BSSG + * Inspired by the classic Amiga Workbench interface + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks + * - Optimized font loading while maintaining authentic look + */ + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Amiga 500 Workbench inspired color scheme */ --bg-color: #0055aa; @@ -21,10 +43,10 @@ --checkered-color1: #0055aa; --checkered-color2: #0066bb; - /* Typography */ - --font-main: 'Topaz', 'Lucida Console', Monaco, monospace; - --font-headings: 'Topaz', 'Lucida Console', Monaco, monospace; - --font-mono: 'Topaz', 'Lucida Console', Monaco, monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Topaz', 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + --font-headings: 'Topaz', 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + --font-mono: 'Topaz', 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; /* Spacing and sizing */ --radius: 0; /* Amiga didn't have rounded corners */ @@ -32,6 +54,7 @@ --transition: 0.2s ease; } +/* OPTIMIZED font loading with better fallbacks */ @font-face { font-family: 'Topaz'; font-style: normal; @@ -41,6 +64,15 @@ font-display: swap; } +/* TEXT BROWSER FALLBACK: Provide fallback when custom fonts aren't supported */ +@supports not (font-display: swap) { + :root { + --font-main: 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + --font-headings: 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + --font-mono: 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + } +} + *, *::before, *::after { box-sizing: border-box; } @@ -62,7 +94,7 @@ html, body { font-smooth: never; } -/* Checkered background pattern like Workbench */ +/* Checkered background pattern like Workbench - OPTIMIZED for performance */ body::before { content: ""; position: fixed; @@ -81,6 +113,17 @@ body::before { z-index: -1; } +/* TEXT BROWSER FALLBACK: Disable complex background when not supported */ +@supports not (background-clip: text) { + body::before { + display: none; + } + + body { + background-color: var(--bg-color); + } +} + ::selection { background-color: var(--link-color); color: var(--window-title-text); @@ -146,6 +189,25 @@ header::after { text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4); } +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title { + color: var(--window-title-bg); + background: none; + } + + .site-title a { + color: var(--window-title-bg); + background: none; + } + + .site-title a:hover, + .site-title a:focus { + color: var(--link-hover-color); + background: none; + } +} + .site-title a { text-decoration: none; display: inline-block; @@ -168,6 +230,18 @@ header::after { text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); } +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-decoration: none; + transform: translateX(2px); + background: linear-gradient(90deg, var(--link-hover-color) 0%, var(--window-title-bg) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); +} + header h1 { font-family: var(--font-headings); font-weight: 400; @@ -219,6 +293,13 @@ nav a:hover { color: var(--window-title-text); } +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); + color: var(--window-title-text); +} + main { padding: 0 15px; margin-bottom: 1.5rem; @@ -465,34 +546,36 @@ article h3, article .meta, article .summary { h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); - color: var(--header-color); - line-height: 1.3; + color: var(--text-color); margin: 1.5rem 0 1rem; font-weight: 400; } h1 { - font-size: 1.4rem; + font-size: 1.8rem; margin-top: 0; - margin-bottom: 0.75rem; + margin-bottom: 1.5rem; } h2 { - font-size: 1.2rem; + font-size: 1.4rem; } h3 { - font-size: 1.1rem; + font-size: 1.2rem; } h4 { - font-size: 1rem; + font-size: 1.1rem; } article h1 { + font-family: var(--font-headings); + font-weight: 400; color: var(--window-title-text); margin: 0; font-size: 1.1rem; + letter-spacing: 0; position: absolute; top: 0; left: 4px; @@ -506,26 +589,26 @@ article h1 { } .date-header { - color: var(--window-title-text); - background-color: var(--window-title-bg); - padding: 2px 10px; - font-size: 1rem; + font-size: 0.8rem; + color: var(--date-color); + margin-bottom: 0.5rem; + font-family: var(--font-headings); font-weight: 400; - margin: 2rem 0 1.5rem; - display: inline-block; - border: 2px solid var(--window-border); + text-transform: uppercase; + letter-spacing: 1px; } article .meta { - font-size: 0.85rem; + font-size: 0.8rem; color: var(--date-color); margin-bottom: 1.5rem; display: flex; - align-items: center; flex-wrap: wrap; - gap: 0.5rem; + gap: 1rem; + align-items: center; } +/* Reading time - TEXT BROWSER FALLBACK */ .reading-time { font-size: 0.8rem; display: inline-flex; @@ -535,6 +618,18 @@ article .meta { border: 1px solid var(--date-color); } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + article .summary { margin-bottom: 1.2rem; overflow-wrap: break-word; @@ -604,6 +699,13 @@ article .tags a:hover, article .tags .tag:hover, article .tags a.tag:hover { transform: translateY(-1px); } +article .tags a:focus, article .tags .tag:focus, article .tags a.tag:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); + transform: translateY(-1px); +} + a { color: var(--link-color); text-decoration: none; @@ -615,6 +717,12 @@ a:hover { color: var(--link-hover-color); } +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + p { margin: 0 0 1.5rem; } @@ -660,6 +768,12 @@ footer a:hover { color: var(--link-hover-color); } +footer a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + blockquote { border-left: 4px solid var(--link-color); padding: 0.5rem 1rem; @@ -726,6 +840,12 @@ hr { background-color: var(--link-hover-color); } +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -751,6 +871,12 @@ hr { background-color: var(--link-hover-color); } +.tags-list a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); +} + .tag-count { background-color: var(--accent-secondary); color: var(--text-color); @@ -764,7 +890,7 @@ hr { margin-bottom: 20px; } -.posts-list h2, .posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } @@ -850,6 +976,12 @@ hr { background-color: var(--link-hover-color); } +.archives-nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); +} + /* Responsive styles */ @media (max-width: 768px) { html { diff --git a/themes/apple2/style.css b/themes/apple2/style.css index 48c056f..09e2030 100644 --- a/themes/apple2/style.css +++ b/themes/apple2/style.css @@ -1,8 +1,26 @@ /* * Apple II Theme for BSSG * Green monochrome text terminal style from the Apple II era + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for gradient text and decorative elements + * - Optimized font loading with better fallbacks + * - Enhanced performance while maintaining authentic Apple II look */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Apple II color scheme */ --bg-color: #000000; @@ -14,13 +32,14 @@ --title-color: #88ff88; --title-highlight: #ccffcc; - /* Typography */ - --font-main: 'Courier New', monospace; - --font-headings: 'Courier New', monospace; - --font-mono: 'Courier New', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-headings: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-mono: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; /* Sizing */ --content-width: 800px; + --transition: 0.2s ease; } /* Apple II screen effect */ @@ -34,6 +53,7 @@ body { text-shadow: 0 0 5px var(--text-color); } +/* OPTIMIZED scanline effect with progressive enhancement */ body::before { content: ""; position: fixed; @@ -47,6 +67,13 @@ body::before { pointer-events: none; } +/* TEXT BROWSER FALLBACK: Disable complex background when not supported */ +@supports not (background-clip: text) { + body::before { + display: none; + } +} + /* Container with simulated terminal screen */ .container { max-width: var(--content-width); @@ -81,6 +108,20 @@ header { text-shadow: 0 0 8px var(--title-color); display: inline-block; position: relative; + transition: all var(--transition); +} + +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title a { + color: var(--title-color); + background: none; + } + + .site-title a:hover { + color: var(--title-highlight); + background: none; + } } .site-title a:hover { @@ -92,6 +133,11 @@ header { text-shadow: 0 0 15px var(--title-highlight); } +.site-title a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; +} + .site-title a::after { content: "_"; display: inline-block; @@ -125,6 +171,7 @@ nav a { border: 1px solid var(--text-color); position: relative; display: inline-block; + transition: all var(--transition); } nav a::before { @@ -143,6 +190,11 @@ nav a:hover, nav a:focus { text-shadow: none; } +nav a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; +} + /* Selected menu item */ nav a.active { background-color: var(--text-color); @@ -173,6 +225,7 @@ h2 { font-size: 1.3rem; } +.posts-list h2, h3 { font-size: 1.2rem; } @@ -200,7 +253,8 @@ p { /* Links */ a { color: var(--link-color); - text-decoration: none; + text-decoration: underline; + transition: color var(--transition); } a:visited { @@ -208,30 +262,40 @@ a:visited { } a:hover { - text-decoration: underline; + color: var(--title-highlight); + text-shadow: 0 0 5px var(--title-highlight); +} + +a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; + color: var(--title-highlight); } -/* Apple II style prompt for articles */ article { margin-bottom: 30px; - padding-bottom: 20px; - border-bottom: 1px dotted var(--text-color); + padding: 15px; + border: 1px dashed var(--text-color); position: relative; } article::before { - content: ">"; - position: absolute; - left: -20px; + content: "***** ARTICLE START *****"; + display: block; + text-align: center; + margin-bottom: 15px; + font-family: var(--font-mono); + text-transform: uppercase; } article:last-child { - border-bottom: none; + margin-bottom: 0; } article .meta { font-size: 0.9rem; - margin-bottom: 15px; + margin-bottom: 10px; + color: var(--text-color); display: flex; flex-wrap: wrap; gap: 15px; @@ -409,10 +473,18 @@ article .meta { .tags a { color: var(--text-color); text-decoration: none; + transition: color var(--transition); } .tags a:hover { text-decoration: underline; + color: var(--title-highlight); +} + +.tags a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; + color: var(--title-highlight); } code { @@ -440,77 +512,92 @@ img { border: 1px solid var(--text-color); } -/* Footer with prompt style */ footer { + margin-top: 30px; + padding: 15px; border-top: 1px solid var(--text-color); - margin-top: 40px; - padding-top: 20px; + text-align: center; font-size: 0.9rem; - display: flex; - justify-content: space-between; } footer::before { - content: "READY."; + content: "***** SYSTEM STATUS: READY *****"; display: block; margin-bottom: 10px; -} - -/* Apple II command prompt pagination */ -.pagination { - display: flex; - justify-content: center; - align-items: center; - margin: 30px 0; - gap: 20px; -} - -.pagination a { - color: var(--link-color); - text-decoration: none; + font-family: var(--font-mono); text-transform: uppercase; } -.pagination a::before { - content: "CMD:"; - margin-right: 5px; +footer a { + color: var(--link-color); + transition: color var(--transition); } -.pagination a:hover { - text-decoration: underline; +footer a:hover { + color: var(--title-highlight); +} + +footer a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; + color: var(--title-highlight); +} + +.pagination { + display: flex; + justify-content: space-between; + margin-top: 30px; + padding: 15px; + border: 1px dashed var(--text-color); +} + +.pagination a { + color: var(--text-color); + text-decoration: none; + padding: 5px 10px; + border: 1px solid var(--text-color); + transition: all var(--transition); +} + +.pagination a::before { + content: "> "; +} + +.pagination a:hover, .pagination a:focus { + background-color: var(--text-color); + color: var(--bg-color); +} + +.pagination a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; } .pagination .page-info { - margin: 0 10px; + color: var(--text-color); + font-family: var(--font-mono); } -/* Media query for responsive design */ @media (max-width: 768px) { .container { margin: 10px; - width: auto; padding: 15px; + max-width: none; } nav { flex-direction: column; - align-items: center; } nav a { - margin: 5px 0; - width: auto; - min-width: 60%; + margin-bottom: 5px; + padding: 8px 12px; text-align: center; - box-sizing: border-box; - padding: 5px 20px; } article::before { - left: 0; - position: relative; - display: inline-block; - margin-right: 10px; + font-size: 0.8rem; + margin-bottom: 10px; } .featured-image, @@ -522,26 +609,26 @@ footer::before { } article { - padding-left: 5px; - padding-right: 5px; + padding: 10px; + margin-bottom: 20px; } footer { - flex-direction: column; - gap: 10px; - align-items: center; - text-align: center; + padding: 10px; + font-size: 0.8rem; } .pagination { flex-direction: column; - gap: 15px; + gap: 10px; + text-align: center; } } @media (max-width: 480px) { body { padding: 10px; + font-size: 0.9rem; } .container { @@ -553,11 +640,12 @@ footer::before { } h2 { - font-size: 1.1rem; + font-size: 1.2rem; } pre { - padding: 5px; + padding: 8px; + font-size: 0.8rem; } .featured-image::before, @@ -567,27 +655,22 @@ footer::before { .featured-image::after, .index-image::after, .tag-image::after, - .archive-image::after { - font-size: 0.8rem; + .archive-image::after, + article::before, + footer::before { + font-size: 0.7rem; } } -/* Apple II style hr element - replaces default grey bar with themed separator */ hr { border: none; - border-top: 1px dotted var(--text-color); - height: 1px; + border-top: 1px dashed var(--text-color); margin: 20px 0; - background-color: transparent; + height: 1px; } -/* Date header styling for index page */ .date-header { color: var(--text-color); - font-size: 1.2rem; - margin-top: 1.5rem; - margin-bottom: 1rem; - font-weight: normal; - border-bottom: 1px dotted var(--text-color); - padding-bottom: 0.5rem; + font-family: var(--font-mono); + text-transform: uppercase; } \ No newline at end of file diff --git a/themes/art-deco/style.css b/themes/art-deco/style.css index f1ef1ee..e1838d1 100644 --- a/themes/art-deco/style.css +++ b/themes/art-deco/style.css @@ -2,8 +2,25 @@ * Art Deco Theme for BSSG * Inspired by 1920s-30s Art Deco style with geometric patterns, * elegant fonts, and gold/black/silver/jewel color palettes + * IMPROVED: Better accessibility, performance, and text browser support */ +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Simplify decorative elements for reduced motion */ + body, header { + background-image: none !important; + background-attachment: scroll !important; + } +} + :root { /* Art Deco color scheme */ --gold: #D4AF37; @@ -34,17 +51,17 @@ --footer-text: var(--silver); --border-color: var(--gold); - /* Typography */ - --font-main: 'Georgia', 'Times New Roman', serif; - --font-headings: 'Copperplate', 'Copperplate Gothic Light', 'Garamond', serif; - --font-mono: 'Courier New', monospace; + /* Typography - IMPROVED fallbacks for text browsers */ + --font-main: Georgia, 'Times New Roman', Times, serif; + --font-headings: 'Copperplate Gothic Light', 'Copperplate', Garamond, Georgia, 'Times New Roman', serif; + --font-mono: 'Courier New', Courier, monospace; /* Sizing */ --content-width: 900px; --content-padding: 2rem; } -/* Base elements */ +/* Base elements - OPTIMIZED performance */ html { font-size: 16px; } @@ -56,15 +73,18 @@ body { margin: 0; padding: 0; line-height: 1.6; + /* OPTIMIZED: Simplified pattern for better performance */ background-image: linear-gradient(45deg, var(--gold) 25%, transparent 25%), linear-gradient(-45deg, var(--gold) 25%, transparent 25%); background-size: 20px 20px; background-position: 0 0, 10px 10px; - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ background-repeat: repeat; background-color: var(--cream); - background-blend-mode: soft-light; + /* OPTIMIZED: Reduced blend mode complexity */ + background-blend-mode: multiply; + opacity: 0.95; } .container { @@ -76,7 +96,7 @@ body { position: relative; } -/* Art Deco border pattern */ +/* Art Deco border pattern - OPTIMIZED */ .container::before { content: ""; position: absolute; @@ -109,25 +129,28 @@ body { ); } -/* Header */ +/* Header - OPTIMIZED performance */ header { background-color: var(--header-bg); color: var(--header-text); padding: 2rem var(--content-padding) 1rem; position: relative; text-align: center; + /* OPTIMIZED: Simplified background pattern */ background-image: linear-gradient(45deg, var(--gold) 25%, transparent 25%), linear-gradient(-45deg, var(--gold) 25%, transparent 25%); background-size: 10px 10px; background-position: 0 0, 5px 5px; - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ background-repeat: repeat; background-color: var(--black); + /* OPTIMIZED: Reduced blend mode complexity */ background-blend-mode: overlay; + opacity: 0.95; } -/* Decorative header element */ +/* Decorative header element - IMPROVED text browser support */ header::before { content: "★ ★ ★"; display: block; @@ -139,6 +162,13 @@ header::before { padding-left: 2rem; } +/* FALLBACK: Text browser support for decorative elements */ +@supports not (content: "★") { + header::before { + content: "* * *"; + } +} + header::after { content: ""; display: block; @@ -148,7 +178,7 @@ header::after { margin: 1rem auto 0; } -/* Site title */ +/* Site title - IMPROVED accessibility */ .site-title { font-family: var(--font-headings); font-weight: normal; @@ -164,6 +194,7 @@ header::after { text-decoration: none; display: inline-block; position: relative; + transition: all 0.2s ease; } .site-title a::before, @@ -176,6 +207,26 @@ header::after { vertical-align: middle; } +/* FALLBACK: Text browser support for decorative elements */ +@supports not (content: "◆") { + .site-title a::before, + .site-title a::after { + content: "*"; + } +} + +.site-title a:hover { + transform: translateY(-2px); + text-shadow: 0 2px 4px rgba(212, 175, 55, 0.3); +} + +.site-title a:focus { + outline: 2px solid var(--gold); + outline-offset: 3px; + transform: translateY(-2px); + text-shadow: 0 2px 4px rgba(212, 175, 55, 0.3); +} + /* Site description */ header p { margin: 0.5rem 0 0; @@ -187,7 +238,7 @@ header p { margin-right: auto; } -/* Navigation */ +/* Navigation - IMPROVED accessibility */ nav { background-color: var(--nav-bg); display: flex; @@ -224,13 +275,22 @@ nav a { letter-spacing: 2px; text-transform: uppercase; position: relative; - transition: all 0.3s ease; + transition: all 0.2s ease; /* Reduced transition time for better performance */ } nav a:hover, nav a.active { background-color: var(--nav-hover-bg); color: var(--nav-hover-text); + transform: translateY(-1px); +} + +nav a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + background-color: var(--nav-hover-bg); + color: var(--nav-hover-text); + transform: translateY(-1px); } /* Content area */ @@ -326,6 +386,13 @@ a:hover { border-bottom-color: var(--link-hover); } +a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + color: var(--link-hover); + border-bottom-color: var(--link-hover); +} + /* Articles */ article { margin-bottom: 4rem; @@ -429,6 +496,14 @@ article .meta { border: none; } +.tags a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + background-color: var(--black); + color: var(--gold); + border: none; +} + /* Tags list page */ .tags-list { list-style-type: none; @@ -464,6 +539,14 @@ article .meta { border: none; } +.tags-list a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + background-color: var(--black); + color: var(--gold); + border: none; +} + /* Footer */ footer { background-color: var(--footer-bg); @@ -507,6 +590,13 @@ footer a:hover { text-decoration: underline; } +footer a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + color: var(--silver); + text-decoration: underline; +} + /* Pagination */ .pagination { display: flex; @@ -536,6 +626,14 @@ footer a:hover { border: none; } +.pagination a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + background-color: var(--black); + color: var(--gold); + border: none; +} + /* Featured images */ .featured-image, .index-image, @@ -593,14 +691,26 @@ figcaption { margin-bottom: 3rem; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; text-align: center; } -/* Responsive styles */ +/* IMPROVED: Responsive styles with mobile optimizations */ @media (max-width: 768px) { + body { + /* OPTIMIZED: Remove complex background patterns on mobile */ + background-image: none; + background-color: var(--cream); + } + + header { + /* OPTIMIZED: Simplified header background on mobile */ + background-image: none; + background-color: var(--black); + } + .container { margin: 0; width: 100%; @@ -642,6 +752,8 @@ figcaption { padding: 0.8rem 1rem; font-size: 0.8rem; text-align: center; + /* OPTIMIZED: Simplified transitions on mobile */ + transition: background-color 0.2s ease; } article { @@ -659,6 +771,8 @@ figcaption { .tag-image, .archive-image { margin: 1.5rem 0; + /* OPTIMIZED: Simplified decorative elements on mobile */ + padding: 5px; } .pagination { @@ -675,6 +789,7 @@ figcaption { .site-title { font-size: 1.7rem; + letter-spacing: 2px; /* Reduced letter spacing for small screens */ } h1 { @@ -703,6 +818,8 @@ figcaption { width: 100%; box-sizing: border-box; border-bottom: 1px solid var(--gold); + /* OPTIMIZED: Remove transforms on small mobile */ + transform: none !important; } nav a:last-child { @@ -720,6 +837,14 @@ figcaption { font-size: 0.9rem; } + /* OPTIMIZED: Simplified decorative elements on very small screens */ + .featured-image, + .index-image, + .tag-image, + .archive-image { + padding: 2px; + } + footer { text-align: center; } diff --git a/themes/atarist/style.css b/themes/atarist/style.css index 3606b97..e6af47e 100644 --- a/themes/atarist/style.css +++ b/themes/atarist/style.css @@ -1,8 +1,26 @@ /* * Atari ST GEM Theme for BSSG * Recreating the Atari ST's Graphical Environment Manager interface from the 1980s + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for decorative elements + * - Optimized font loading with better fallbacks + * - Enhanced performance while maintaining authentic GEM look */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Atari ST GEM color scheme */ --bg-color: #c0c0c0; @@ -21,15 +39,16 @@ --title-gradient-start: #ffffff; --title-gradient-end: #aaaaff; - /* Typography */ - --font-main: 'Courier New', monospace; - --font-headings: 'Courier New', monospace; - --font-mono: 'Courier New', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-headings: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-mono: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; /* Sizing */ --content-width: 800px; --border-width: 1px; --border-radius: 0; + --transition: 0.2s ease; } /* Base elements */ @@ -83,14 +102,32 @@ header { justify-content: center; font-size: 10px; cursor: pointer; + transition: background-color var(--transition); } +.gem-control:hover { + background-color: var(--dropdown-bg); +} + +.gem-control:focus { + outline: 2px solid var(--title-text); + outline-offset: 2px; +} + +/* TEXT BROWSER FALLBACK: Provide text alternatives for window controls */ .gem-close::after { content: "×"; color: black; font-weight: bold; } +/* TEXT BROWSER FALLBACK: Use "X" when × isn't supported */ +@supports not (content: "×") { + .gem-close::after { + content: "X"; + } +} + .gem-minimize::after { content: "_"; color: black; @@ -103,6 +140,13 @@ header { color: black; } +/* TEXT BROWSER FALLBACK: Use "[]" when □ isn't supported */ +@supports not (content: "□") { + .gem-maximize::after { + content: "[]"; + } +} + header h1 { margin: 0; padding: 0; @@ -149,6 +193,7 @@ nav a { display: inline-block; border-right: var(--border-width) solid var(--border-color); white-space: nowrap; + transition: all var(--transition); } nav a:first-child { @@ -164,6 +209,11 @@ nav a:hover, nav a:focus { color: var(--menu-text); } +nav a:focus { + outline: 2px solid var(--title-text); + outline-offset: 2px; +} + /* Selected menu item */ nav a.active { background-color: var(--menu-highlight); @@ -194,25 +244,26 @@ h2 { font-size: 1.4rem; } +.posts-list h2, h3 { font-size: 1.2rem; } article h1 { font-size: 1.4rem; - border-bottom: var(--border-width) solid var(--border-color); - padding-bottom: 5px; + margin-top: 0; } p { margin-bottom: 1rem; - line-height: 1.4; + line-height: 1.5; } /* Links */ a { color: var(--link-color); - text-decoration: none; + text-decoration: underline; + transition: color var(--transition); } a:visited { @@ -220,19 +271,24 @@ a:visited { } a:hover { - text-decoration: underline; + color: var(--menu-highlight); } -/* GEM style button */ +a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; + color: var(--menu-highlight); +} + +/* GEM-style button */ .gem-button { background-color: var(--dropdown-bg); + color: var(--text-color); border: var(--border-width) solid var(--border-color); padding: 3px 10px; - color: var(--text-color); - font-family: var(--font-main); font-size: 0.9rem; cursor: pointer; - display: inline-block; + transition: background-color var(--transition); } .gem-button:active { @@ -240,21 +296,24 @@ a:hover { color: var(--menu-text); } -/* Articles with borders */ +.gem-button:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; +} + article { margin-bottom: 20px; - padding: 10px; - border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 15px; + border-bottom: var(--border-width) solid #e0e0e0; } article:last-child { - margin-bottom: 0; + border-bottom: none; } article .meta { - font-size: 0.85rem; - color: #333; + font-size: 0.9rem; + color: #666; margin-bottom: 10px; display: flex; flex-wrap: wrap; @@ -265,6 +324,18 @@ article .meta { font-style: italic; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .tags { display: flex; flex-wrap: wrap; @@ -278,6 +349,7 @@ article .meta { font-size: 0.75rem; border: var(--border-width) solid var(--border-color); text-decoration: none; + transition: all var(--transition); } .tags a:hover { @@ -285,6 +357,13 @@ article .meta { color: var(--menu-text); } +.tags a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; + background-color: var(--menu-highlight); + color: var(--menu-text); +} + .tags-list { list-style-type: none; padding: 0; @@ -293,6 +372,20 @@ article .meta { gap: 10px; } +.tags-list a { + transition: all var(--transition); +} + +.tags-list a:hover { + background-color: var(--menu-highlight); + color: var(--menu-text); +} + +.tags-list a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; +} + .tag-count { color: #666; font-size: 0.85em; @@ -341,6 +434,21 @@ footer { align-items: center; } +footer a { + color: var(--link-color); + transition: color var(--transition); +} + +footer a:hover { + color: var(--menu-highlight); +} + +footer a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; + color: var(--menu-highlight); +} + /* GEM style scrollbar (simulated with CSS) */ .gem-scrollbar { width: 15px; @@ -391,6 +499,7 @@ footer { text-decoration: none; border: var(--border-width) solid var(--border-color); font-size: 0.9rem; + transition: all var(--transition); } .pagination a:hover { @@ -398,6 +507,13 @@ footer { color: var(--menu-text); } +.pagination a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; + background-color: var(--menu-highlight); + color: var(--menu-text); +} + .pagination .page-info { margin: 0 10px; font-size: 0.9rem; @@ -458,7 +574,8 @@ footer { @media (max-width: 480px) { body { - padding: 8px; + padding: 10px; + font-size: 13px; } .container { @@ -470,13 +587,14 @@ footer { } h1 { - font-size: 1.3rem; + font-size: 1.4rem; } h2 { font-size: 1.2rem; } + .posts-list h2, h3 { font-size: 1.1rem; } @@ -486,185 +604,200 @@ footer { .tag-image::before, .archive-image::before { font-size: 0.8rem; - padding: 2px 5px; } article .meta { flex-direction: column; - align-items: flex-start; - gap: 8px; + gap: 5px; } .site-title { - font-size: 0.9rem; + font-size: 1rem; } article h1 { font-size: 1.2rem; + line-height: 1.3; } } -/* Site title with GEM-style gradient */ .site-title { - margin: 0; - padding: 0; font-size: 1rem; + margin: 0; font-weight: bold; - font-family: var(--font-headings); - /* Gradient that matches Atari ST GEM aesthetic */ - background: linear-gradient(90deg, var(--title-gradient-start), var(--title-gradient-end)); + color: var(--title-text); + /* GEM-style gradient */ + background: linear-gradient(to bottom, var(--title-gradient-start) 0%, var(--title-gradient-end) 100%); -webkit-background-clip: text; background-clip: text; color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); - display: inline-block; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3); +} + +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title { + color: var(--title-text); + background: none; + } + + .site-title a { + color: var(--title-text); + background: none; + } + + .site-title a:hover { + color: var(--title-gradient-end); + background: none; + } } .site-title a { text-decoration: none; - background: linear-gradient(90deg, var(--title-gradient-start), var(--title-gradient-end)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); - transition: all 0.2s ease; - display: inline-block; + color: inherit; + transition: all var(--transition); } .site-title a:hover { - text-decoration: none; - background: linear-gradient(90deg, var(--title-gradient-end), var(--title-gradient-start)); + background: linear-gradient(to bottom, var(--title-gradient-end) 0%, var(--title-gradient-start) 100%); -webkit-background-clip: text; background-clip: text; color: transparent; } -/* Featured image styles for Atari ST GEM */ +.site-title a:focus { + outline: 2px solid var(--title-text); + outline-offset: 2px; +} + +/* Featured image styles for GEM */ .featured-image { - margin: 15px 0; + display: block; + margin: 20px auto; border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 10px; + background-color: var(--dropdown-bg); position: relative; - box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); - padding-top: 20px; /* Space for the title bar */ } .featured-image::before { - content: "IMAGE VIEWER"; - position: absolute; - top: 0; - left: 0; - right: 0; - background-color: var(--title-bar); - color: var(--title-text); - font-size: 0.9rem; - padding: 2px 10px; - font-weight: bold; + content: "*** FEATURED IMAGE ***"; + display: block; text-align: center; + margin-bottom: 10px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text-color); + background-color: var(--window-bg); + padding: 2px; + border: var(--border-width) solid var(--border-color); } .featured-image img { display: block; - width: 100%; + margin: 0 auto; + max-width: 100%; height: auto; - margin: 0; - padding: 0; - border: none; - box-shadow: none; + border: var(--border-width) solid var(--border-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); } .featured-image .image-caption { - padding: 5px 10px; - background-color: var(--dropdown-bg); - border-top: var(--border-width) solid var(--border-color); + text-align: center; + margin-top: 10px; font-size: 0.9rem; + font-style: italic; } .index-image { - margin: 15px 0; + display: block; + margin: 20px auto; border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 10px; + background-color: var(--dropdown-bg); position: relative; - box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); - padding-top: 20px; /* Space for the title bar */ } .index-image::before { - content: "INDEX IMAGE"; - position: absolute; - top: 0; - left: 0; - right: 0; - background-color: var(--title-bar); - color: var(--title-text); - font-size: 0.9rem; - padding: 2px 10px; - font-weight: bold; + content: "*** INDEX IMAGE ***"; + display: block; text-align: center; + margin-bottom: 10px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text-color); + background-color: var(--window-bg); + padding: 2px; + border: var(--border-width) solid var(--border-color); } .index-image img { display: block; - width: 100%; + margin: 0 auto; + max-width: 100%; height: auto; - margin: 0; - padding: 0; - border: none; - box-shadow: none; + border: var(--border-width) solid var(--border-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); } .tag-image { - margin: 15px 0; + display: block; + margin: 20px auto; border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 10px; + background-color: var(--dropdown-bg); position: relative; - box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); - padding-top: 20px; /* Space for the title bar */ } .tag-image::before { - content: "TAG IMAGE"; - position: absolute; - top: 0; - left: 0; - right: 0; - background-color: var(--title-bar); - color: var(--title-text); - font-size: 0.9rem; - padding: 2px 10px; - font-weight: bold; + content: "*** TAG IMAGE ***"; + display: block; text-align: center; + margin-bottom: 10px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text-color); + background-color: var(--window-bg); + padding: 2px; + border: var(--border-width) solid var(--border-color); } .tag-image img { display: block; - width: 100%; + margin: 0 auto; + max-width: 100%; height: auto; - margin: 0; - padding: 0; - border: none; - box-shadow: none; + border: var(--border-width) solid var(--border-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); } .archive-image { - margin: 15px 0; + display: block; + margin: 20px auto; border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 10px; + background-color: var(--dropdown-bg); position: relative; - box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); - padding-top: 20px; /* Space for the title bar */ } .archive-image::before { - content: "ARCHIVE IMAGE"; - position: absolute; - top: 0; - left: 0; - right: 0; - background-color: var(--title-bar); - color: var(--title-text); - font-size: 0.9rem; - padding: 2px 10px; - font-weight: bold; + content: "*** ARCHIVE IMAGE ***"; + display: block; text-align: center; + margin-bottom: 10px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text-color); + background-color: var(--window-bg); + padding: 2px; + border: var(--border-width) solid var(--border-color); +} + +.archive-image img { + display: block; + margin: 0 auto; + max-width: 100%; + height: auto; + border: var(--border-width) solid var(--border-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); } \ No newline at end of file diff --git a/themes/bauhaus/style.css b/themes/bauhaus/style.css index 1d0dc01..7b29f97 100644 --- a/themes/bauhaus/style.css +++ b/themes/bauhaus/style.css @@ -2,8 +2,24 @@ * Bauhaus Theme for BSSG * Inspired by the Bauhaus design school, emphasizing functionality, * primary colors, geometric shapes, and clean sans-serif typography + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for geometric reading time indicator + * - Optimized performance while maintaining Bauhaus design principles */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + :root { /* Bauhaus color scheme */ --red: #E53935; @@ -102,6 +118,12 @@ header { text-decoration: none; } +.site-title a:focus { + outline: 2px solid var(--tertiary-accent); + outline-offset: 2px; + text-decoration: none; +} + /* Site description */ header p { margin: 0.5rem 0 0 0; @@ -137,6 +159,13 @@ nav a.active { color: var(--nav-hover-text); } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--nav-hover-bg); + color: var(--nav-hover-text); +} + /* Content area */ main { padding: var(--content-padding); @@ -210,6 +239,12 @@ a:hover { border-bottom: 2px solid currentColor; } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-bottom: 2px solid currentColor; +} + /* Articles */ article { margin-bottom: 4rem; @@ -240,16 +275,27 @@ article .meta { padding-left: 1.5rem; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: ""; + content: "Time: "; position: absolute; left: 0; top: 50%; transform: translateY(-50%); + font-style: normal; + font-weight: bold; +} + +/* Modern browsers: Use geometric shape when CSS transforms are supported */ +@supports (transform: translateY(-50%)) and (border-radius: 50%) { + .reading-time::before { + content: ""; width: 1rem; height: 1rem; background-color: var(--tertiary-accent); border-radius: 50%; + font-weight: normal; + } } /* Tags */ @@ -276,6 +322,14 @@ article .meta { border: none; } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--secondary-accent); + color: var(--white); + border: none; +} + /* Tags list page */ .tags-list { list-style-type: none; @@ -307,6 +361,13 @@ article .meta { color: var(--white); } +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--secondary-accent); + color: var(--white); +} + /* Footer */ footer { background-color: var(--footer-bg); @@ -340,6 +401,12 @@ footer a:hover { text-decoration: underline; } +footer a:focus { + outline: 2px solid var(--tertiary-accent); + outline-offset: 2px; + text-decoration: underline; +} + /* Pagination */ .pagination { display: flex; @@ -367,6 +434,14 @@ footer a:hover { border: none; } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--secondary-accent); + color: var(--white); + border: none; +} + /* Featured images */ .featured-image, .index-image, @@ -411,7 +486,7 @@ footer a:hover { border-bottom: none; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; } diff --git a/themes/bbs/style.css b/themes/bbs/style.css index f4389c3..39925d2 100644 --- a/themes/bbs/style.css +++ b/themes/bbs/style.css @@ -2,9 +2,25 @@ * BBS Theme for BSSG * Inspired by old-school Bulletin Board Systems * Features ANSI colors, ASCII art aesthetics, and terminal feel + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Added text browser fallbacks for gradient text + * - Enhanced font fallbacks with comprehensive system monospace font stacks + * - Optimized performance while maintaining authentic BBS terminal aesthetics */ -@import url('https://fonts.googleapis.com/css2?family=VT323&family=IBM+Plex+Mono:wght@400;700&display=swap'); +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} :root { /* ANSI color scheme */ @@ -24,12 +40,13 @@ --title-color-start: #ffff55; /* ANSI yellow */ --title-color-end: #ff5555; /* ANSI red */ - /* Typography */ - --font-main: 'VT323', monospace; - --font-mono: 'IBM Plex Mono', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-mono: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; /* Sizing */ --content-width: 900px; + --transition: 0.3s ease; } /* Base elements with terminal feel */ @@ -112,6 +129,24 @@ header h1 { position: relative; } +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title { + color: var(--title-color-start); + background: none; + } + + .site-title a { + color: var(--title-color-start); + background: none; + } + + .site-title a:hover { + color: var(--title-color-end); + background: none; + } +} + .site-title a { text-decoration: none; /* Replicate the gradient for links */ @@ -119,7 +154,7 @@ header h1 { -webkit-background-clip: text; background-clip: text; color: transparent; - transition: all 0.3s; + transition: all var(--transition); display: inline-block; } @@ -140,6 +175,24 @@ header h1 { color: var(--title-color-start); } +/* Focus styles for site title */ +.site-title a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + background: linear-gradient(90deg, var(--title-color-end) 0%, var(--title-color-start) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.site-title a:focus::after { + content: "_"; + position: relative; + display: inline-block; + animation: blink 1s step-end infinite; + color: var(--title-color-start); +} + /* BBS menu navigation */ nav { background-color: var(--nav-bg); @@ -160,6 +213,7 @@ nav a { position: relative; display: inline-block; margin-right: 2px; + transition: all var(--transition); } /* BBS-style menu item with brackets and number */ @@ -175,7 +229,7 @@ nav a::after { margin-left: 3px; } -/* Number the menu items like in a BBS */ +/* Numbered menu items */ nav a:nth-child(1)::before { content: "[1-"; } nav a:nth-child(2)::before { content: "[2-"; } nav a:nth-child(3)::before { content: "[3-"; } @@ -187,137 +241,145 @@ nav a:nth-child(8)::before { content: "[8-"; } nav a:nth-child(9)::before { content: "[9-"; } nav a:hover { - color: var(--accent-1); - background-color: var(--bg-color); + background-color: var(--accent-1); + color: var(--bright-text); + text-decoration: none; +} + +nav a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + background-color: var(--accent-1); + color: var(--bright-text); + text-decoration: none; } -/* Active navigation item */ nav a.active { - color: var(--accent-1); - background-color: var(--bg-color); + background-color: var(--accent-1); + color: var(--bright-text); } -/* Special RSS button that looks like a BBS command */ nav a:last-child { - color: var(--header-fg); + margin-right: 0; } nav a:last-child::before { - content: "[R-"; + content: "[X-"; + color: var(--accent-1); } -/* Main content with scanlines */ +/* Main content area */ main { padding: 20px; - position: relative; background-color: var(--bg-color); + min-height: 400px; + position: relative; + border-bottom: 2px dashed var(--border-color); +} + +/* BBS prompt line */ +main::before { + content: "C:\\BSSG> TYPE CONTENT.TXT"; + display: block; + color: var(--accent-1); font-family: var(--font-mono); font-size: 16px; - color: var(--text-color); + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px dashed var(--dim-text); } -/* Scanline effect */ -main::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: repeating-linear-gradient( - 0deg, - rgba(0, 0, 0, 0.15), - rgba(0, 0, 0, 0.15) 1px, - transparent 1px, - transparent 2px - ); - pointer-events: none; - z-index: 1; -} - -/* Terminal typography */ +/* Typography */ h1, h2, h3, h4, h5, h6 { - color: var(--header-fg); - margin-top: 1.5rem; - margin-bottom: 1rem; - font-weight: normal; font-family: var(--font-main); + color: var(--bright-text); + margin: 1.5rem 0 1rem 0; + font-weight: bold; + text-transform: uppercase; } h1 { font-size: 2rem; - text-transform: uppercase; + color: var(--header-fg); } h2 { - font-size: 1.7rem; + font-size: 1.6rem; color: var(--accent-1); - border-bottom: 1px dashed var(--accent-1); - padding-bottom: 5px; + position: relative; + padding-left: 20px; } h2::before { - content: "## "; + content: ">> "; + color: var(--accent-1); } +.posts-list h2, h3 { - font-size: 1.4rem; - color: var(--link-color); + font-size: 1.3rem; + color: var(--link-hover); } +.posts-list h2::before, h3::before { - content: ">>> "; + content: "> "; + color: var(--accent-1); } p { - margin-bottom: 1rem; + margin-bottom: 1.2rem; + line-height: 1.5; } -/* BBS-style links */ +/* Links */ a { color: var(--link-color); - text-decoration: none; - transition: all 0.2s; + text-decoration: underline; + transition: color var(--transition); } a:hover { color: var(--link-hover); - text-decoration: underline; + text-decoration: none; } -/* ASCII box for articles */ +a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: none; +} + +/* BBS-style articles */ article { - margin-bottom: 40px; - background-color: rgba(0, 0, 0, 0.5); - border: 1px solid var(--border-color); + margin-bottom: 30px; padding: 15px; + border: 1px dashed var(--border-color); position: relative; + background-color: rgba(0, 0, 0, 0.3); } -/* ASCII box top border */ +/* Article header with BBS styling */ article::before { - content: "+-[ MESSAGE ]---------------------------------------------------+"; + content: "+==[ ARTICLE START ]=======================================================+"; display: block; - color: var(--bright-text); + color: var(--accent-1); font-family: var(--font-mono); - font-size: 16px; - margin: -15px -15px 15px -15px; - padding: 5px 15px; - background-color: var(--header-bg); - border-bottom: 1px solid var(--border-color); + font-size: 14px; + margin-bottom: 15px; + margin-top: -5px; } -/* ASCII box bottom border */ +/* Article footer */ article::after { - content: "+---------------------------------------------------------------------+"; + content: "+==[ ARTICLE END ]=========================================================+"; display: block; - color: var(--bright-text); + color: var(--accent-1); font-family: var(--font-mono); - font-size: 16px; - margin: 15px -15px -15px -15px; - padding: 5px 15px; - background-color: var(--header-bg); - border-top: 1px solid var(--border-color); + font-size: 14px; + margin-top: 15px; + margin-bottom: -5px; } article:last-child { @@ -325,23 +387,21 @@ article:last-child { } article h1 { - font-size: 1.5rem; + font-size: 1.8rem; margin-top: 0; - margin-bottom: 10px; - color: var(--accent-1); - font-weight: bold; + margin-bottom: 15px; + color: var(--header-fg); + border-bottom: 1px dashed var(--accent-1); + padding-bottom: 5px; } -/* BBS-style metadata with ANSI colors */ article .meta { font-size: 0.9rem; color: var(--dim-text); - margin-bottom: 20px; + margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 15px; - padding-bottom: 10px; - border-bottom: 1px dashed var(--dim-text); font-family: var(--font-mono); } @@ -376,6 +436,7 @@ article .meta { text-decoration: none; font-family: var(--font-mono); font-size: 0.9em; + transition: color var(--transition); } .tags a:hover { @@ -383,6 +444,13 @@ article .meta { text-decoration: underline; } +.tags a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + .tags-list { list-style-type: none; padding: 0; @@ -404,6 +472,20 @@ article .meta { color: var(--accent-1); } +.tags-list a { + transition: color var(--transition); +} + +.tags-list a:hover { + color: var(--link-hover); +} + +.tags-list a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); +} + .tag-count { color: var(--accent-2); font-size: 0.9em; @@ -549,11 +631,11 @@ img { /* ASCII art top border */ .tag-image::before { - content: "+==[ TAG IMAGE ]============================================================+"; + content: "+==[ TAG IMAGE ]=============================================================+"; display: block; font-family: var(--font-mono); font-size: 14px; - color: var(--link-color); + color: var(--accent-1); margin-bottom: 10px; white-space: nowrap; overflow: hidden; @@ -565,7 +647,7 @@ img { display: block; font-family: var(--font-mono); font-size: 14px; - color: var(--link-color); + color: var(--accent-1); margin-top: 10px; white-space: nowrap; overflow: hidden; @@ -590,11 +672,11 @@ img { /* ASCII art top border */ .archive-image::before { - content: "+==[ ARCHIVE IMAGE ]========================================================+"; + content: "+==[ ARCHIVE IMAGE ]=========================================================+"; display: block; font-family: var(--font-mono); font-size: 14px; - color: var(--accent-2); + color: var(--accent-1); margin-bottom: 10px; white-space: nowrap; overflow: hidden; @@ -606,7 +688,7 @@ img { display: block; font-family: var(--font-mono); font-size: 14px; - color: var(--accent-2); + color: var(--accent-1); margin-top: 10px; white-space: nowrap; overflow: hidden; @@ -621,32 +703,29 @@ img { margin: 0; } -/* BBS-style footer */ +/* BBS footer */ footer { background-color: var(--header-bg); - color: var(--dim-text); + color: var(--header-fg); padding: 15px; - font-size: 0.9rem; text-align: center; border-top: 2px solid var(--border-color); font-family: var(--font-mono); - position: relative; } -/* ASCII art border for footer */ +/* ASCII art footer border */ footer::before { - content: "+------------------------------[ END OF TRANSMISSION ]------------------------------+"; + content: "+==[ SYSTEM STATUS: ONLINE ]=======[ USERS: 1337 ]=======[ UPTIME: 24:7 ]==+"; display: block; color: var(--bright-text); - font-family: var(--font-mono); - font-size: 16px; + font-size: 14px; margin-bottom: 10px; - margin-top: -5px; } footer a { color: var(--link-color); text-decoration: none; + transition: color var(--transition); } footer a:hover { @@ -654,40 +733,56 @@ footer a:hover { text-decoration: underline; } -/* BBS-style pagination */ +footer a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + +/* BBS pagination */ .pagination { display: flex; justify-content: center; - align-items: center; + gap: 10px; margin: 30px 0; - gap: 15px; - font-family: var(--font-main); + font-family: var(--font-mono); } .pagination a { color: var(--link-color); - padding: 5px 10px; text-decoration: none; - border: 1px solid var(--border-color); - background-color: var(--bg-color); - font-weight: bold; + padding: 5px 15px; + border: 1px dashed var(--border-color); + background-color: rgba(0, 0, 0, 0.5); + transition: all var(--transition); } .pagination a:hover { - background-color: var(--header-bg); - color: var(--link-hover); + background-color: var(--accent-1); + color: var(--bright-text); + border-color: var(--accent-1); +} + +.pagination a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + background-color: var(--accent-1); + color: var(--bright-text); + border-color: var(--accent-1); } .pagination .page-info { - color: var(--bright-text); + color: var(--dim-text); + padding: 5px 15px; font-family: var(--font-mono); } -/* Terminal cursor blinking effect */ +/* BBS cursor */ .cursor { display: inline-block; - width: 10px; - height: 18px; + width: 0.6em; + height: 1em; background-color: var(--text-color); animation: blink 1s step-end infinite; vertical-align: text-bottom; @@ -699,43 +794,45 @@ footer a:hover { 50% { opacity: 0; } } -/* Horizontal rule as a BBS divider */ +/* BBS horizontal rule */ hr { border: none; height: 1px; - background-color: var(--dim-text); - margin: 20px 0; + background-color: var(--border-color); + margin: 30px 0; position: relative; } hr::before { - content: "---===[ * ]===---"; + content: "+============================================================================+"; position: absolute; - top: -10px; + top: -8px; left: 50%; transform: translateX(-50%); background-color: var(--bg-color); - padding: 0 15px; color: var(--accent-1); + font-family: var(--font-mono); + font-size: 14px; + padding: 0 10px; } -/* BBS Command prompt */ +/* BBS prompt styling */ .bbs-prompt { - padding: 10px; - margin: 20px 0; - border: 1px solid var(--dim-text); - background-color: rgba(0, 0, 0, 0.5); + color: var(--accent-1); font-family: var(--font-mono); - color: var(--bright-text); + font-weight: bold; + margin: 20px 0; + padding: 10px; + border: 1px dashed var(--border-color); + background-color: rgba(0, 0, 0, 0.5); } .bbs-prompt::before { - content: "BSSG>"; - color: var(--accent-1); - margin-right: 8px; + content: "C:\\BSSG> "; + color: var(--header-fg); } -/* ANSI color classes for text */ +/* ANSI color classes */ .ansi-red { color: var(--accent-1); } .ansi-green { color: var(--text-color); } .ansi-yellow { color: var(--header-fg); } @@ -744,55 +841,51 @@ hr::before { .ansi-cyan { color: var(--link-hover); } .ansi-white { color: var(--bright-text); } -/* Media query for responsive design */ +/* Responsive design */ @media (max-width: 768px) { header::before, header::after, + footer::before, article::before, article::after, - footer::before { + .featured-image::before, + .featured-image::after, + .index-image::before, + .index-image::after, + .tag-image::before, + .tag-image::after, + .archive-image::before, + .archive-image::after, + hr::before { font-size: 12px; - overflow: hidden; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; - max-width: 100%; - box-sizing: border-box; - content: "+-------------[ BSSG BBS ]-------------+"; - } - - header::after, - article::after, - footer::before { - content: "+---------------------------------------+"; } .container { - margin: 10px; - width: auto; + margin: 5px; + border-width: 1px; } header, footer, main { - padding: 15px; + padding: 10px; } nav { flex-direction: column; padding: 5px; - align-items: stretch; } nav a { - margin: 2px 0; - text-align: left; - padding: 8px 15px; - border-bottom: 1px dashed var(--dim-text); - box-sizing: border-box; - overflow: hidden; - text-overflow: ellipsis; + padding: 8px 10px; + margin-bottom: 2px; + text-align: center; + font-size: 0.9rem; } nav a:last-child { - border-bottom: none; + margin-bottom: 0; } article { @@ -800,22 +893,21 @@ hr::before { } pre { - max-width: 100%; - overflow-x: auto; - font-size: 14px; + padding: 10px; + font-size: 0.8rem; } .featured-image, .index-image, .tag-image, .archive-image { - margin: 10px 0; + margin: 15px 0; + padding: 8px; } article .meta { flex-direction: column; - align-items: flex-start; - gap: 8px; + gap: 5px; } .site-title { @@ -823,51 +915,55 @@ hr::before { } h1 { - font-size: 1.8rem; + font-size: 1.6rem; } h2 { - font-size: 1.5rem; + font-size: 1.4rem; } + .posts-list h2, h3 { - font-size: 1.3rem; + font-size: 1.2rem; } .pagination { flex-direction: column; + gap: 5px; align-items: center; - gap: 10px; } } @media (max-width: 480px) { body { - font-size: 16px; padding: 5px; + font-size: 16px; } .container { - margin: 5px; + margin: 2px; } header, footer, main { - padding: 10px; + padding: 8px; } pre, code { - font-size: 13px; + font-size: 0.7rem; } .featured-image::before, - .index-image::before, - .tag-image::before, - .archive-image::before, .featured-image::after, + .index-image::before, .index-image::after, + .tag-image::before, .tag-image::after, - .archive-image::after { - font-size: 11px; + .archive-image::before, + .archive-image::after, + header::before, + header::after, + footer::before { + font-size: 10px; } .site-title { @@ -875,23 +971,25 @@ hr::before { } h1 { - font-size: 1.5rem; + font-size: 1.4rem; } h2 { - font-size: 1.3rem; + font-size: 1.2rem; } + .posts-list h2, h3 { - font-size: 1.2rem; + font-size: 1.1rem; } .tags { flex-direction: column; - align-items: flex-start; + gap: 5px; } nav a { - padding: 6px 10px; + font-size: 0.8rem; + padding: 6px 8px; } -} \ No newline at end of file +} diff --git a/themes/beos/style.css b/themes/beos/style.css index 70a6ba6..edd9cd2 100644 --- a/themes/beos/style.css +++ b/themes/beos/style.css @@ -1,8 +1,32 @@ /* * Modern BeOS Theme for BSSG * A contemporary take on the classic BeOS interface + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for gradient text + * - Optimized font loading with better fallbacks + * - Enhanced performance while maintaining BeOS aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + nav a:hover, + .site-title a:hover, + article:hover { + transform: none !important; + } +} + :root { /* Modern BeOS-inspired color scheme */ --bg-color: #e8e8e8; @@ -21,10 +45,10 @@ --title-color-start: #3584e4; --title-color-end: #1d57a5; - /* Typography */ - --font-main: 'Inter', 'Segoe UI', 'Arial', sans-serif; - --font-headings: 'Inter', 'Segoe UI', 'Arial', sans-serif; - --font-mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-headings: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-mono: 'Consolas', 'Courier New', 'Courier', monospace; /* Sizing */ --content-width: 950px; @@ -99,6 +123,21 @@ header::after { transition: var(--transition); border-bottom: none; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + /* ACCESSIBILITY: Focus outline */ + outline: none; +} + +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title a { + color: var(--title-color-start); + background: none; + } + + .site-title a:hover { + color: var(--title-color-end); + background: none; + } } .site-title a:hover { @@ -110,6 +149,17 @@ header::after { border-bottom: none; } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + transform: translateY(-1px); + background: linear-gradient(to bottom, var(--title-color-end), var(--title-color-start)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + header h1 { margin: 0; padding: 0; @@ -153,6 +203,8 @@ nav a { display: inline-block; transition: var(--transition); top: 1px; + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a:hover { @@ -167,6 +219,12 @@ nav a:focus { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Content area with border to connect with tabs */ main { padding: 25px; @@ -194,6 +252,7 @@ h2 { font-size: 1.6rem; } +.posts-list h2, h3 { font-size: 1.4rem; } @@ -208,6 +267,8 @@ a { text-decoration: none; transition: var(--transition); border-bottom: 1px solid transparent; + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:visited { @@ -218,6 +279,13 @@ a:hover { border-bottom: 1px solid currentColor; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-bottom: 1px solid currentColor; +} + /* Articles in modern BeOS window style */ article { margin-bottom: 30px; @@ -274,6 +342,18 @@ article .meta { font-size: 13px; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags styling with proper spacing */ .tags { display: flex; @@ -292,6 +372,8 @@ article .meta { display: inline-block; border-radius: 4px; transition: var(--transition); + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags a:hover { @@ -301,6 +383,15 @@ article .meta { border-bottom: 1px solid var(--border-color); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--title-yellow); + transform: translateY(-2px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -310,6 +401,12 @@ article .meta { list-style-type: none; } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + .tag-count { color: #555; font-size: 12px; @@ -371,6 +468,12 @@ footer { text-align: center; } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Pagination */ .pagination { display: flex; @@ -390,6 +493,8 @@ footer { border-radius: 4px; transition: var(--transition); font-weight: 500; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a:hover { @@ -399,6 +504,15 @@ footer { border-bottom: 1px solid var(--border-color); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--title-yellow); + transform: translateY(-2px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + /* Blockquotes */ blockquote { border-left: 4px solid var(--title-yellow); @@ -604,6 +718,7 @@ tr:nth-child(even) { font-size: 1.4rem; } + .posts-list h2, h3 { font-size: 1.2rem; } diff --git a/themes/blackberry/style.css b/themes/blackberry/style.css index b97b5fa..0734f30 100644 --- a/themes/blackberry/style.css +++ b/themes/blackberry/style.css @@ -1,3 +1,30 @@ +/* + * BlackBerry Theme for BSSG + * Inspired by classic BlackBerry mobile interface + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for gradient text + * - Optimized font loading with better fallbacks + * - Enhanced performance while maintaining BlackBerry aesthetics + */ + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + article { + animation: none !important; + } +} + :root { /* BlackBerry inspired color scheme */ --bg-color: #000914; @@ -24,10 +51,10 @@ --title-color-start: #57a9ff; --title-color-end: #004080; - /* Typography */ - --font-main: 'SF UI Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - --font-headings: 'SF UI Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - --font-mono: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --font-headings: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --font-mono: 'Consolas', 'Liberation Mono', 'Menlo', 'Courier New', 'Courier', monospace; /* Spacing and sizing */ --radius: 4px; @@ -192,6 +219,8 @@ nav a { border: 1px solid var(--border-color); position: relative; box-shadow: 0 2px 0 rgba(0,0,0,0.3); + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a::after { @@ -215,6 +244,14 @@ nav a:active { transform: translateY(2px); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--nav-hover); + color: var(--link-hover-color); +} + main { margin-bottom: 3rem; padding: 0 1.5rem; @@ -303,6 +340,18 @@ article .meta { border: 1px solid var(--border-color); } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .summary { margin-bottom: 1.2rem; } @@ -328,6 +377,8 @@ article .tags a { border: 1px solid rgba(0,0,0,0.2); box-shadow: 0 1px 0 rgba(0,0,0,0.3); position: relative; + /* ACCESSIBILITY: Focus outline */ + outline: none; } article .tags a::after { @@ -346,17 +397,34 @@ article .tags a:hover { transform: translateY(-1px); } +/* ACCESSIBILITY: Focus states for tags */ +article .tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); + transform: translateY(-1px); +} + a { color: var(--link-color); text-decoration: none; font-weight: 500; transition: color var(--transition); + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:hover { color: var(--link-hover-color); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + p { margin: 0 0 1.5rem; } @@ -403,6 +471,13 @@ footer a:hover { color: var(--link-hover-color); } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + blockquote { border-left: 4px solid var(--accent-secondary); padding: 1rem 1.5rem; @@ -470,6 +545,8 @@ hr { border: 1px solid var(--border-color); position: relative; box-shadow: 0 2px 0 rgba(0,0,0,0.3); + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a::after { @@ -492,6 +569,13 @@ hr { transform: translateY(2px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -514,6 +598,8 @@ hr { border: 1px solid rgba(0,0,0,0.2); box-shadow: 0 1px 0 rgba(0,0,0,0.3); position: relative; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags-list a::after { @@ -531,6 +617,13 @@ hr { background-color: var(--accent-secondary); } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); +} + .tag-count { background-color: rgba(0, 0, 0, 0.2); padding: 0.1rem 0.4rem; @@ -544,7 +637,7 @@ hr { margin-bottom: 2rem; } -.posts-list h2, .posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } diff --git a/themes/braun/style.css b/themes/braun/style.css index 27d11a8..9ae4abb 100644 --- a/themes/braun/style.css +++ b/themes/braun/style.css @@ -4,6 +4,16 @@ * Minimalism, functionality, elegance, and precision */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + :root { /* Braun-inspired color scheme */ --white: #FFFFFF; @@ -104,6 +114,12 @@ header { color: var(--header-text); } +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--header-text); +} + /* Site description - minimal and precise */ header p { margin: calc(var(--grid-size)) 0 0; @@ -139,6 +155,12 @@ nav a.active { font-weight: 500; } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--nav-hover-text); +} + /* Content area with grid precision */ main { min-height: 70vh; @@ -191,6 +213,13 @@ a:hover { border-bottom: 1px solid var(--link-hover); } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + border-bottom: 1px solid var(--link-hover); +} + /* Articles with precise spacing and clean borders */ article { margin-bottom: calc(var(--grid-size) * 8); @@ -219,6 +248,18 @@ article .meta { position: relative; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags with Braun-inspired geometric precision */ .tags { display: flex; @@ -241,6 +282,14 @@ article .meta { border: none; } +.tags a:focus { + outline: 2px solid var(--secondary-accent); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--black); + border: none; +} + /* Tags list page - grid layout */ .tags-list { list-style-type: none; @@ -273,6 +322,14 @@ article .meta { border: none; } +.tags-list a:focus { + outline: 2px solid var(--secondary-accent); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--black); + border: none; +} + /* Footer - clean and precise */ footer { margin-top: calc(var(--grid-size) * 6); @@ -297,6 +354,13 @@ footer a:hover { border-bottom: 1px solid var(--link-hover); } +footer a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + border-bottom: 1px solid var(--link-hover); +} + /* Pagination - clean and precise */ .pagination { display: flex; @@ -323,6 +387,14 @@ footer a:hover { border: none; } +.pagination a:focus { + outline: 2px solid var(--secondary-accent); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--black); + border: none; +} + /* Featured images - clean and precise presentation */ .featured-image, .index-image, @@ -369,7 +441,7 @@ footer a:hover { padding-bottom: 0; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.5rem; margin-bottom: calc(var(--grid-size) * 1); diff --git a/themes/brutalist/style.css b/themes/brutalist/style.css index a3af6f2..a77cfdc 100644 --- a/themes/brutalist/style.css +++ b/themes/brutalist/style.css @@ -2,8 +2,19 @@ * Brutalist Theme for BSSG * Raw, minimalist concrete-inspired design * Features harsh typography, exposed elements, and high contrast + * IMPROVED: Enhanced accessibility and text browser support */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + :root { /* Brutalist colors - limited, harsh palette */ --bg-color: #ffffff; @@ -12,14 +23,14 @@ --accent: #ff0000; --text-color: #000000; --link-color: #0000ff; - --link-visited: #551a8b; + --link-visited: #8B0000; /* Dark red for better contrast on white background */ --transition: 0.2s ease; --radius: 0; - /* Typography - harsh, utilitarian */ - --font-main: 'Helvetica Neue', Arial, sans-serif; - --font-headings: 'Arial Black', 'Impact', sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - harsh, utilitarian - IMPROVED fallbacks for text browsers */ + --font-main: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, Arial, sans-serif; + --font-headings: 'Arial Black', 'Impact', -apple-system, BlinkMacSystemFont, Arial, sans-serif; + --font-mono: 'Courier New', 'Monaco', 'Consolas', monospace; /* Sizing */ --content-width: 1200px; @@ -75,7 +86,7 @@ header h1 { font-family: var(--font-headings); } -/* Site title with brutalist gradient style */ +/* Site title with brutalist gradient style - IMPROVED accessibility */ .site-title { margin: 0; padding: 0; @@ -85,33 +96,72 @@ header h1 { letter-spacing: -2px; line-height: 1; font-family: var(--font-headings); - background: linear-gradient(90deg, black 0%, var(--accent) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: black; /* Fallback for text browsers */ border-left: 8px solid var(--accent); padding-left: 10px; display: inline-block; } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title { + background: linear-gradient(90deg, black 0%, var(--accent) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + .site-title a { text-decoration: none; - background: linear-gradient(90deg, black 0%, var(--accent) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: black; /* Fallback for text browsers */ transition: all var(--transition); } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(90deg, black 0%, var(--accent) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + .site-title a:hover { text-decoration: none; - background: linear-gradient(90deg, var(--accent) 0%, black 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--accent); /* Fallback for text browsers */ transform: translateX(5px); } +/* Progressive enhancement for hover gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a:hover { + background: linear-gradient(90deg, var(--accent) 0%, black 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + +.site-title a:focus { + outline: 3px solid var(--accent); + outline-offset: 3px; + text-decoration: none; + color: var(--accent); /* Fallback for text browsers */ + transform: translateX(5px); +} + +/* Progressive enhancement for focus gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a:focus { + background: linear-gradient(90deg, var(--accent) 0%, black 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + /* Navigation with stark contrast */ nav { background-color: black; @@ -134,6 +184,10 @@ nav a { border-right: 1px solid var(--concrete); } +nav a:visited { + color: var(--link-visited); +} + nav a:hover { background-color: var(--accent); color: white; @@ -155,6 +209,20 @@ nav a:last-child:hover { color: var(--accent); } +nav a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + background-color: var(--accent); + color: white; +} + +nav a:last-child:focus { + outline: 3px solid white; + outline-offset: 2px; + background-color: black; + color: var(--accent); +} + /* Content area with visible grid */ main { padding: 30px; @@ -198,6 +266,7 @@ h2 { letter-spacing: -1px; } +.posts-list h2, h3 { font-size: 1.5rem; } @@ -224,6 +293,13 @@ a:hover { text-decoration: underline; } +a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + color: var(--accent); + text-decoration: underline; +} + /* Brutalist articles with visible structure */ article { margin-bottom: 50px; @@ -413,6 +489,13 @@ article .meta { color: white; } +.tags a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + background-color: var(--accent); + color: white; +} + .tags-list { display: flex; flex-wrap: wrap; @@ -505,6 +588,12 @@ footer a:hover { color: var(--accent); } +footer a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + color: var(--accent); +} + .pagination { display: flex; justify-content: space-between; @@ -526,11 +615,23 @@ footer a:hover { border: 2px solid black; } +.pagination a:visited { + color: var(--link-visited); + background-color: var(--concrete); +} + .pagination a:hover { background-color: var(--accent); color: white; } +.pagination a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + background-color: var(--accent); + color: white; +} + .pagination a:last-child { margin-left: auto; } @@ -567,6 +668,7 @@ footer a:hover { font-size: 1.8rem; } + .posts-list h2, h3 { font-size: 1.5rem; } @@ -641,6 +743,7 @@ footer a:hover { font-size: 1.6rem; } + .posts-list h2, h3 { font-size: 1.4rem; } @@ -695,6 +798,7 @@ footer a:hover { font-size: 1.5rem; } + .posts-list h2, h3 { font-size: 1.3rem; } diff --git a/themes/c64/style.css b/themes/c64/style.css index 7200c6a..5c83d88 100644 --- a/themes/c64/style.css +++ b/themes/c64/style.css @@ -1,3 +1,31 @@ +/* + * Commodore 64 Theme for BSSG + * Inspired by the classic Commodore 64 computer interface + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system monospace font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic C64 aesthetics + */ + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable blinking animation for reduced motion */ + .site-title a::after { + animation: none !important; + } +} + :root { /* Commodore 64 inspired color scheme */ --bg-color: #4040e0; @@ -16,10 +44,10 @@ --title-color-start: #ffff80; --title-color-end: #ffb080; - /* Typography */ - --font-main: 'C64 Pro', 'VT323', 'Px437 C64 Pro', monospace; - --font-headings: 'C64 Pro', 'VT323', 'Px437 C64 Pro', monospace; - --font-mono: 'C64 Pro', 'VT323', 'Px437 C64 Pro', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-headings: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-mono: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; /* Spacing and sizing */ --radius: 0; /* C64 didn't have rounded corners */ @@ -28,14 +56,6 @@ --transition: 0.1s ease; } -@font-face { - font-family: 'VT323'; - font-style: normal; - font-weight: 400; - src: url('https://fonts.gstatic.com/s/vt323/v12/pxiKyp0ihIEF2isQFJXGdg.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - *, *::before, *::after { box-sizing: border-box; } @@ -120,6 +140,13 @@ nav a:hover { color: var(--link-hover-color); } +nav a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--accent-secondary); + color: var(--link-hover-color); +} + main { margin-bottom: 3rem; } @@ -177,6 +204,14 @@ article h1::before { color: var(--link-color); } +/* TEXT BROWSER FALLBACK: Reset positioning when absolute positioning isn't supported */ +@supports not (position: absolute) { + article h1::before { + position: static; + left: auto; + } +} + .date-header { color: var(--date-color); font-size: 1.5rem; @@ -203,6 +238,18 @@ article .meta { align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .summary { margin-bottom: 1.2rem; } @@ -232,6 +279,14 @@ article .tags a:hover { transform: translateY(-2px); } +article .tags a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--link-hover-color); + color: var(--bg-color); + transform: translateY(-2px); +} + a { color: var(--link-color); text-decoration: none; @@ -242,6 +297,12 @@ a:hover { color: var(--link-hover-color); } +a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + p { margin: 0 0 1.5rem; } @@ -295,6 +356,12 @@ footer a:hover { color: var(--link-hover-color); } +footer a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + blockquote { border-left: 4px solid var(--link-color); padding: 1rem 1.5rem; @@ -359,6 +426,12 @@ hr { background-color: var(--accent-secondary); } +.pagination a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--accent-secondary); +} + .pagination .page-info { color: var(--date-color); } @@ -388,6 +461,13 @@ hr { color: var(--bg-color); } +.tags-list a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--link-hover-color); + color: var(--bg-color); +} + .tag-count { background-color: var(--accent-color); padding: 0.1rem 0.4rem; @@ -401,12 +481,12 @@ hr { margin-bottom: 2rem; } -.posts-list h2, .posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } -.posts-list h2::before, .posts-list h3::before { +.posts-list h2::before { content: ">"; margin-right: 0.5rem; color: var(--link-color); @@ -473,6 +553,12 @@ hr { background-color: var(--accent-secondary); } +.archives-nav a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--accent-secondary); +} + /* Responsive styles */ @media (max-width: 768px) { html { @@ -713,6 +799,24 @@ hr { text-shadow: 2px 0 0 var(--accent-color); } +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title a { + color: var(--title-color-start); + background: none; + } + + .site-title a:hover { + color: var(--title-color-end); + background: none; + } + + .site-title a:focus { + color: var(--title-color-end); + background: none; + } +} + .site-title a:hover { background: linear-gradient(to right, var(--title-color-end), var(--title-color-start)); -webkit-background-clip: text; @@ -721,6 +825,16 @@ hr { text-shadow: -2px 0 0 var(--accent-color); } +.site-title a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background: linear-gradient(to right, var(--title-color-end), var(--title-color-start)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: -2px 0 0 var(--accent-color); +} + .site-title a::after { content: "█"; animation: blink 1s step-end infinite; @@ -729,6 +843,14 @@ hr { color: var(--link-color); } +/* TEXT BROWSER FALLBACK: Use simple cursor when block character isn't supported */ +@supports not (animation: blink 1s step-end infinite) { + .site-title a::after { + content: "_"; + animation: none; + } +} + @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } diff --git a/themes/cyber-dark/style.css b/themes/cyber-dark/style.css new file mode 100644 index 0000000..1b1f60b --- /dev/null +++ b/themes/cyber-dark/style.css @@ -0,0 +1,42 @@ +/* + * Cyber-Dark theme by Nigel Swan + * for BSSG, https://bssg.dragas.net + */ +:root { + --highlight1: lightseagreen; + --highlight2: #e441e1; + --bright: #fff; + --text: #cecece; + --muted-text: #999999; + --background: #121212; + --border: #121212; + --blockquote: #222; + background-color: var(--background); +} + +a { font-family:sans-serif; color:var(--highlight2); text-decoration:none; } +body { font-size:1.1em; color:var(--text); padding:0.2em; font-family:sans-serif; max-width:60em; margin:auto; line-height:1.5; } +h1 { font-size:2em; color:var(--highlight1); text-shadow:0 0 20px; } +h2 { font-size:1.7em; color:var(--highlight1); text-shadow:0 0 20px; } +.posts-list h2, +h3 { font-size:1.4em; color:var(--highlight1); text-shadow:0 0 20px; } +nav { display:block; text-align:center; padding-top:0.8em; padding-bottom:3.5em; } +nav { a { padding-left:0.5em; padding-right:0.5em; text-decoration:underline var(--highlight1); text-shadow:0 0 9px var(--highlight1); } } +hr { border:1px solid var(--highlight2); } +p { padding-top:0.5em; padding-bottom:0.5em; } +header { text-align:center; margin:auto; } +header { p { text-shadow:0 0 10px var(--highlight2); } } +img { display:block; max-width: 100%; margin: auto; padding-top: 20px; padding-bottom: 20px; } +.posts-list { h2 { a { color:var(--highlight1); text-decoration: underline var(--highlight2); text-shadow:0 0 15px var(--highlight2); } } } +.featured-image.index-image { img { display:block; max-width: 100%; max-height:640px; margin: auto; padding-top: 20px; padding-bottom: 20px; } } +.image-caption { color:var(--muted-text); text-align: center; } +.site-title { a { text-shadow:0 0 30px var(--highlight2); text-decoration:underline 2px var(--highlight2); font-size:3em; font-weight:bold; color:var(--highlight1); }} +.meta, .page-meta { font-size:0.8em; color:var(--muted-text); } +.tag::before { content:"#"; } +.tags-list a::before { white-space:pre; content:'\A'; } +.summary { max-width:99%; margin:auto; padding-top:1em; padding-bottom:2em; } +article { padding-bottom:1em; } +blockquote { background:var(--blockquote); color:var(--bright); max-width:90%; padding:1em; border-radius:0.5em; margin:auto; display:flex; } +pre { background:var(--blockquote); max-width:90%; padding:1em; border-radius:0.5em; margin:auto; display:flex; } +code { font-family:monospace; color:var(--highlight1); text-shadow:0 0 5px var(--highlight2); } +footer { text-align:center; font-size:0.8em; } diff --git a/themes/dark/style.css b/themes/dark/style.css index 57fa2e4..e6fb210 100644 --- a/themes/dark/style.css +++ b/themes/dark/style.css @@ -1,36 +1,53 @@ /* * Dark Theme for BSSG - * A modern dark theme + * A modern dark theme with improved accessibility and compatibility */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + :root { - /* Dark palette */ - --bg-color: #121212; - --card-bg: #1e1e1e; - --text-color: #e0e0e0; - --link-color: #bb86fc; - --link-hover: #d7b8ff; - --header-color: #ffffff; - --border-color: #333333; - --accent-color: #03dac6; - --accent-secondary: #cf6679; - --tag-bg: #2d2d2d; - --tag-text: #bb86fc; - --code-bg: #1e1e1e; - --date-color: #9e9e9e; - --highlight-color: rgba(187, 134, 252, 0.1); + /* Dark palette - improved contrast ratios */ + --bg-color: #0d1117; + --card-bg: #161b22; + --text-color: #e6edf3; + --link-color: #58a6ff; + --link-hover: #79c0ff; + --header-color: #f0f6fc; + --border-color: #30363d; + --accent-color: #39d353; + --accent-secondary: #f85149; + --tag-bg: #21262d; + --tag-text: #58a6ff; + --code-bg: #161b22; + --date-color: #8b949e; + --highlight-color: rgba(88, 166, 255, 0.1); --transition: 0.3s ease; --radius: 8px; - /* Typography */ - --font-main: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --font-headings: 'Inter', sans-serif; - --font-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + /* Typography with better fallbacks */ + --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; + --font-headings: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, 'SFMono-Regular', 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; /* Sizing */ --content-width: 720px; } +/* Text browser and older browser support */ +@supports not (background-clip: text) { + :root { + --gradient-fallback-color: var(--header-color); + } +} + /* Base elements */ html { font-size: 16px; @@ -87,7 +104,7 @@ a { color: var(--link-color); text-decoration: none; transition: color var(--transition); - border-bottom: 1px solid rgba(187, 134, 252, 0.3); + border-bottom: 1px solid rgba(88, 166, 255, 0.3); padding-bottom: 1px; } @@ -96,6 +113,13 @@ a:hover { border-bottom-color: var(--link-hover); } +a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); + border-bottom-color: var(--link-hover); +} + /* Header */ header { margin-bottom: 3rem; @@ -115,7 +139,7 @@ header::after { border-radius: var(--radius); } -/* Site title with gradient effect */ +/* Site title - improved accessibility and text browser support */ .site-title { font-family: var(--font-headings); font-weight: 700; @@ -123,30 +147,68 @@ header::after { margin: 0; font-size: 2.5rem; letter-spacing: -0.03em; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: none; + } } .site-title a { text-decoration: none; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); + color: var(--header-color); transition: all var(--transition); } -.site-title a:hover { +/* Progressive enhancement for gradient text links */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } + + .site-title a:hover { + background: linear-gradient(120deg, var(--link-hover) 0%, var(--accent-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); + } + + .site-title a:focus { + background: linear-gradient(120deg, var(--link-hover) 0%, var(--accent-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); + } +} + +/* Fallback for browsers without gradient text support */ +@supports not ((background-clip: text) or (-webkit-background-clip: text)) { + .site-title a:hover { + color: var(--link-hover); + transform: translateY(-1px); + } + + .site-title a:focus { + color: var(--link-hover); + transform: translateY(-1px); + } +} + +.site-title a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; text-decoration: none; - background: linear-gradient(120deg, var(--link-hover) 0%, var(--accent-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - transform: translateY(-1px); } header h1 { @@ -154,11 +216,17 @@ header h1 { margin-bottom: 0.5rem; font-size: 2.5rem; letter-spacing: -0.03em; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); + color: var(--header-color); +} + +/* Progressive enhancement for header h1 gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + header h1 { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } } header p { @@ -203,6 +271,16 @@ nav a:hover::after { width: 100%; } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); +} + +nav a:focus::after { + width: 100%; +} + /* Article */ article { margin-bottom: 4rem; @@ -242,6 +320,13 @@ article .meta { font-size: 0.9rem; } +/* Text browser fallback for reading time icon */ +@media (max-width: 0) { /* This targets text browsers */ + .reading-time::before { + content: "[time] "; + } +} + /* Featured Images */ .featured-image { margin: 2rem 0; @@ -266,7 +351,7 @@ article .meta { bottom: 0; left: 0; right: 0; - background: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.8); color: #fff; padding: 0.5rem 1rem; font-size: 0.9rem; @@ -316,6 +401,7 @@ code { font-size: 0.9rem; border-radius: var(--radius); color: var(--accent-color); + border: 1px solid var(--border-color); } pre { @@ -325,12 +411,14 @@ pre { border-radius: var(--radius); margin: 2rem 0; border-left: 3px solid var(--accent-color); + border: 1px solid var(--border-color); } pre code { padding: 0; background-color: transparent; color: var(--text-color); + border: none; } /* Tags */ @@ -350,12 +438,12 @@ pre code { color: var(--tag-text); border-radius: var(--radius); transition: transform var(--transition), background-color var(--transition); - border: none; + border: 1px solid var(--border-color); } .tags a:hover { transform: translateY(-2px); - background-color: rgba(45, 45, 45, 0.8); + background-color: var(--border-color); } .tags-list { @@ -376,6 +464,7 @@ pre code { width: 1.5rem; height: 1.5rem; margin-left: 0.4rem; + border: 1px solid var(--border-color); } img { @@ -436,7 +525,7 @@ footer::before { padding-bottom: 1.5rem; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } @@ -450,23 +539,30 @@ blockquote { border-left: 4px solid var(--accent-color); padding: 1rem 1.5rem; margin: 2rem 0; - background-color: rgba(187, 134, 252, 0.05); + background-color: rgba(88, 166, 255, 0.05); border-radius: 0 var(--radius) var(--radius) 0; font-style: italic; position: relative; } blockquote::before { - content: """; + content: "\201C"; font-size: 3rem; font-family: Georgia, serif; color: var(--accent-color); - opacity: 0.2; + opacity: 0.3; position: absolute; top: -1rem; left: 0.5rem; } +/* Text browser fallback for blockquote */ +@media (max-width: 0) { /* This targets text browsers */ + blockquote::before { + content: ""; + } +} + /* Lists */ ul, ol { margin-top: 0; @@ -499,6 +595,13 @@ hr::before { font-size: 0.8rem; } +/* Text browser fallback for hr decoration */ +@media (max-width: 0) { /* This targets text browsers */ + hr::before { + content: "---"; + } +} + /* Pagination - Dark Theme */ .pagination { display: flex; @@ -525,6 +628,13 @@ hr::before { color: var(--bg-color); } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--bg-color); +} + .pagination .page-info { color: var(--date-color); font-size: 0.9rem; @@ -696,6 +806,4 @@ hr::before { margin: 2rem 0 0.75rem; padding: 0.8rem 0; } -} - - \ No newline at end of file +} \ No newline at end of file diff --git a/themes/default/style.css b/themes/default/style.css index 01483a5..725ada9 100644 --- a/themes/default/style.css +++ b/themes/default/style.css @@ -15,10 +15,10 @@ --highlight-color: #fef3c7; --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05); - /* Typography */ - --font-main: 'Spectral', Georgia, serif; - --font-headings: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; + /* Typography - System fonts for better performance and compatibility */ + --font-main: Georgia, 'Times New Roman', Times, serif; + --font-headings: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; /* Spacing and sizing */ --radius: 10px; @@ -45,29 +45,33 @@ } } -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all transforms and animations */ + .site-title a:hover, + article:hover, + article .tags a:hover, + article .tags a:focus, + .featured-image:hover img, + img:hover, + .tags-list a:hover, + .posts-list article:hover { + transform: none !important; + } + + @keyframes fadeIn { + from, to { opacity: 1; } + } } -@font-face { - font-family: 'Spectral'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;0,500;0,600;1,400&display=swap'); -} - -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap'); -} +/* Removed external font loading for better performance and text browser compatibility */ *, *::before, *::after { box-sizing: border-box; @@ -125,32 +129,55 @@ header::after { margin: 0; font-size: 2.75rem; letter-spacing: -0.5px; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: 0 1px 1px rgba(0,0,0,0.05); + } } .site-title a { text-decoration: none; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); + color: var(--header-color); transition: all var(--transition); + outline: 2px solid transparent; + outline-offset: 2px; } .site-title a:hover { text-decoration: none; - background: linear-gradient(120deg, var(--link-hover-color) 0%, var(--accent-secondary) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: var(--link-color); transform: translateY(-1px); } +.site-title a:focus { + outline-color: var(--link-color); +} + +/* Progressive enhancement for gradient text on site title links */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: 0 1px 1px rgba(0,0,0,0.05); + } + + .site-title a:hover { + background: linear-gradient(120deg, var(--link-hover-color) 0%, var(--accent-secondary) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } +} + header h1 { font-family: var(--font-headings); font-weight: 700; @@ -158,11 +185,17 @@ header h1 { margin: 0; font-size: 2.75rem; letter-spacing: -0.5px; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); +} + +/* Progressive enhancement for gradient text on header h1 */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + header h1 { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: 0 1px 1px rgba(0,0,0,0.05); + } } header p { @@ -210,6 +243,17 @@ nav a:hover::after { width: 100%; } +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); + transform: translateY(-1px); +} + +nav a:focus::after { + width: 100%; +} + main { margin-bottom: 3.5rem; } @@ -342,6 +386,14 @@ article .meta { margin-right: 0.35rem; } +/* Text browser fallback for reading time icon */ +@supports not (content: "⏱") { + .reading-time::before { + content: "Time: "; + margin-right: 0.35rem; + } +} + .summary { margin-top: 1.5rem; } @@ -372,6 +424,14 @@ article .tags a:hover { transform: translateY(-2px); } +article .tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-text); + color: var(--tag-bg); + transform: translateY(-2px); +} + .featured-image { margin: 2rem 0; border-radius: var(--radius); @@ -449,6 +509,14 @@ a:hover { text-decoration-thickness: 2px; } +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + border-radius: 2px; + color: var(--link-hover-color); + text-decoration-thickness: 2px; +} + p { margin: 1.5rem 0; } @@ -486,6 +554,16 @@ footer { position: relative; } +footer a { + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 2px; +} + +footer a:focus { + outline-color: var(--link-color); +} + footer::before { content: ""; position: absolute; @@ -518,6 +596,13 @@ blockquote::before { left: 0.5rem; } +/* Text browser fallback for blockquote decoration */ +@supports not (content: '"') { + blockquote::before { + content: ""; + } +} + code { font-family: var(--font-mono); font-size: 0.9em; @@ -581,6 +666,13 @@ hr::before { transform: translate(-50%, -50%); } +/* Text browser fallback for hr decoration */ +@supports not (content: "✦") { + hr::before { + content: "*"; + } +} + .pagination { display: flex; justify-content: space-between; @@ -591,6 +683,17 @@ hr::before { font-weight: 500; } +.pagination a { + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 2px; + transition: all var(--transition); +} + +.pagination a:focus { + outline-color: var(--link-color); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -618,6 +721,14 @@ hr::before { transform: translateY(-2px); } +.tags-list a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-text); + color: var(--tag-bg); + transform: translateY(-2px); +} + .tag-count { background-color: rgba(0, 0, 0, 0.1); border-radius: 100px; @@ -631,11 +742,21 @@ hr::before { padding-bottom: 1.5rem; } -.posts-list h2, .posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } +.posts-list h2 a { + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 2px; +} + +.posts-list h2 a:focus { + outline-color: var(--link-color); +} + article > p:first-of-type::first-letter { initial-letter: 2; -webkit-initial-letter: 2; @@ -729,6 +850,15 @@ article > p:first-of-type::first-letter { .tags-list { gap: 0.7rem; } + + .related-posts { + margin-top: 2rem; + padding-top: 1.5rem; + } + + .related-post { + padding: 1.2rem; + } } /* Medium-small screens */ @@ -804,6 +934,19 @@ article > p:first-of-type::first-letter { blockquote { padding: 1.1rem 1.3rem; } + + .related-posts { + margin-top: 1.5rem; + padding-top: 1.2rem; + } + + .related-post { + padding: 1rem; + } + + .related-post h4 { + font-size: 1rem; + } } /* Small screens */ @@ -972,3 +1115,91 @@ article > p:first-of-type::first-letter { background-color: var(--accent-secondary); color: var(--text-color); } + +.archives-nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); + color: var(--text-color); +} + +/* Related Posts Styles */ +.related-posts { + margin-top: 3rem; + padding-top: 2rem; + border-top: 1px solid var(--border-color); +} + +.related-posts h3 { + font-family: var(--font-headings); + font-size: 1.5rem; + font-weight: 600; + color: var(--header-color); + margin-bottom: 1.5rem; + position: relative; +} + +.related-posts h3::after { + content: ""; + position: absolute; + bottom: -0.5rem; + left: 0; + width: 60px; + height: 2px; + background: linear-gradient(90deg, var(--link-color), var(--accent-secondary)); + border-radius: var(--radius); +} + +.related-posts-list { + display: grid; + gap: 1.5rem; + margin-top: 1.5rem; +} + +.related-post { + padding: 1.5rem; + background-color: var(--accent-color); + border-radius: var(--radius); + border-left: 4px solid var(--accent-secondary); + transition: all var(--transition); + position: relative; +} + +.related-post:hover { + transform: translateY(-2px); + box-shadow: var(--card-shadow); + border-left-color: var(--link-color); +} + +.related-post h4 { + margin: 0 0 0.75rem 0; + font-family: var(--font-headings); + font-size: 1.1rem; + font-weight: 600; + line-height: 1.4; +} + +.related-post h4 a { + color: var(--header-color); + text-decoration: none; + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 2px; +} + +.related-post h4 a:hover { + color: var(--link-color); + text-decoration: underline; +} + +.related-post h4 a:focus { + outline-color: var(--link-color); + color: var(--link-color); +} + +.related-post p { + margin: 0; + color: var(--date-color); + font-size: 0.9rem; + line-height: 1.5; +} diff --git a/themes/diary/style.css b/themes/diary/style.css index 9524815..731473c 100644 --- a/themes/diary/style.css +++ b/themes/diary/style.css @@ -1,8 +1,37 @@ /* * Reflections - A modern, elegant diary theme * Rich visual design with progressive enhancement + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining elegant diary aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable fade-in animations */ + .container, article { + animation: none !important; + } + + /* Disable hover transforms */ + article:hover, figure:hover, .featured-image:hover, + .index-image:hover, .tag-image:hover, .archive-image:hover { + transform: none !important; + } +} + :root { /* Modern, refined color palette */ --bg-color: #faf7f2; @@ -24,11 +53,11 @@ --title-gradient-start: #a2836e; --title-gradient-end: #8fb9aa; - /* Typography - using system fonts + optional web fonts */ - --font-main: 'Spectral', Georgia, 'Times New Roman', serif; - --font-headings: 'Fraunces', 'Playfair Display', Georgia, serif; - --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Monaco, Consolas, monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: Georgia, 'Times New Roman', Times, serif; + --font-headings: Georgia, 'Times New Roman', Times, serif; + --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --font-mono: 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* Spacing and structure */ --content-width: 46rem; @@ -79,38 +108,7 @@ } } -/* Optional web fonts - will fallback gracefully if not available */ -@font-face { - font-family: 'Spectral'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;0,500;1,400&display=swap'); -} - -@font-face { - font-family: 'Fraunces'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600&display=swap'); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'); -} - -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap'); -} +/* REMOVED: External font loading for better performance and text browser compatibility */ /* Base styles */ * { @@ -300,10 +298,9 @@ article::before { border-radius: 2px; } -/* Optional hover effect */ +/* PERFORMANCE: Simplified hover effect without expensive transforms */ article:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-lg); + box-shadow: var(--shadow-md); /* Reduced shadow complexity */ } article:last-child { @@ -339,10 +336,15 @@ h2::after { background: linear-gradient(to right, var(--accent-primary), transparent); } +.posts-list h2, h3 { font-size: 1.5rem; } +.posts-list h2::after { + content: none; +} + h4 { font-size: 1.25rem; } @@ -423,6 +425,18 @@ blockquote::before { line-height: 1; } +/* TEXT BROWSER FALLBACK: Simplify quote mark when positioning isn't supported */ +@supports not (position: absolute) { + blockquote::before { + content: '"'; + position: static; + top: auto; + left: auto; + font-size: 1.2rem; + opacity: 1; + } +} + blockquote p:last-child { margin-bottom: 0; } @@ -457,12 +471,10 @@ pre code { /* Images */ figure { margin: var(--spacing-lg) 0; - transition: transform var(--transition-normal); + /* PERFORMANCE: Removed expensive transform transition */ } -figure:hover { - transform: scale(1.01); -} +/* PERFORMANCE: Removed scale transform that causes layout recalculation */ img { max-width: 100%; @@ -503,23 +515,39 @@ figcaption { align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Date: " prefix for date */ .date::before { - content: "📅"; + content: "Date: "; margin-right: 0.35rem; font-size: 1.1em; } +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .date::before { + content: "📅"; + } +} + .reading-time { display: inline-flex; align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏱️"; + content: "Time: "; margin-right: 0.35rem; font-size: 1.1em; } +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱️"; + } +} + /* Tags */ .tags { margin-top: var(--spacing-md); @@ -542,12 +570,12 @@ figcaption { box-shadow: var(--shadow-sm); } +/* PERFORMANCE: Simplified hover without expensive transform */ .tags a:hover, .tags a:focus { background-color: var(--tag-hover); color: var(--accent-primary); - transform: translateY(-2px); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-sm); /* Reduced shadow complexity */ } /* Horizontal rule */ @@ -581,6 +609,21 @@ hr::before { font-size: 1rem; } +/* TEXT BROWSER FALLBACK: Simplify decorative element when positioning isn't supported */ +@supports not (position: absolute) { + hr::before { + content: "*"; + position: static; + left: auto; + top: auto; + transform: none; + width: auto; + height: auto; + border-radius: 0; + background-color: transparent; + } +} + /* Footer */ footer { padding-top: var(--spacing-lg); @@ -630,11 +673,11 @@ footer p { box-shadow: var(--shadow-sm); } +/* PERFORMANCE: Simplified hover without expensive transform */ .pagination a:hover, .pagination a:focus { background-color: var(--bg-secondary); - transform: translateY(-2px); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-sm); /* Reduced shadow complexity */ } .pagination a.prev::before { @@ -662,9 +705,9 @@ footer p { transition: all var(--transition-normal); } +/* PERFORMANCE: Simplified hover without expensive transform */ .archive-list li:hover { - transform: translateX(4px); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-sm); /* Reduced shadow complexity */ } .archive-date { @@ -685,9 +728,9 @@ footer p { border-left: 3px solid var(--accent-primary); } +/* PERFORMANCE: Simplified hover without expensive transform */ .post-summary:hover { - transform: translateY(-3px); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-sm); /* Reduced shadow complexity */ } .post-summary h3 { @@ -735,39 +778,34 @@ footer p { color: var(--accent-primary); } -/* Animation for page load - optional enhancement */ +/* PERFORMANCE OPTIMIZATION: Simplified animations */ @keyframes fadeIn { from { opacity: 0; - transform: translateY(10px); } to { opacity: 1; - transform: translateY(0); } } +/* PERFORMANCE: Removed staggered animations that cause layout thrashing */ .container { - animation: fadeIn 0.5s ease-out; + opacity: 1; /* Default visible state for better performance */ } -/* Animation for article entries - optional */ +/* PERFORMANCE: Simplified article animation without transforms */ article { - opacity: 0; - animation: fadeIn 0.5s ease-out forwards; + opacity: 1; /* Default visible state */ + animation: fadeIn 0.3s ease-out; /* Shorter, simpler animation */ } -article:nth-child(1) { animation-delay: 0.1s; } -article:nth-child(2) { animation-delay: 0.2s; } -article:nth-child(3) { animation-delay: 0.3s; } -article:nth-child(4) { animation-delay: 0.4s; } -article:nth-child(5) { animation-delay: 0.5s; } +/* PERFORMANCE: Removed staggered delays that cause multiple repaints */ /* Ensure content is visible even without animations */ @media (prefers-reduced-motion: reduce) { .container, article { - animation: none; - opacity: 1; + animation: none !important; + opacity: 1 !important; } } @@ -789,6 +827,7 @@ article:nth-child(5) { animation-delay: 0.5s; } font-size: 1.6rem; } + .posts-list h2, h3 { font-size: 1.3rem; } @@ -845,6 +884,7 @@ article:nth-child(5) { animation-delay: 0.5s; } font-size: 1.5rem; } + .posts-list h2, h3 { font-size: 1.25rem; } @@ -974,6 +1014,7 @@ article:nth-child(5) { animation-delay: 0.5s; } font-size: 1.4rem; } + .posts-list h2, h3 { font-size: 1.2rem; } @@ -1088,13 +1129,9 @@ article:nth-child(5) { animation-delay: 0.5s; } transition: all var(--transition-normal); } +/* PERFORMANCE: Simplified hover without expensive transform and gradient recalculation */ .site-title a:hover { - background: linear-gradient(120deg, var(--title-gradient-end), var(--title-gradient-start)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - transform: translateY(-1px); - text-shadow: 0px 2px 2px rgba(0,0,0,0.1); + color: var(--accent-secondary); /* Simple color change instead of gradient */ } /* Fallback for browsers that don't support background-clip */ @@ -1141,14 +1178,12 @@ article:nth-child(5) { animation-delay: 0.5s; } margin: 0; } +/* PERFORMANCE: Simplified hover without expensive transform and filter */ .featured-image:hover { - box-shadow: var(--shadow-lg); - transform: translateY(-2px); + box-shadow: var(--shadow-md); /* Reduced shadow complexity */ } -.featured-image:hover img { - filter: brightness(1.03); -} +/* PERFORMANCE: Removed expensive filter effect */ .featured-image .image-caption { padding: var(--spacing-sm) var(--spacing-xs) 0; @@ -1190,9 +1225,9 @@ article:nth-child(5) { animation-delay: 0.5s; } margin: 0; } +/* PERFORMANCE: Simplified hover without expensive transform */ .index-image:hover { - box-shadow: var(--shadow-lg); - transform: translateY(-2px); + box-shadow: var(--shadow-md); /* Reduced shadow complexity */ } .tag-image { @@ -1226,9 +1261,9 @@ article:nth-child(5) { animation-delay: 0.5s; } margin: 0; } +/* PERFORMANCE: Simplified hover without expensive transform */ .tag-image:hover { - box-shadow: var(--shadow-lg); - transform: translateY(-2px); + box-shadow: var(--shadow-md); /* Reduced shadow complexity */ } .archive-image { diff --git a/themes/docs/style.css b/themes/docs/style.css index fbc90fc..ad658cd 100644 --- a/themes/docs/style.css +++ b/themes/docs/style.css @@ -2,8 +2,24 @@ * Documentation Theme for BSSG * A clean, structured theme ideal for technical documentation or guides. * Features clear navigation, excellent code formatting, and readable typography. + * Enhanced with accessibility, performance, and compatibility improvements. */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + html { + scroll-behavior: auto !important; + } +} + :root { /* Color scheme - professional documentation style */ --bg-color: #ffffff; @@ -31,9 +47,9 @@ --tip-bg: #e6f6e6; --tip-border: #a8d8a8; - /* Typography */ - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + /* Typography - enhanced system fonts for better compatibility */ + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, 'Courier New', Courier, monospace; --font-headings: var(--font-sans); /* Sizing */ @@ -51,12 +67,19 @@ --heading-line-height: 1.3; } -/* Base elements */ +/* Base elements - optimized for accessibility */ html { font-size: 16px; scroll-behavior: smooth; } +/* Progressive enhancement for smooth scrolling */ +@media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; + } +} + body { font-family: var(--font-sans); background-color: var(--bg-color); @@ -180,6 +203,18 @@ nav a.active { border-bottom: 2px solid var(--nav-active); } +/* Accessibility: Focus outlines for all interactive elements */ +nav a:focus, +.site-title a:focus, +a:focus, +.tags a:focus, +.pagination a:focus, +.menu-toggle:focus, +.anchor:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Mobile menu toggle button */ .menu-toggle { display: none; diff --git a/themes/field-journal/style.css b/themes/field-journal/style.css new file mode 100644 index 0000000..e13a04d --- /dev/null +++ b/themes/field-journal/style.css @@ -0,0 +1,461 @@ +/* + * Field Journal Theme for BSSG + * Warm paper, natural inks, and notebook-like details. + */ + +:root { + --bg-color: #f6f0e2; + --paper-color: #fffaf0; + --text-color: #2f3a2b; + --heading-color: #273221; + --muted-text: #68725f; + --link-color: #365d3b; + --link-hover: #234028; + --border-color: #d5c9a8; + --accent-color: #8d6f43; + --accent-soft: #ede2cb; + --tag-bg: #e8dcc2; + --tag-text: #3f5133; + --quote-bg: #f2e8d3; + --code-bg: #efe6d2; + --content-width: 860px; + --radius: 8px; + --shadow: 0 4px 12px rgba(68, 53, 24, 0.11); + --font-body: "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif; + --font-heading: "Hoefler Text", Baskerville, "Times New Roman", serif; + --font-ui: "Trebuchet MS", Verdana, Arial, sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 18px; +} + +body { + margin: 0; + color: var(--text-color); + background-color: var(--bg-color); + background-image: + radial-gradient(circle at 2px 2px, rgba(92, 77, 46, 0.06) 0.6px, transparent 0.6px), + linear-gradient(180deg, #f8f3e7 0%, #f2ead7 100%); + background-size: 4px 4px, 100% 100%; + font-family: var(--font-body); + line-height: 1.75; +} + +::selection { + background: #d7c49a; + color: #20261b; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 2rem 1.3rem 2.8rem; +} + +header { + margin-bottom: 2rem; + padding: 1.2rem 0 1rem; + border-top: 2px solid var(--border-color); + border-bottom: 2px solid var(--border-color); + background: linear-gradient(180deg, rgba(255, 252, 244, 0.82) 0%, rgba(246, 238, 221, 0.72) 100%); +} + +.site-title, +header h1 { + margin: 0; + color: var(--heading-color); + font-family: var(--font-heading); + font-size: clamp(2rem, 4.4vw, 2.9rem); + font-weight: 600; + letter-spacing: 0.02em; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +header p { + margin: 0.45rem 0 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.85rem; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +nav { + margin-top: 0.95rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +nav a { + display: inline-block; + text-decoration: none; + color: var(--link-color); + border: 1px solid #cfbf9b; + border-radius: 999px; + padding: 0.22rem 0.68rem; + font-family: var(--font-ui); + font-size: 0.74rem; + letter-spacing: 0.08em; + text-transform: uppercase; + background: rgba(255, 251, 239, 0.84); +} + +nav a:hover, +nav a:focus { + color: #1d3622; + background: #e5d9bd; +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.5rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--paper-color); + border: 1px solid #d3c4a0; + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 1.35rem 1.15rem 1.15rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.25rem 0 0.75rem; + color: var(--heading-color); + font-family: var(--font-heading); + line-height: 1.3; +} + +h1 { font-size: clamp(1.8rem, 3.7vw, 2.4rem); } +h2 { font-size: clamp(1.45rem, 3.1vw, 1.9rem); } +h3 { font-size: clamp(1.18rem, 2.3vw, 1.45rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 1rem; +} + +article.post > p:first-of-type::first-letter { + float: left; + font-size: 2.6em; + line-height: 0.88; + margin-right: 0.12em; + margin-top: 0.06em; + color: #4a633f; +} + +a { + color: var(--link-color); + text-underline-offset: 0.16em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.85rem; +} + +.meta { + margin: 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.77rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.reading-time { + margin-top: 0.28rem; +} + +.summary { + margin-top: 0.75rem; + color: #43533a; +} + +.post-content { + margin-top: 0.7rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 1.05rem 0; +} + +img { + max-width: 100%; + height: auto; + display: block; + border-radius: 4px; + border: 1px solid #bcae8c; +} + +.image-caption, +figcaption { + margin-top: 0.38rem; + color: var(--muted-text); + font-style: italic; + font-size: 0.86rem; +} + +blockquote { + margin: 1.25rem 0; + padding: 0.75rem 0.95rem; + background: var(--quote-bg); + border-left: 4px solid #9a8256; + color: #3d4d34; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.87rem; +} + +code { + background: var(--code-bg); + padding: 0.1rem 0.3rem; + border-radius: 3px; +} + +pre { + background: var(--code-bg); + border: 1px solid #cdbd99; + border-radius: 6px; + padding: 0.86rem; + overflow-x: auto; +} + +pre code { + background: none; + padding: 0; +} + +hr { + border: 0; + border-top: 2px dashed #c7b792; + margin: 1.3rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.42rem; + margin-top: 1rem; +} + +.tags a, +.tags-list a { + display: inline-block; + padding: 0.22rem 0.62rem; + border-radius: 999px; + border: 1px solid #bea97d; + background: var(--tag-bg); + color: var(--tag-text); + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.75rem; + letter-spacing: 0.05em; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #ddcea9; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.tag-count { + color: #5e6b57; +} + +.posts-list article + article { + margin-top: 0.95rem; +} + +.posts-list h2 { + margin-top: 0; +} + +.pagination { + margin-top: 1.4rem; + display: flex; + flex-wrap: wrap; + gap: 0.55rem; + align-items: center; +} + +.pagination a { + display: inline-block; + padding: 0.26rem 0.7rem; + border-radius: 999px; + border: 1px solid #baa778; + background: #f0e5cd; + color: #32452b; + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.75rem; + letter-spacing: 0.05em; +} + +.pagination a:hover, +.pagination a:focus { + background: #e3d2ab; +} + +.page-info { + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.07em; +} + +.related-posts { + margin-top: 1.6rem; + padding-top: 1rem; + border-top: 2px dotted #bca779; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.1rem; +} + +.related-posts-list { + display: grid; + gap: 0.7rem; +} + +.related-post { + background: var(--accent-soft); + border: 1px solid #ccb993; + border-left: 4px solid #8f7448; + border-radius: 5px; + padding: 0.62rem 0.72rem; +} + +.related-post h4 { + margin: 0 0 0.3rem; +} + +.related-post p { + margin: 0; + color: #495b3f; +} + +footer { + margin-top: 2rem; + border-top: 2px solid var(--border-color); + padding-top: 0.95rem; + color: #62705d; + font-family: var(--font-ui); + font-size: 0.76rem; + letter-spacing: 0.04em; +} + +footer p { + margin: 0.38rem 0; +} + +footer a { + color: inherit; +} + +footer a:hover, +footer a:focus { + color: #2f4b35; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.95rem 0; +} + +th, +td { + border: 1px solid #c9ba96; + padding: 0.42rem 0.58rem; +} + +th { + background: #efe3c9; + font-family: var(--font-ui); + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +@media (max-width: 780px) { + html { + font-size: 17px; + } + + .container { + padding: 1.3rem 0.9rem 2.2rem; + } + + article.post, + article.page, + .posts-list article { + padding: 1.05rem 0.9rem 0.95rem; + } + + nav { + gap: 0.38rem; + } + + nav a, + .meta, + .pagination a, + .page-info { + letter-spacing: 0.04em; + } +} diff --git a/themes/flat/style.css b/themes/flat/style.css index c32042a..988748b 100644 --- a/themes/flat/style.css +++ b/themes/flat/style.css @@ -2,6 +2,7 @@ * Flat Design Theme for BSSG * Inspired by Microsoft Metro/Modern UI design language * Features typography-focused design, solid colors, and minimal decoration + * Enhanced with accessibility, performance, and compatibility improvements */ :root { @@ -33,6 +34,34 @@ --gap-size: 10px; } +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all transforms and animations */ + .site-title a:hover, + .metro-tile:hover, + .metro-button:hover, + .tags a:hover, + .pagination a:hover, + footer a:hover::after, + .featured-image:hover img, + .index-image:hover img, + .tag-image:hover img, + .archive-image:hover img { + transform: none !important; + } + + .progress-indicator { + transition: none !important; + } +} + /* Base elements */ body { font-family: var(--font-main); @@ -74,29 +103,35 @@ header { .site-title a { color: var(--text-on-accent); text-decoration: none; - background: linear-gradient(90deg, var(--title-gradient-start), var(--title-gradient-end)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; display: inline-block; position: relative; transition: all 0.2s ease; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); font-weight: 500; + outline: 2px solid transparent; + outline-offset: 2px; } .site-title a:hover { transform: translateY(-1px); - text-shadow: 0 2px 5px rgba(0, 0, 0, 0.4); } -/* Fallback for browsers that don't support background-clip */ -@supports not (background-clip: text) { +.site-title a:focus { + outline-color: rgba(255, 255, 255, 0.8); +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { .site-title a { - color: white; - background: none; + background: linear-gradient(90deg, var(--title-gradient-start), var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); } + + .site-title a:hover { + text-shadow: 0 2px 5px rgba(0, 0, 0, 0.4); + } } header h1 { @@ -143,6 +178,11 @@ nav a:hover, nav a:focus { border-bottom: 3px solid rgba(255, 255, 255, 0.5); } +nav a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; +} + /* Active navigation item */ nav a.active { color: white; @@ -188,6 +228,7 @@ h2 { margin-top: 40px; } +.posts-list h2, h3 { font-size: 20px; margin-top: 30px; @@ -213,6 +254,12 @@ a:hover { color: var(--accent-hover); } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-hover); +} + /* Metro style button */ .metro-button { display: inline-block; @@ -232,6 +279,12 @@ a:hover { background-color: var(--accent-hover); } +.metro-button:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-hover); +} + /* Articles as flat tiles */ article { margin-bottom: 30px; @@ -274,13 +327,21 @@ article:nth-child(5n+5) .article-header { background-color: var(--tile-color-5); } -article h1 { +article .article-header h1 { font-size: 24px; margin: 0; padding: 0; color: white; } +/* Fix for article content h1 - should use normal text color */ +article .article-content h1 { + color: var(--text-color); + font-size: 24px; + margin: 0 0 1rem 0; + padding: 0; +} + /* Article content with clean padding */ article .article-content { padding: 30px; @@ -324,6 +385,13 @@ article .meta { color: white; } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + color: white; +} + .tags-list { list-style-type: none; padding: 0; @@ -405,6 +473,15 @@ footer a:hover::after { transform: scaleX(1); } +footer a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; +} + +footer a:focus::after { + transform: scaleX(1); +} + /* Metro-style grid layout */ .metro-grid { display: grid; @@ -430,6 +507,12 @@ footer a:hover::after { background-color: var(--accent-hover); } +.metro-tile:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-hover); +} + /* Rotating colors for metro tiles */ .metro-tile:nth-child(5n+1) { background-color: var(--tile-color-1); @@ -481,6 +564,12 @@ footer a:hover::after { background-color: var(--accent-hover); } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-hover); +} + .pagination .page-info { margin: 0 15px; font-size: 14px; @@ -517,6 +606,7 @@ footer a:hover::after { font-size: 24px; } + .posts-list h2, h3 { font-size: 18px; } @@ -639,6 +729,7 @@ footer a:hover::after { margin-top: 30px; } + .posts-list h2, h3 { font-size: 17px; margin-top: 20px; @@ -706,6 +797,7 @@ footer a:hover::after { font-size: 18px; } + .posts-list h2, h3 { font-size: 16px; } diff --git a/themes/freebsd/style.css b/themes/freebsd/style.css new file mode 100644 index 0000000..4c64628 --- /dev/null +++ b/themes/freebsd/style.css @@ -0,0 +1,516 @@ +/* + * FreeBSD Theme for BSSG + * Recognizable FreeBSD-inspired look using two reds + black and the orb motif. + */ + +:root { + --freebsd-red-1: #c61f2b; + --freebsd-red-2: #9b1a22; + --freebsd-black: #101112; + --freebsd-charcoal: #1a1d1f; + --paper: #fcfcfc; + --paper-2: #f4f5f6; + --text: #1d1f22; + --muted: #5a6169; + --border: #d5d9de; + --link: #a3151f; + --link-hover: #7f0f18; + --tag-bg: #f8e7e9; + --tag-text: #7f141d; + --quote-bg: #f6f7f8; + --code-bg: #f2f4f6; + --radius: 8px; + --shadow: 0 12px 28px rgba(0, 0, 0, 0.25); + --content-width: 920px; + --font-body: "Roboto", "Helvetica Neue", Arial, sans-serif; + --font-heading: "Montserrat", "Arial Narrow", "Trebuchet MS", sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 17px; +} + +body { + margin: 0; + color: var(--text); + font-family: var(--font-body); + line-height: 1.7; + background: + radial-gradient(circle at 80% 10%, rgba(198, 31, 43, 0.22) 0, transparent 32%), + linear-gradient(180deg, #1b1d20 0%, #0f1011 100%); +} + +::selection { + background: #f0c8cc; + color: #1a1a1a; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 0 1.25rem 2.7rem; + background: linear-gradient(180deg, #ffffff 0%, var(--paper) 100%); + border-left: 1px solid #3c4146; + border-right: 1px solid #3c4146; + box-shadow: var(--shadow); + min-height: 100vh; +} + +header { + margin: 0 -1.25rem 2rem; + padding: 1.1rem 1.25rem 1rem; + background: + linear-gradient(135deg, rgba(198, 31, 43, 0.12) 0 14%, transparent 15%), + linear-gradient(180deg, #fefefe 0%, #ebedf0 100%); + border-top: 6px solid var(--freebsd-red-1); + border-bottom: 1px solid #c8cfd6; + position: relative; +} + +header::before { + content: "FreeBSD: The Power To Serve"; + position: absolute; + top: 12px; + right: 18px; + background: linear-gradient(180deg, #c42935 0%, #981b24 100%); + color: #fff; + border: 1px solid #7f141d; + border-radius: 4px; + padding: 0.16rem 0.56rem; + font-family: var(--font-mono); + font-size: 0.58rem; + font-weight: 700; + letter-spacing: 0.01em; + text-transform: none; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.22); +} + +header::after { + content: ""; + position: absolute; + inset: auto 0 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--freebsd-red-1), transparent); +} + +.site-title, +header h1 { + margin: 0; + color: var(--freebsd-black); + font-family: var(--font-heading); + font-weight: 700; + font-size: clamp(1.9rem, 3.8vw, 2.75rem); + letter-spacing: 0.03em; + text-transform: uppercase; + line-height: 1.2; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: var(--freebsd-red-2); +} + +header p { + margin: 0.42rem 0 0; + color: #4f5660; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.1em; + font-weight: 600; +} + +nav { + margin-top: 0.95rem; + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +nav a { + display: inline-block; + color: #eef1f4; + background: linear-gradient(180deg, #2c3035 0%, #1c1f23 100%); + border: 1px solid #4b5158; + border-bottom-color: #7a8796; + border-radius: 5px; + padding: 0.26rem 0.6rem; + text-decoration: none; + font-size: 0.73rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.07em; + transition: transform 0.16s ease, background-color 0.16s ease; +} + +nav a:hover, +nav a:focus { + background: linear-gradient(180deg, #b12430 0%, #8f1b24 100%); + border-color: #7f1a23; + color: #fff; + transform: translateY(-1px); +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.45rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--paper); + border: 1px solid var(--border); + border-left: 5px solid var(--freebsd-red-1); + border-radius: var(--radius); + padding: 1.2rem 1rem 1rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #141619; + font-family: var(--font-heading); + margin: 1.1rem 0 0.7rem; + line-height: 1.3; + letter-spacing: 0.01em; +} + +h1 { font-size: clamp(1.75rem, 3.2vw, 2.25rem); } +h2 { font-size: clamp(1.4rem, 2.6vw, 1.8rem); } +h3 { font-size: clamp(1.16rem, 2vw, 1.42rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.95rem; +} + +a { + color: var(--link); + text-decoration-thickness: 1px; + text-underline-offset: 0.15em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.75rem; +} + +.meta { + margin: 0; + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.reading-time { + margin-top: 0.26rem; +} + +.summary { + margin-top: 0.75rem; + color: #3c4249; +} + +.post-content { + margin-top: 0.65rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 0.95rem 0; +} + +img { + max-width: 100%; + height: auto; + display: block; + border-radius: 4px; + border: 1px solid #b9c1ca; +} + +.image-caption, +figcaption { + margin-top: 0.35rem; + color: #626a73; + font-size: 0.78rem; + font-style: italic; +} + +blockquote { + margin: 1rem 0; + padding: 0.7rem 0.85rem; + border-left: 4px solid var(--freebsd-red-1); + background: var(--quote-bg); + color: #2e343b; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.84rem; +} + +code { + background: var(--code-bg); + border: 1px solid #d8dde3; + padding: 0.08rem 0.28rem; + border-radius: 3px; +} + +pre { + background: var(--code-bg); + border: 1px solid #d3d9df; + border-radius: 6px; + padding: 0.8rem; + overflow-x: auto; +} + +pre code { + background: none; + border: 0; + padding: 0; +} + +hr { + border: 0; + border-top: 1px solid #d0d6dd; + margin: 1.15rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.9rem; +} + +.tags a, +.tags-list a { + display: inline-block; + text-decoration: none; + color: var(--tag-text); + background: var(--tag-bg); + border: 1px solid #debec2; + border-radius: 999px; + padding: 0.18rem 0.52rem; + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #f2d4d8; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.tag-count { + color: #6e5d60; +} + +.posts-list article + article { + margin-top: 0.85rem; +} + +.posts-list h2 { + margin-top: 0; +} + +.pagination { + margin-top: 1.25rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.45rem; +} + +.pagination a { + display: inline-block; + background: #f0f3f6; + color: #1f252c; + border: 1px solid #c5cdd5; + border-radius: 5px; + text-decoration: none; + padding: 0.22rem 0.62rem; + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.pagination a:hover, +.pagination a:focus { + color: #fff; + background: linear-gradient(180deg, #ba2733 0%, #931b25 100%); + border-color: #801820; +} + +.page-info { + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.related-posts { + margin-top: 1.35rem; + padding-top: 0.85rem; + border-top: 1px dashed #bcc5ce; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.05rem; + color: #1f252b; +} + +.related-posts-list { + display: grid; + gap: 0.62rem; +} + +.related-post { + border: 1px solid #cfd6dd; + border-left: 4px solid var(--freebsd-red-1); + background: #f8fafc; + border-radius: 5px; + padding: 0.55rem 0.66rem; +} + +.related-post h4 { + margin: 0 0 0.22rem; + font-size: 0.95rem; +} + +.related-post p { + margin: 0; + color: #4b525a; + font-size: 0.9rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.92rem 0; +} + +th, +td { + border: 1px solid #d2d9e0; + padding: 0.4rem 0.52rem; +} + +th { + background: #e9edf2; + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +footer { + margin-top: 1.85rem; + padding: 0.9rem 0 0.1rem; + border-top: 2px solid #d1d7de; + color: #525861; + font-family: var(--font-mono); + font-size: 0.71rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +footer p { + margin: 0.33rem 0; +} + +footer a { + color: var(--link); +} + +footer a:hover, +footer a:focus { + color: var(--link-hover); +} + +@media (max-width: 840px) { + .container { + padding: 0 0.82rem 2rem; + } + + header { + margin: 0 -0.82rem 1.5rem; + padding: 0.92rem 0.82rem 0.82rem; + } + + nav { + padding-right: 0; + } + + header::before { + top: 10px; + right: 10px; + padding: 0.12rem 0.42rem; + font-size: 0.5rem; + letter-spacing: 0; + } + + article.post, + article.page, + .posts-list article { + padding: 0.95rem 0.8rem 0.82rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.02em; + } +} diff --git a/themes/gameboy/style.css b/themes/gameboy/style.css index 2d40020..ae028bd 100644 --- a/themes/gameboy/style.css +++ b/themes/gameboy/style.css @@ -2,9 +2,32 @@ * Game Boy Theme for BSSG * A clean retro theme inspired by the original Nintendo Game Boy, * with high readability while maintaining the nostalgic feel + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Game Boy aesthetics */ -@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms */ + .site-title a:hover, + nav a:hover { + transform: none !important; + } +} :root { /* Classic Game Boy palette */ @@ -23,10 +46,10 @@ --accent-color: #8bac0f; --border-color: #8bac0f; - /* Typography */ - --font-main: 'Press Start 2P', monospace; - --font-headings: 'Press Start 2P', monospace; - --font-mono: monospace; + /* Typography - Game Boy pixelated fonts with fallbacks */ + --font-main: 'Courier New', 'Lucida Console', 'Monaco', 'Consolas', 'Liberation Mono', monospace; + --font-headings: 'Courier New', 'Lucida Console', 'Monaco', 'Consolas', 'Liberation Mono', monospace; + --font-mono: 'Courier New', 'Lucida Console', 'Monaco', 'Consolas', 'Liberation Mono', monospace; /* Sizing */ --content-width: 90%; @@ -46,19 +69,21 @@ /* Base elements */ html { - font-size: 14px; /* Smaller base font size for pixelated font */ + font-size: 16px; /* Larger base font size for better Game Boy readability */ background-color: var(--gb-darkest); scroll-behavior: smooth; } body { font-family: var(--font-main); - line-height: 1.5; + line-height: 1.4; /* Tighter line height for pixelated look */ color: var(--text-color); background-color: var(--bg-color); margin: 0; padding: 0; overflow-x: hidden; + font-weight: bold; /* Bold text for authentic Game Boy appearance */ + text-rendering: optimizeSpeed; /* Crisp pixelated rendering */ } .container { @@ -76,7 +101,7 @@ body { overflow: hidden; /* Ensure contained elements don't break the Game Boy screen */ } -/* Enhanced header styling */ +/* Enhanced header styling with Game Boy power indicator */ header { text-align: center; margin-bottom: var(--spacing-xl); @@ -85,6 +110,19 @@ header { position: relative; } +/* Game Boy power indicator */ +header::before { + content: "● POWER"; + position: absolute; + top: -10px; + right: 10px; + font-size: 0.7rem; + color: var(--gb-light); + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; +} + header::after { content: ""; display: block; @@ -98,19 +136,22 @@ header::after { } .site-title { - font-size: 1.5rem; - letter-spacing: -1px; + font-size: 2rem; /* Larger, more prominent Game Boy title */ + letter-spacing: 0px; /* Remove negative spacing for pixelated look */ margin: 0 0 var(--spacing-md) 0; padding: var(--spacing-sm) 0; color: var(--heading-color); - text-shadow: 2px 2px 0 rgba(15, 56, 15, 0.2); + text-shadow: 2px 2px 0 rgba(15, 56, 15, 0.3); /* Stronger shadow for depth */ position: relative; display: inline-block; + font-weight: bold; + text-transform: uppercase; /* Game Boy style uppercase */ } +/* TEXT BROWSER FALLBACK: Simplify decorative elements */ .site-title::before, .site-title::after { - content: "〓"; + content: "*"; color: var(--gb-light); position: absolute; top: 50%; @@ -118,6 +159,15 @@ header::after { font-size: 1rem; } +/* Use Game Boy style decoration when positioning is supported */ +@supports (position: absolute) { + .site-title::before, + .site-title::after { + content: "▓"; /* More pixelated Game Boy style block */ + font-size: 1.2rem; + } +} + .site-title::before { left: -1.5rem; } @@ -139,14 +189,23 @@ header::after { transform: scale(1.05); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + transform: scale(1.05); +} + /* Site description */ header p { color: var(--text-color); - font-size: 0.7rem; + font-size: 0.9rem; /* Larger for better readability */ margin: 0 auto; max-width: 85%; opacity: 0.9; - line-height: 1.8; + line-height: 1.4; /* Consistent with body line height */ + font-weight: normal; /* Less bold for description */ } /* Improved Navigation buttons */ @@ -163,20 +222,22 @@ nav { nav a { display: inline-block; - padding: var(--spacing-xs) var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); /* Larger padding for Game Boy buttons */ background-color: var(--gb-light); color: var(--gb-darkest); text-decoration: none; - font-size: 0.7rem; - border: 2px solid var(--gb-dark); + font-size: 0.9rem; /* Larger font for better readability */ + border: 3px solid var(--gb-dark); /* Thicker border for Game Boy button look */ border-radius: 0; - box-shadow: 2px 2px 0 var(--gb-dark); + box-shadow: 3px 3px 0 var(--gb-dark); /* Stronger shadow for 3D effect */ transform: translateY(0); transition: all var(--animation-speed) ease-in-out; text-align: center; - min-width: 80px; + min-width: 100px; /* Wider buttons */ position: relative; overflow: hidden; + font-weight: bold; + text-transform: uppercase; /* Game Boy style uppercase */ } nav a::after { @@ -197,6 +258,16 @@ nav a:hover::after { nav a:hover, nav a.active { + background-color: var(--gb-dark); + color: var(--bg-color); + transform: translateY(3px); /* Deeper press effect */ + box-shadow: 0 0 0 var(--gb-dark); +} + +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; background-color: var(--gb-dark); color: var(--bg-color); transform: translateY(2px); @@ -218,23 +289,33 @@ h1, h2, h3, h4, h5, h6 { } h1 { - font-size: 1.3rem; - border-bottom: 2px solid var(--accent-color); + font-size: 1.6rem; /* Larger for Game Boy prominence */ + border-bottom: 3px solid var(--accent-color); /* Thicker border */ padding-bottom: var(--spacing-xs); + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; /* Spaced out for pixelated effect */ } h2 { - font-size: 1.1rem; + font-size: 1.3rem; /* Larger for better hierarchy */ + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; } h3 { - font-size: 1rem; + font-size: 1.1rem; /* Larger for readability */ + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; } p { margin: var(--spacing-md) 0; - font-size: 0.8rem; - line-height: 1.7; + font-size: 1rem; /* Larger for better readability */ + line-height: 1.4; /* Consistent pixelated spacing */ + font-weight: normal; /* Less bold for body text */ } /* Links */ @@ -249,6 +330,13 @@ a:hover { color: var(--link-hover); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); +} + /* Article styling */ article { margin-bottom: var(--spacing-xl); @@ -265,18 +353,21 @@ article h1 { flex-wrap: wrap; justify-content: center; gap: var(--spacing-md); - font-size: 0.7rem; + font-size: 0.9rem; /* Larger for readability */ margin: var(--spacing-md) 0 var(--spacing-lg); text-align: center; color: var(--text-color); - opacity: 0.7; + opacity: 0.8; /* Slightly more visible */ + font-weight: bold; + text-transform: uppercase; } /* Lists */ ul, ol { padding-left: 1.5rem; margin: var(--spacing-md) 0; - font-size: 0.8rem; + font-size: 1rem; /* Larger for readability */ + font-weight: normal; } li { @@ -286,34 +377,77 @@ li { /* Code blocks */ pre, code { font-family: var(--font-mono); + font-size: 0.9rem; /* Larger for readability */ + font-weight: bold; + border-radius: 0; /* Sharp corners for pixelated look */ +} + +/* Inline code styling */ +code { background-color: var(--gb-lightest); color: var(--gb-darkest); - border-radius: 2px; - font-size: 0.8rem; -} - -code { padding: 0.1em 0.3em; + border: 1px solid var(--gb-dark); /* Thinner border for inline code */ } +/* Code block styling */ pre { + background-color: var(--gb-lightest); + color: var(--gb-darkest); + border: 2px solid var(--gb-dark); /* Game Boy style border for blocks */ padding: var(--spacing-sm); overflow-x: auto; margin: var(--spacing-md) 0; } +/* Code inside pre blocks - remove all styling to prevent double borders */ pre code { padding: 0; background: transparent; + border: none; /* Remove border from code inside pre */ + font-size: inherit; } -/* Blockquotes */ +/* Blockquotes - Game Boy style */ blockquote { margin: var(--spacing-md) 0; - padding: var(--spacing-sm) var(--spacing-md); - border-left: 4px solid var(--accent-color); - background-color: rgba(139, 172, 15, 0.1); - font-style: italic; + padding: var(--spacing-md); + border: 3px solid var(--gb-dark); + background-color: var(--gb-lightest); + color: var(--gb-darkest); + font-style: normal; /* Remove italic for pixelated look */ + font-weight: bold; + border-radius: 0; /* Sharp corners for Game Boy style */ + box-shadow: 3px 3px 0 var(--gb-dark); + position: relative; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 0.9rem; +} + +/* Game Boy style quote decoration */ +blockquote::before { + content: "▓▓"; + position: absolute; + top: -3px; + left: -3px; + background-color: var(--gb-dark); + color: var(--gb-lightest); + padding: 2px 4px; + font-size: 0.7rem; + line-height: 1; +} + +/* TEXT BROWSER FALLBACK: Simple quote marks */ +@supports not (position: absolute) { + blockquote::before { + content: ">> "; + position: static; + background: none; + color: var(--gb-dark); + padding: 0; + font-size: 1rem; + } } blockquote p:first-child { @@ -338,15 +472,18 @@ img { figcaption, .caption { text-align: center; - font-size: 0.7rem; + font-size: 0.9rem; /* Larger for readability */ margin-top: var(--spacing-sm); padding: var(--spacing-xs); background-color: var(--gb-lightest); color: var(--gb-darkest); - border-radius: 2px; + border-radius: 0; /* Sharp corners for pixelated look */ + border: 2px solid var(--gb-dark); /* Game Boy style border */ display: inline-block; margin-left: auto; margin-right: auto; + font-weight: bold; + text-transform: uppercase; } /* Post list styling */ @@ -388,19 +525,32 @@ figcaption, .tags a { display: inline-block; - padding: var(--spacing-xs); - font-size: 0.65rem; + padding: var(--spacing-xs) var(--spacing-sm); /* More padding */ + font-size: 0.8rem; /* Larger for readability */ background-color: var(--gb-lightest); color: var(--gb-darkest); text-decoration: none; - border: 1px solid var(--gb-dark); + border: 2px solid var(--gb-dark); /* Thicker border */ border-radius: 0; transition: all var(--animation-speed) ease; position: relative; overflow: hidden; + font-weight: bold; + text-transform: uppercase; + box-shadow: 2px 2px 0 var(--gb-dark); /* Game Boy button shadow */ } .tags a:hover { + background-color: var(--gb-dark); + color: var(--gb-lightest); + transform: translateY(2px); /* Deeper press effect */ + box-shadow: 0 0 0 var(--gb-dark); /* Pressed shadow */ +} + +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; background-color: var(--gb-dark); color: var(--gb-lightest); transform: translateY(1px); @@ -426,26 +576,37 @@ footer { .pagination a { display: inline-block; - padding: var(--spacing-xs) var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); /* Larger padding */ background-color: var(--gb-light); color: var(--bg-color); text-decoration: none; - font-size: 0.7rem; - border: 2px solid var(--gb-dark); - box-shadow: 2px 2px 0 var(--gb-dark); + font-size: 0.9rem; /* Larger font */ + border: 3px solid var(--gb-dark); /* Thicker border */ + box-shadow: 3px 3px 0 var(--gb-dark); /* Stronger shadow */ transform: translateY(0); transition: all var(--animation-speed) ease-in-out; - min-width: 80px; + min-width: 100px; /* Wider buttons */ text-align: center; + font-weight: bold; + text-transform: uppercase; } .pagination a:hover { + background-color: var(--gb-dark); + transform: translateY(3px); /* Deeper press effect */ + box-shadow: 0 0 0 var(--gb-dark); +} + +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; background-color: var(--gb-dark); transform: translateY(2px); box-shadow: 0 0 0 var(--gb-dark); } -/* Screen scanlines effect */ +/* Enhanced Game Boy screen scanlines effect */ .container::before { content: ""; position: absolute; @@ -456,12 +617,13 @@ footer { background: linear-gradient( to bottom, - rgba(139, 172, 15, 0.02) 1px, + rgba(139, 172, 15, 0.05) 1px, /* More visible scanlines */ transparent 1px ); - background-size: 100% 4px; + background-size: 100% 3px; /* Tighter scanlines for authentic feel */ pointer-events: none; z-index: 1; + opacity: 0.7; /* More prominent effect */ } /* Enhanced reading time indicator */ @@ -475,11 +637,19 @@ footer { border-radius: 2px; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏲"; + content: "Time: "; margin-right: var(--spacing-xs); } +/* Use Game Boy style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏲"; + } +} + /* Better game boy hover effects */ a:not(.site-title a):not(nav a):not(.pagination a):not(.tags a)::after { content: ""; @@ -661,6 +831,14 @@ a:not(.site-title a):not(nav a):not(.pagination a):not(.tags a):hover::after { } blockquote { - background-color: rgba(15, 56, 15, 0.5); + background-color: var(--gb-darkest); + color: var(--gb-lightest); + border-color: var(--gb-light); + box-shadow: 3px 3px 0 var(--gb-light); + } + + blockquote::before { + background-color: var(--gb-light); + color: var(--gb-darkest); } } \ No newline at end of file diff --git a/themes/glassmorphism/style.css b/themes/glassmorphism/style.css index c73096b..67d30f7 100644 --- a/themes/glassmorphism/style.css +++ b/themes/glassmorphism/style.css @@ -2,43 +2,66 @@ * Glassmorphism Theme for BSSG * Modern frosted glass effect with blur, transparency, and subtle borders * Features: backdrop-filter blur, soft shadows, and subtle gradients + * IMPROVED: Better accessibility, performance, and text browser support */ +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects for reduced motion */ + .container, nav a, .glass-button, .glass-card { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + } + + /* Simplify hover effects */ + .featured-image:hover, .index-image:hover, .tag-image:hover, .archive-image:hover { + transform: none !important; + } +} + :root { - /* Glassmorphism color scheme - updated with cooler tones */ - --glass-bg: rgba(255, 255, 255, 0.1); - --glass-border: rgba(255, 255, 255, 0.2); - --glass-shadow: rgba(0, 0, 0, 0.1); - --glass-highlight: rgba(255, 255, 255, 0.05); + /* Glassmorphism color scheme - IMPROVED contrast ratios */ + --glass-bg: rgba(255, 255, 255, 0.20); /* MAXIMUM CONTRAST: Much higher opacity for better readability */ + --glass-border: rgba(255, 255, 255, 0.25); /* Increased opacity */ + --glass-shadow: rgba(0, 0, 0, 0.15); /* Increased for better definition */ + --glass-highlight: rgba(255, 255, 255, 0.08); --bg-color-1: #0F3443; --bg-color-2: #348F50; --bg-color-3: #56B4D3; - --text-color: rgba(255, 255, 255, 0.9); - --text-color-muted: rgba(255, 255, 255, 0.6); + --text-color: rgba(0, 0, 0, 0.9); /* MAXIMUM contrast: Dark text on light backgrounds */ + --text-color-muted: rgba(0, 0, 0, 0.8); /* MAXIMUM contrast: Slightly muted but still dark */ --accent-color: #00FFCC; --accent-color-hover: #99FFE6; - --link-color: rgba(0, 255, 204, 0.9); + --link-color: rgba(0, 255, 204, 0.95); /* Improved contrast */ --link-hover: rgba(150, 255, 230, 1); --title-gradient-start: rgba(0, 255, 204, 1); --title-gradient-end: rgba(56, 180, 211, 1); - /* Typography */ - --font-main: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-headings: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-mono: 'SF Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + /* Typography - Added fallbacks for text browsers */ + --font-main: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-headings: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'SF Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, 'Courier New', monospace; - /* Sizing */ + /* Sizing - OPTIMIZED blur amount */ --content-width: 1000px; - --blur-amount: 20px; + --blur-amount: 12px; /* Reduced from 20px for better performance */ + --blur-amount-light: 6px; /* For less critical elements */ --border-radius: 16px; --small-radius: 10px; } -/* Base elements with vibrant gradient background */ +/* Base elements with optimized gradient background */ body { font-family: var(--font-main); background: linear-gradient(135deg, var(--bg-color-1) 0%, var(--bg-color-2) 50%, var(--bg-color-3) 100%); - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ color: var(--text-color); margin: 0; padding: 20px; @@ -48,13 +71,14 @@ body { min-height: 100vh; } -/* Container with glass morphism effect */ +/* OPTIMIZED: Container with reduced glass morphism effect */ .container { max-width: var(--content-width); margin: 40px auto; padding: 0; position: relative; background: var(--glass-bg); + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ backdrop-filter: blur(var(--blur-amount)); -webkit-backdrop-filter: blur(var(--blur-amount)); border: 1px solid var(--glass-border); @@ -63,22 +87,41 @@ body { overflow: hidden; } -/* Gradient glass highlight effect */ +/* FALLBACK: For browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(1px)) { + .container { + background: rgba(255, 255, 255, 0.9); /* Light solid fallback background for better contrast */ + } + + nav a { + background: rgba(255, 255, 255, 0.8); + } + + .glass-button { + background: rgba(255, 255, 255, 0.8); + } + + .glass-card { + background: rgba(255, 255, 255, 0.8); + } +} + +/* OPTIMIZED: Simplified gradient glass highlight effect */ .container::before { content: ""; position: absolute; top: 0; left: 0; right: 0; - height: 100px; + height: 80px; /* Reduced height for performance */ background: linear-gradient(to bottom, var(--glass-highlight), transparent); pointer-events: none; border-radius: var(--border-radius) var(--border-radius) 0 0; } -/* Header with deeper glass effect */ +/* Header with optimized glass effect */ header { - background: rgba(255, 255, 255, 0.05); + background: rgba(255, 255, 255, 0.20); /* MAXIMUM CONTRAST: Much higher opacity for better readability */ padding: 30px 40px; position: relative; border-bottom: 1px solid var(--glass-border); @@ -93,7 +136,7 @@ header h1 { opacity: 0.95; } -/* Site title with glassmorphic gradient effect */ +/* Site title with improved glassmorphic gradient effect */ .site-title { margin: 0; padding: 0; @@ -123,14 +166,21 @@ header h1 { border-bottom: none; } -/* Fallback for browsers that don't support background-clip */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-bottom: none; +} + +/* FALLBACK: For browsers that don't support background-clip */ @supports not (background-clip: text) { .site-title a { - color: var(--accent-color); + color: var(--accent-color) !important; + background: none !important; } } -/* Navigation with glass buttons */ +/* OPTIMIZED: Navigation with reduced glass effects */ nav { display: flex; flex-wrap: nowrap; @@ -139,7 +189,7 @@ nav { justify-content: flex-end; position: relative; gap: 12px; - background: rgba(255, 255, 255, 0.03); + background: rgba(255, 255, 255, 0.05); /* Increased opacity */ } nav a { @@ -152,43 +202,48 @@ nav a { display: inline-block; border-radius: 30px; background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ + backdrop-filter: blur(var(--blur-amount-light)); /* Reduced blur for nav items */ + -webkit-backdrop-filter: blur(var(--blur-amount-light)); border: 1px solid var(--glass-border); transition: all 0.2s ease; white-space: nowrap; } nav a:hover, nav a:focus { - background: rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.18); /* Increased opacity for better contrast */ transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + /* IMPROVED: Focus outline for accessibility */ + outline: 2px solid var(--accent-color); + outline-offset: 2px; } /* Active navigation with accent glow */ nav a.active { - background: rgba(0, 204, 255, 0.15); - border-color: rgba(0, 204, 255, 0.3); + background: rgba(0, 204, 255, 0.2); /* Increased opacity */ + border-color: rgba(0, 204, 255, 0.4); /* Increased opacity */ box-shadow: 0 0 15px rgba(0, 204, 255, 0.3); } /* RSS in accent color */ nav a:last-child { - background: rgba(0, 204, 255, 0.2); - border-color: rgba(0, 204, 255, 0.3); + background: rgba(0, 204, 255, 0.25); /* Increased opacity */ + border-color: rgba(0, 204, 255, 0.4); /* Increased opacity */ } nav a:last-child:hover { - background: rgba(0, 204, 255, 0.3); + background: rgba(0, 204, 255, 0.35); /* Increased opacity */ } /* Content area with glass effect */ main { padding: 40px; position: relative; + background: rgba(255, 255, 255, 0.15); /* IMPROVED: Better background for content readability */ } -/* Typography with clear modern style */ +/* Typography with improved contrast */ h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); margin-top: 2rem; @@ -207,21 +262,22 @@ h2 { font-size: 2rem; } +.posts-list h2, h3 { font-size: 1.5rem; } p { + margin-top: 0; margin-bottom: 1.5rem; - opacity: 0.9; + color: var(--text-color); /* Improved contrast */ } -/* Elegant links with subtle hover effect */ a { color: var(--link-color); text-decoration: none; - transition: all 0.2s; - border-bottom: 1px solid transparent; + border-bottom: 1px solid rgba(0, 255, 204, 0.3); + transition: all 0.2s ease; } a:hover { @@ -229,22 +285,27 @@ a:hover { border-bottom-color: var(--link-hover); } -/* Articles with glass cards */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + border-bottom-color: var(--link-hover); +} + article { - margin-bottom: 30px; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); + margin-bottom: 3rem; + padding: 2rem; + background: rgba(255, 255, 255, 0.25); /* MAXIMUM CONTRAST: Much higher opacity for text readability */ border: 1px solid var(--glass-border); border-radius: var(--small-radius); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); - overflow: hidden; - transition: transform 0.2s, box-shadow 0.2s; + /* OPTIMIZED: Reduced shadow complexity */ + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; } article:hover { - transform: translateY(-5px); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } article:last-child { @@ -252,125 +313,132 @@ article:last-child { } article h1 { - font-size: 1.8rem; - margin: 0; - padding: 25px 30px 15px; + margin-top: 0; + color: var(--text-color); } article .article-content { - padding: 0 30px 30px; + color: var(--text-color); /* Improved contrast */ } article .meta { + color: rgba(255, 255, 255, 1); /* WHITE text on dark background for maximum contrast */ font-size: 0.9rem; - margin-bottom: 20px; + margin-bottom: 1.5rem; display: flex; flex-wrap: wrap; - gap: 15px; - padding: 0 30px 15px; - color: var(--text-color-muted); - border-bottom: 1px solid var(--glass-border); + gap: 1rem; + /* MAXIMUM CONTRAST: Very strong background for perfect metadata visibility */ + background: rgba(0, 0, 0, 0.8); + padding: 0.75rem 1rem; + border-radius: var(--small-radius); + border: 2px solid rgba(255, 255, 255, 0.4); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } + + .reading-time { - opacity: 0.7; + color: rgba(255, 255, 255, 1); /* WHITE text on dark meta background for maximum contrast */ + font-weight: 500; /* IMPROVED: Make it slightly bolder */ } .tags { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 0.5rem; + margin-top: 1rem; } .tags a { - background: rgba(255, 255, 255, 0.1); - color: var(--text-color); - padding: 4px 12px; - font-size: 0.75rem; + background: rgba(0, 0, 0, 0.8); /* Dark background for better contrast */ + color: rgba(0, 255, 204, 1); /* Bright cyan text on dark background */ + padding: 0.25rem 0.75rem; border-radius: 20px; - border: 1px solid var(--glass-border); - transition: all 0.2s; + font-size: 0.8rem; + border: 1px solid rgba(0, 255, 204, 0.5); + border-bottom: 1px solid rgba(0, 255, 204, 0.5); /* Consistent border */ + transition: all 0.2s ease; + font-weight: 500; /* Make text slightly bolder for better readability */ } .tags a:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.3); - transform: translateY(-2px); + background: rgba(0, 0, 0, 0.9); /* Darker background on hover */ + color: rgba(0, 255, 204, 1); /* Keep bright cyan text */ + transform: translateY(-1px); + border: 1px solid rgba(0, 255, 204, 0.8); /* Brighter border on hover */ + border-bottom: 1px solid rgba(0, 255, 204, 0.8); /* Consistent border */ + box-shadow: 0 2px 8px rgba(0, 255, 204, 0.3); /* Add cyan glow on hover */ } .tags-list { - list-style-type: none; - padding: 0; display: flex; flex-wrap: wrap; - gap: 10px; + gap: 1rem; + margin-bottom: 2rem; } .tag-count { - background: rgba(0, 204, 255, 0.2); - color: var(--text-color); - font-size: 0.7em; - margin-left: 5px; - padding: 2px 8px; - border-radius: 10px; + background: rgba(0, 255, 204, 0.2); /* Increased opacity */ + color: var(--accent-color); + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + margin-left: 0.5rem; } -/* Code with frosted glass effect */ code { - font-family: var(--font-mono); - background: rgba(0, 0, 0, 0.2); - color: var(--text-color); - padding: 3px 6px; - font-size: 0.9em; + background: rgba(0, 0, 0, 0.4); /* Improved contrast */ + color: var(--accent-color); + padding: 0.2rem 0.5rem; border-radius: 4px; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); + font-family: var(--font-mono); + font-size: 0.9rem; + border: 1px solid rgba(0, 255, 204, 0.2); } pre { - background: rgba(0, 0, 0, 0.2); - padding: 20px; - overflow-x: auto; - font-size: 0.9em; + background: rgba(0, 0, 0, 0.5); /* Improved contrast */ + color: var(--text-color); + padding: 1.5rem; border-radius: var(--small-radius); - margin: 20px 0; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - border: 1px solid rgba(255, 255, 255, 0.1); + overflow-x: auto; + border: 1px solid var(--glass-border); + margin: 1.5rem 0; } pre code { - background: transparent; + background: none; + border: none; padding: 0; - border-radius: 0; - backdrop-filter: none; - -webkit-backdrop-filter: none; + color: inherit; } img { max-width: 100%; height: auto; - border-radius: 8px; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); - transition: transform 0.2s; + border-radius: var(--small-radius); + margin: 1rem 0; } img:hover { - transform: scale(1.03); + transform: scale(1.02); + transition: transform 0.3s ease; } -/* Featured image with glass effect */ +/* OPTIMIZED: Featured image with reduced glass effects */ .featured-image { - margin: 30px 0; position: relative; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - border: 1px solid var(--glass-border); - border-radius: var(--small-radius); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + margin: 2rem 0; + border-radius: var(--border-radius); overflow: hidden; - transition: all 0.3s ease; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); } .featured-image::before { @@ -378,51 +446,52 @@ img:hover { position: absolute; top: 0; left: 0; - width: 100%; - height: 4px; - background: linear-gradient(to right, var(--title-gradient-start), var(--title-gradient-end)); - z-index: 2; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.1) 100%); + pointer-events: none; + z-index: 1; } .featured-image img { width: 100%; height: auto; display: block; - transition: transform 0.5s ease; - border: none; - margin: 0; + transition: all 0.3s ease; + /* OPTIMIZED: Simplified filter effects */ + filter: brightness(1.05) contrast(1.05); } .featured-image:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); + transform: translateY(-4px); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); } .featured-image:hover img { - transform: scale(1.02); + transform: scale(1.02); /* Reduced scale for better performance */ } .featured-image .image-caption { - padding: 15px; - color: var(--text-color-muted); + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.7); /* Improved contrast */ + color: var(--text-color); + padding: 1rem; font-size: 0.9rem; - border-top: 1px solid var(--glass-border); - background: rgba(0, 0, 0, 0.1); - backdrop-filter: blur(var(--blur-amount)); - -webkit-backdrop-filter: blur(var(--blur-amount)); + z-index: 2; } +/* OPTIMIZED: Index image with reduced effects */ .index-image { - margin: 30px 0; position: relative; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - border: 1px solid var(--glass-border); + margin: 1.5rem 0; border-radius: var(--small-radius); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); overflow: hidden; - transition: all 0.3s ease; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .index-image::before { @@ -430,41 +499,38 @@ img:hover { position: absolute; top: 0; left: 0; - width: 100%; - height: 4px; - background: linear-gradient(to right, rgba(150, 255, 230, 1), rgba(0, 150, 200, 1)); - z-index: 2; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.05) 100%); + pointer-events: none; + z-index: 1; } .index-image img { width: 100%; height: auto; display: block; - transition: transform 0.5s ease; - border: none; - margin: 0; + transition: all 0.3s ease; + filter: brightness(1.02) contrast(1.02); } .index-image:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); } .index-image:hover img { - transform: scale(1.02); + transform: scale(1.01); } .tag-image { - margin: 30px 0; position: relative; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - border: 1px solid var(--glass-border); + margin: 1.5rem 0; border-radius: var(--small-radius); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); overflow: hidden; - transition: all 0.3s ease; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .tag-image::before { @@ -472,41 +538,38 @@ img:hover { position: absolute; top: 0; left: 0; - width: 100%; - height: 4px; - background: linear-gradient(to right, rgba(0, 204, 255, 1), rgba(0, 255, 204, 1)); - z-index: 2; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.05) 100%); + pointer-events: none; + z-index: 1; } .tag-image img { width: 100%; height: auto; display: block; - transition: transform 0.5s ease; - border: none; - margin: 0; + transition: all 0.3s ease; + filter: brightness(1.02) contrast(1.02); } .tag-image:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); } .tag-image:hover img { - transform: scale(1.02); + transform: scale(1.01); } .archive-image { - margin: 30px 0; position: relative; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - border: 1px solid var(--glass-border); + margin: 1.5rem 0; border-radius: var(--small-radius); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); overflow: hidden; - transition: all 0.3s ease; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .archive-image::before { @@ -514,220 +577,207 @@ img:hover { position: absolute; top: 0; left: 0; - width: 100%; - height: 4px; - background: linear-gradient(to right, rgba(150, 110, 255, 1), rgba(0, 204, 255, 1)); - z-index: 2; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.05) 100%); + pointer-events: none; + z-index: 1; } .archive-image img { width: 100%; height: auto; display: block; - transition: transform 0.5s ease; - border: none; - margin: 0; + transition: all 0.3s ease; + filter: brightness(1.02) contrast(1.02); } .archive-image:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); } .archive-image:hover img { - transform: scale(1.02); + transform: scale(1.01); } -/* Footer with glass effect */ footer { - background: rgba(0, 0, 0, 0.1); - backdrop-filter: blur(var(--blur-amount)); - -webkit-backdrop-filter: blur(var(--blur-amount)); - border-top: 1px solid var(--glass-border); - color: var(--text-color); - padding: 30px 40px; - font-size: 0.9rem; + background: rgba(255, 255, 255, 0.05); + padding: 2rem 40px; text-align: center; - display: flex; - justify-content: space-between; - align-items: center; - border-radius: 0 0 var(--border-radius) var(--border-radius); + border-top: 1px solid var(--glass-border); + color: var(--text-color-muted); + font-size: 0.9rem; } footer a { - color: var(--text-color); - opacity: 0.8; - transition: opacity 0.2s; + color: var(--accent-color); + border-bottom: 1px solid rgba(0, 255, 204, 0.3); } footer a:hover { - opacity: 1; - border-bottom-color: var(--text-color); + color: var(--link-hover); + border-bottom-color: var(--link-hover); } -/* Pagination with glass buttons */ .pagination { display: flex; justify-content: center; align-items: center; - margin: 40px 0 10px; - gap: 15px; + gap: 1rem; + margin: 2rem 0; } .pagination a { - color: var(--text-color); - padding: 10px 20px; - text-decoration: none; - border-radius: 30px; background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); + color: var(--text-color); + padding: 0.75rem 1.5rem; + border-radius: 30px; border: 1px solid var(--glass-border); - transition: all 0.2s; + transition: all 0.2s ease; font-weight: 500; + border-bottom: 1px solid var(--glass-border); /* Consistent border */ } .pagination a:hover { background: rgba(255, 255, 255, 0.15); transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-bottom: 1px solid var(--glass-border); /* Consistent border */ } .pagination .page-info { color: var(--text-color-muted); - font-size: 0.9rem; + font-weight: 500; } -/* Custom glass morphism button */ +/* OPTIMIZED: Glass button component with reduced effects */ .glass-button { - display: inline-block; - color: var(--text-color); background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); - padding: 10px 20px; - border-radius: 30px; + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ + backdrop-filter: blur(var(--blur-amount-light)); + -webkit-backdrop-filter: blur(var(--blur-amount-light)); border: 1px solid var(--glass-border); + border-radius: var(--small-radius); + padding: 0.75rem 1.5rem; + color: var(--text-color); + text-decoration: none; + transition: all 0.2s ease; + display: inline-block; font-weight: 500; - font-size: 0.9rem; - transition: all 0.2s; - cursor: pointer; - text-align: center; } .glass-button:hover { background: rgba(255, 255, 255, 0.15); transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } -/* Glass card with hover effect */ +/* OPTIMIZED: Glass card component with reduced effects */ .glass-card { background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ + backdrop-filter: blur(var(--blur-amount-light)); + -webkit-backdrop-filter: blur(var(--blur-amount-light)); border: 1px solid var(--glass-border); - border-radius: var(--small-radius); - padding: 25px; - margin-bottom: 20px; - transition: transform 0.2s, box-shadow 0.2s; + border-radius: var(--border-radius); + padding: 1.5rem; + margin: 1rem 0; + transition: all 0.3s ease; } .glass-card:hover { - transform: translateY(-5px); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { + body { + padding: 10px; + /* OPTIMIZED: Simplified background on mobile */ + background: linear-gradient(135deg, var(--bg-color-1) 0%, var(--bg-color-2) 100%); + } + + .container { + margin: 20px auto; + border-radius: var(--small-radius); + /* OPTIMIZED: Remove backdrop-filter on mobile for better performance */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(255, 255, 255, 0.9); /* Light solid background for mobile with maximum contrast */ + } + + header, main, footer { + padding: 20px; + } + + .site-title { + font-size: 2rem; + } + + nav { + flex-direction: column; + align-items: center; + gap: 8px; + padding: 16px 20px; + } + + nav a { + width: 100%; + text-align: center; + padding: 12px 16px; + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(255, 255, 255, 0.1); + } + + article h1 { + font-size: 1.8rem; + } + + article .meta { + flex-direction: column; + gap: 0.5rem; + } + + article .article-content { + font-size: 1rem; + } + + footer { + padding: 20px; + } +} + +@media (max-width: 480px) { body { padding: 5px; } - + .container { - margin: 10px 5px; - width: calc(100% - 10px); - max-width: 100%; - border-radius: 12px; + margin: 10px auto; + border-radius: 8px; } - - header, main, footer { - padding: 15px; - width: 100%; - box-sizing: border-box; - } - - header h1 { - font-size: 2rem; - word-break: break-word; - } - + nav { - padding: 10px 15px; - justify-content: center; - flex-wrap: wrap; - gap: 8px; - width: 100%; - box-sizing: border-box; + padding: 12px 16px; } - + nav a { - padding: 8px 16px; + padding: 10px 12px; font-size: 0.85rem; - flex-grow: 0; - text-align: center; } - - article h1 { - font-size: 1.5rem; - padding: 15px 15px 10px; - word-break: break-word; - } - - article .meta { - padding: 0 15px 10px; - } - - article .article-content { - padding: 0 15px 15px; - } - - footer { - flex-direction: column; - gap: 15px; - } -} - -/* Additional breakpoint for very small screens */ -@media (max-width: 480px) { - body { - padding: 0; - } - - .container { - margin: 0; - width: 100%; - border-radius: 0; - } - - nav { - flex-direction: column; - align-items: stretch; - } - - nav a { - width: 100%; - margin: 4px 0; - text-align: center; - box-sizing: border-box; - } - + .pagination { flex-direction: column; - gap: 10px; + gap: 0.5rem; } - + .pagination a { width: 100%; + text-align: center; + padding: 12px; } } diff --git a/themes/ios/style.css b/themes/ios/style.css index ddc0726..982586a 100644 --- a/themes/ios/style.css +++ b/themes/ios/style.css @@ -1,6 +1,7 @@ /* * iOS Theme for BSSG * Styled after the modern iOS 17 interface with Dynamic Island elements + * Enhanced with accessibility, performance, and compatibility improvements */ :root { @@ -36,7 +37,7 @@ --font-mono: 'SF Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; /* Sizing and spacing */ - --content-width: 700px; + --content-width: 900px; --radius: 14px; --radius-sm: 10px; --radius-lg: 20px; @@ -50,6 +51,38 @@ --spacing-xl: 2rem; } +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + :root { + --transition: 0.01s; + } + + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + .ios-card:hover, + header:hover, + .featured-image:hover, + .featured-image:hover img, + img:hover, + .posts-list article:hover, + .site-title a:hover, + .ios-button:hover, + .tags a:hover, + .pagination a:hover { + transform: none !important; + } + + .dynamic-island { + opacity: 0 !important; + transform: none !important; + } +} + /* Base elements */ html { font-size: 16px; @@ -76,6 +109,14 @@ body { background-color: transparent; } +/* Desktop-specific improvements */ +@media (min-width: 1024px) { + .container { + max-width: 1000px; + padding: 0 var(--spacing-lg); + } +} + /* Card styling for content */ .ios-card { background-color: var(--card-bg); @@ -109,7 +150,7 @@ header:hover { box-shadow: var(--floating-shadow); } -/* iOS style site title with gradient */ +/* iOS style site title with enhanced accessibility */ .site-title { font-family: var(--font-headings); font-weight: 700; @@ -123,34 +164,41 @@ header:hover { .site-title a { text-decoration: none; - background: linear-gradient(135deg, var(--title-gradient-start), var(--title-gradient-end)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--accent-color); display: inline-block; transition: all var(--transition); position: relative; + outline: 2px solid transparent; + outline-offset: 2px; } -.site-title a:hover { - transform: translateY(-1px) scale(1.01); - background: linear-gradient(135deg, var(--title-gradient-end), var(--title-gradient-start)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} - -/* Fallback for browsers that don't support background-clip */ -@supports not (background-clip: text) { +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { .site-title a { - color: var(--accent-color); + background: linear-gradient(135deg, var(--title-gradient-start), var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; } .site-title a:hover { - color: var(--teal); + background: linear-gradient(135deg, var(--title-gradient-end), var(--title-gradient-start)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + transform: translateY(-1px) scale(1.01); } } +/* Fallback for browsers without gradient text support */ +.site-title a:hover { + color: var(--teal); +} + +.site-title a:focus { + outline-color: var(--accent-color); +} + header h1 { font-family: var(--font-headings); font-weight: 700; @@ -199,12 +247,19 @@ header:hover .dynamic-island { padding: var(--spacing-xs); margin: 0 auto var(--spacing-sm); transition: color var(--transition); + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: var(--button-radius); } .menu-toggle:hover, .menu-toggle:focus { color: var(--link-active); } +.menu-toggle:focus { + outline-color: var(--accent-color); +} + /* Navigation - iOS style tab bar */ nav { display: flex; @@ -213,12 +268,16 @@ nav { border-radius: var(--radius); margin-bottom: var(--spacing-lg); box-shadow: var(--card-shadow); - justify-content: space-around; + justify-content: center; align-items: center; border: 1px solid var(--border-color); position: relative; z-index: 2; transition: all var(--transition); + width: 100%; + box-sizing: border-box; + flex-wrap: wrap; + gap: 4px; } nav:hover { @@ -232,10 +291,14 @@ nav a { font-size: 0.9rem; transition: all var(--transition); text-align: center; - flex-grow: 1; - padding: var(--spacing-sm) 0; + flex: 0 1 auto; + padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--button-radius); position: relative; + outline: 2px solid transparent; + outline-offset: 2px; + white-space: nowrap; + margin: 0 2px; } nav a::after { @@ -255,6 +318,10 @@ nav a:hover, nav a.active { background-color: rgba(0, 122, 255, 0.1); } +nav a:focus { + outline-color: var(--accent-color); +} + nav a:hover::after, nav a.active::after { width: 30%; } @@ -295,18 +362,24 @@ p { line-height: 1.6; } -/* Links */ +/* Links with enhanced focus states */ a { color: var(--link-color); text-decoration: none; transition: all var(--transition); position: relative; + outline: 2px solid transparent; + outline-offset: 2px; } a:hover, a:active { color: var(--link-active); } +a:focus { + outline-color: var(--link-color); +} + /* iOS-style buttons */ .ios-button { display: inline-block; @@ -322,6 +395,8 @@ a:hover, a:active { cursor: pointer; text-align: center; -webkit-tap-highlight-color: transparent; /* Remove tap highlight on iOS */ + outline: 2px solid transparent; + outline-offset: 2px; } .ios-button:hover, .ios-button:active { @@ -332,6 +407,10 @@ a:hover, a:active { box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3); } +.ios-button:focus { + outline-color: var(--accent-color); +} + .ios-button.secondary { background-color: rgba(0, 122, 255, 0.1); color: var(--accent-color); @@ -342,6 +421,10 @@ a:hover, a:active { box-shadow: 0 4px 12px rgba(0, 122, 255, 0.15); } +.ios-button.secondary:focus { + outline-color: var(--accent-color); +} + .ios-button.destructive { background-color: var(--red); } @@ -351,6 +434,10 @@ a:hover, a:active { box-shadow: 0 4px 12px rgba(255, 59, 48, 0.3); } +.ios-button.destructive:focus { + outline-color: var(--red); +} + /* Article styling */ article { background-color: var(--card-bg); @@ -385,6 +472,14 @@ article .meta { margin-right: var(--spacing-xs); } +/* Text browser fallback for reading time icon */ +@supports not (content: "⏱") { + .reading-time::before { + content: "Time: "; + margin-right: var(--spacing-xs); + } +} + .tags { display: flex; flex-wrap: wrap; @@ -402,12 +497,19 @@ article .meta { text-decoration: none; transition: all var(--transition); border: 1px solid rgba(0, 122, 255, 0.2); + outline: 2px solid transparent; + outline-offset: 2px; } .tags a:hover, .tags a:active { background-color: rgba(0, 122, 255, 0.2); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 122, 255, 0.2); + text-decoration: none; +} + +.tags a:focus { + outline-color: var(--accent-color); } .tags-list { @@ -499,6 +601,13 @@ footer p { footer a { color: var(--accent-color); + outline: 2px solid transparent; + outline-offset: 2px; + transition: outline-color var(--transition); +} + +footer a:focus { + outline-color: var(--accent-color); } /* Date header for post lists */ @@ -523,16 +632,16 @@ footer a { box-shadow: var(--floating-shadow); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: var(--spacing-xs); } -.posts-list h3 a { +.posts-list h2 a { color: var(--header-color); } -.posts-list h3 a:hover, .posts-list h3 a:active { +.posts-list h2 a:hover, .posts-list h2 a:active { color: var(--accent-color); } @@ -690,12 +799,19 @@ hr { align-items: center; justify-content: center; min-width: 100px; + outline: 2px solid transparent; + outline-offset: 2px; } .pagination a:hover, .pagination a:active { background-color: rgba(0, 122, 255, 0.1); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 122, 255, 0.2); + text-decoration: none; +} + +.pagination a:focus { + outline-color: var(--accent-color); } .pagination .page-info { @@ -741,14 +857,20 @@ hr { padding: var(--spacing-sm) var(--spacing-md); color: var(--text-color); font-size: 0.9rem; - backdrop-filter: blur(5px); - -webkit-backdrop-filter: blur(5px); position: absolute; bottom: 0; left: 0; right: 0; } +/* Optimized backdrop-filter with fallbacks */ +@supports (backdrop-filter: blur(5px)) { + .featured-image .image-caption { + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + } +} + /* Lists with iOS styling */ ul, ol { padding-left: 1.5rem; @@ -787,38 +909,25 @@ ul li, ol li { font-size: 1.1rem; } - /* Mobile navigation */ + /* Mobile navigation - always visible */ .menu-toggle { - display: block; - width: 100%; - background-color: var(--card-bg); - border-radius: var(--radius); - box-shadow: var(--card-shadow); - padding: var(--spacing-sm); - color: var(--accent-color); - font-weight: 600; - margin-bottom: var(--spacing-sm); - border: 1px solid var(--border-color); + display: none; } nav { - max-height: 0; - overflow: hidden; - padding: 0 var(--spacing-sm); flex-direction: column; - transition: max-height var(--transition), padding var(--transition); - margin-bottom: var(--spacing-sm); - } - - nav.open { - max-height: 300px; padding: var(--spacing-sm); + margin-bottom: var(--spacing-lg); + gap: 0; } nav a { - padding: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); width: 100%; - margin: var(--spacing-xs) 0; + margin: 2px 0; + flex: none; + border-radius: var(--button-radius); + text-align: center; } .dynamic-island { diff --git a/themes/italy/style.css b/themes/italy/style.css index 7d71b31..d98c459 100644 --- a/themes/italy/style.css +++ b/themes/italy/style.css @@ -1,3 +1,35 @@ +/* + * Italy Theme for BSSG + * Authentic Italian design with rich typography and elegant aesthetics + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Italian design aesthetics + */ + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms */ + .site-title a:hover, + nav a:hover, + .tags a:hover, + .pagination a:hover { + transform: none !important; + } +} + :root { /* Authentic Italian color palette */ --bg-color: #fffbf0; /* Venetian plaster */ @@ -27,11 +59,11 @@ --venice-gold: #e6b954; --verona-pink: #e8a598; - /* Typography - distinctly Italian */ - --font-main: 'Cardo', 'Palatino Linotype', 'Book Antiqua', Georgia, serif; - --font-headings: 'Cinzel', 'Trajan Pro', 'Times New Roman', serif; - --font-decorative: 'Cormorant Garamond', 'Garamond', serif; - --font-mono: 'Fira Code', 'Consolas', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Palatino Linotype', 'Book Antiqua', 'Palatino', Georgia, 'Times New Roman', Times, serif; + --font-headings: 'Times New Roman', Times, Georgia, 'Palatino', serif; + --font-decorative: 'Garamond', Georgia, 'Times New Roman', Times, serif; + --font-mono: 'Consolas', 'Liberation Mono', 'Courier New', Courier, monospace; /* Spacing and sizing */ --radius: 8px; @@ -78,8 +110,7 @@ } } -/* Properly import Google Fonts */ -@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Cardo:ital,wght@0,400;0,700;1,400&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Fira+Code&display=swap'); +/* REMOVED: External font loading for better performance and text browser compatibility */ *, *::before, *::after { box-sizing: border-box; @@ -185,6 +216,14 @@ header::after { text-shadow: 0 2px 4px rgba(0,0,0,0.1); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + transform: translateY(-2px); + text-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + /* Italian decorative element for site title */ .site-title a::after { content: ""; @@ -293,6 +332,13 @@ nav a:hover { color: var(--link-hover-color); } +/* ACCESSIBILITY: Enhanced focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + nav a:hover::after, nav a.active::after { transform: scaleX(1); transform-origin: left; @@ -369,6 +415,14 @@ a:hover { text-decoration-color: var(--link-hover-color); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); + text-decoration-color: var(--link-hover-color); +} + /* Article styling inspired by Renaissance art */ article { margin-bottom: var(--spacing-xxl); @@ -402,6 +456,38 @@ article .meta { align-items: center; } +/* Page meta styling for post pages */ +.page-meta { + font-family: var(--font-decorative); + margin-bottom: var(--spacing-lg); +} + +.page-meta .meta { + color: var(--date-color); + font-style: italic; + margin: 0 0 var(--spacing-xs) 0; + font-size: 0.9rem; +} + +.page-meta .meta:first-child { + font-size: 0.95rem; + margin-bottom: var(--spacing-sm); +} + +.page-meta .meta time { + font-weight: 500; +} + +.page-meta .meta strong { + color: var(--text-color); + font-weight: 600; +} + +.page-meta .reading-time { + font-size: 0.85rem; + margin: 0; +} + .reading-time { font-style: normal; color: var(--date-color); @@ -410,12 +496,20 @@ article .meta { align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏱"; + content: "Time: "; margin-right: var(--spacing-xs); font-style: normal; } +/* Use Italian-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags with Italian color influence */ .tags { display: flex; @@ -443,6 +537,16 @@ article .meta { box-shadow: 0 3px 6px rgba(0,0,0,0.1); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-text); + color: var(--tag-bg); + transform: translateY(-2px); + box-shadow: 0 3px 6px rgba(0,0,0,0.1); +} + /* Featured image with Renaissance framing */ .featured-image { margin: var(--spacing-lg) 0; @@ -497,7 +601,7 @@ blockquote { } blockquote::before { - content: """; + content: "\201C"; font-family: serif; font-size: 3rem; position: absolute; @@ -634,6 +738,16 @@ footer p { box-shadow: 0 3px 6px rgba(0,0,0,0.1); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); + color: var(--bg-color); + transform: translateY(-2px); + box-shadow: 0 3px 6px rgba(0,0,0,0.1); +} + .pagination .page-info { color: var(--date-color); font-style: italic; @@ -873,18 +987,7 @@ footer p { } } -/* JavaScript for mobile menu toggle */ -/* -document.addEventListener('DOMContentLoaded', function() { - const menuToggle = document.querySelector('.menu-toggle'); - const nav = document.querySelector('nav'); - - if (menuToggle && nav) { - menuToggle.addEventListener('click', function() { - nav.classList.toggle('open'); - menuToggle.setAttribute('aria-expanded', - nav.classList.contains('open') ? 'true' : 'false'); - }); - } -}); -*/ +/* + * Note: Mobile menu toggle functionality would require JavaScript + * This theme uses CSS-only responsive navigation for better compatibility + */ diff --git a/themes/liquid-glass/style.css b/themes/liquid-glass/style.css new file mode 100644 index 0000000..1ba5dd6 --- /dev/null +++ b/themes/liquid-glass/style.css @@ -0,0 +1,610 @@ +/* + * Liquid Glass Theme for BSSG + * Accessibility-first translucent design with high-contrast reading surfaces. + */ + +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); + +:root { + --page-bg: #0b101a; + --page-bg-2: #15213a; + --glow-a: rgba(10, 132, 255, 0.28); + --glow-b: rgba(191, 90, 242, 0.18); + --glass-surface: rgba(18, 26, 42, 0.9); + --glass-surface-strong: rgba(21, 31, 50, 0.95); + --glass-panel: rgba(27, 39, 62, 0.9); + --glass-border: rgba(173, 202, 242, 0.26); + --glass-border-strong: rgba(199, 221, 250, 0.38); + --text: #f4f7ff; + --text-muted: #c9d4ea; + --heading: #ffffff; + --link: #9dcfff; + --link-hover: #cde7ff; + --accent: #64d2ff; + --accent-2: #0a84ff; + --shadow-lg: 0 20px 48px rgba(2, 8, 20, 0.5); + --shadow-sm: 0 10px 24px rgba(2, 8, 20, 0.35); + --quote-bg: rgba(17, 31, 52, 0.86); + --quote-border: rgba(100, 210, 255, 0.72); + --code-inline-bg: rgba(11, 18, 31, 0.84); + --code-inline-border: rgba(170, 199, 240, 0.35); + --code-inline-text: #e9f2ff; + --code-block-bg: #0f1b30; + --code-block-border: rgba(155, 189, 234, 0.36); + --code-block-text: #ecf3ff; + --content-width: 980px; + --radius-lg: 22px; + --radius-md: 14px; + --radius-sm: 10px; + --blur: 14px; + --blur-light: 8px; + --font-main: "Inter", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --font-headings: "Inter", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: light) { + :root { + --page-bg: #eef4ff; + --page-bg-2: #d8e5fb; + --glow-a: rgba(0, 122, 255, 0.17); + --glow-b: rgba(88, 86, 214, 0.12); + --glass-surface: rgba(255, 255, 255, 0.9); + --glass-surface-strong: rgba(255, 255, 255, 0.95); + --glass-panel: rgba(249, 252, 255, 0.93); + --glass-border: rgba(84, 117, 171, 0.24); + --glass-border-strong: rgba(63, 97, 157, 0.34); + --text: #1d2739; + --text-muted: #52617c; + --heading: #10192b; + --link: #005fcc; + --link-hover: #0047a3; + --accent: #007aff; + --accent-2: #5856d6; + --shadow-lg: 0 20px 45px rgba(58, 92, 149, 0.18); + --shadow-sm: 0 8px 22px rgba(58, 92, 149, 0.12); + --quote-bg: rgba(232, 240, 255, 0.88); + --quote-border: rgba(0, 122, 255, 0.62); + --code-inline-bg: rgba(227, 237, 255, 0.84); + --code-inline-border: rgba(79, 111, 170, 0.34); + --code-inline-text: #18345c; + --code-block-bg: #132342; + --code-block-border: rgba(56, 93, 149, 0.44); + --code-block-text: #ebf2ff; + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 18px; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + min-height: 100vh; + color: var(--text); + font-family: var(--font-main); + line-height: 1.72; + background: + radial-gradient(circle at 12% 10%, var(--glow-a) 0, transparent 36%), + radial-gradient(circle at 84% 6%, var(--glow-b) 0, transparent 32%), + linear-gradient(165deg, var(--page-bg) 0%, var(--page-bg-2) 55%, var(--page-bg) 100%); + padding: 22px; + position: relative; +} + +body::before, +body::after { + content: ""; + position: fixed; + pointer-events: none; + z-index: 0; +} + +body::before { + inset: -12% auto auto -10%; + width: 340px; + height: 340px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.22) 0, rgba(255, 255, 255, 0.04) 58%, transparent 74%); + filter: blur(9px); +} + +body::after { + inset: auto -8% -14% auto; + width: 380px; + height: 380px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.16) 0, rgba(255, 255, 255, 0.03) 60%, transparent 76%); + filter: blur(11px); +} + +::selection { + background: rgba(100, 210, 255, 0.35); + color: var(--heading); +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.container { + position: relative; + z-index: 1; + max-width: var(--content-width); + margin: 0 auto; + background: var(--glass-surface); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + overflow: hidden; + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); +} + +@supports not (backdrop-filter: blur(1px)) { + .container { + background: var(--glass-surface-strong); + } +} + +.container::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: + linear-gradient(120deg, rgba(255, 255, 255, 0.15), transparent 28%), + linear-gradient(330deg, rgba(255, 255, 255, 0.08), transparent 36%); + z-index: 0; +} + +header, +main, +footer { + position: relative; + z-index: 1; +} + +header { + padding: 30px 38px 17px; + border-bottom: 1px solid var(--glass-border); + background: var(--glass-surface-strong); +} + +.site-title, +header h1 { + margin: 0; + color: var(--heading); + font-family: var(--font-headings); + font-size: clamp(2rem, 3.7vw, 2.75rem); + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.15; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: var(--link-hover); +} + +header p { + margin: 0.56rem 0 0; + color: var(--text-muted); + font-size: 1rem; +} + +nav { + margin-top: 1rem; + display: flex; + flex-wrap: wrap; + gap: 0.46rem; +} + +nav a { + display: inline-flex; + align-items: center; + padding: 0.32rem 0.74rem; + border-radius: 999px; + border: 1px solid var(--glass-border-strong); + background: var(--glass-panel); + color: var(--text); + text-decoration: none; + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.01em; + backdrop-filter: blur(var(--blur-light)); + -webkit-backdrop-filter: blur(var(--blur-light)); + transition: background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease, transform 0.16s ease; +} + +nav a:hover, +nav a:focus { + transform: translateY(-1px); + background: rgba(100, 210, 255, 0.2); + border-color: rgba(100, 210, 255, 0.55); + color: var(--heading); +} + +main { + padding: 28px 38px 32px; + min-height: 62vh; +} + +article { + margin-bottom: 1.25rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--glass-panel); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + padding: 1.16rem 1.1rem; +} + +article.post + article.post, +.posts-list article + article { + margin-top: 0.95rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.08rem 0 0.72rem; + color: var(--heading); + font-family: var(--font-headings); + line-height: 1.3; + letter-spacing: -0.01em; +} + +h1 { font-size: clamp(1.72rem, 3vw, 2.24rem); } +h2 { font-size: clamp(1.4rem, 2.35vw, 1.78rem); } +h3 { font-size: clamp(1.18rem, 1.8vw, 1.4rem); } + +article.post > h1, +article.page > h1, +.posts-list h2 { + margin-top: 0; +} + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.95rem; +} + +a { + color: var(--link); + text-decoration-thickness: 1px; + text-underline-offset: 0.14em; + text-decoration-color: rgba(157, 207, 255, 0.65); +} + +a:hover, +a:focus { + color: var(--link-hover); + text-decoration-color: currentColor; +} + +.meta, +.page-meta { + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 500; + letter-spacing: 0.02em; +} + +.page-meta { + margin-bottom: 0.68rem; +} + +.reading-time { + margin-top: 0.24rem; +} + +.summary { + color: var(--text-muted); +} + +.post-content { + margin-top: 0.65rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 0.92rem 0; +} + +img { + display: block; + max-width: 100%; + height: auto; + border-radius: 12px; + border: 1px solid var(--glass-border-strong); + box-shadow: 0 8px 20px rgba(2, 8, 20, 0.3); +} + +.image-caption, +figcaption { + margin-top: 0.42rem; + color: var(--text-muted); + font-size: 0.83rem; + font-style: italic; +} + +blockquote { + margin: 1rem 0; + padding: 0.72rem 0.92rem; + border-left: 4px solid var(--quote-border); + border-radius: var(--radius-sm); + background: var(--quote-bg); + color: var(--text); +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.84rem; +} + +code { + padding: 0.09rem 0.3rem; + border-radius: 6px; + border: 1px solid var(--code-inline-border); + background: var(--code-inline-bg); + color: var(--code-inline-text); +} + +pre { + margin: 1rem 0; + padding: 0.88rem; + border-radius: var(--radius-sm); + border: 1px solid var(--code-block-border); + background: var(--code-block-bg); + color: var(--code-block-text); + overflow-x: auto; +} + +pre code { + padding: 0; + border: 0; + background: none; + color: inherit; +} + +hr { + border: 0; + margin: 1.28rem 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(180, 206, 242, 0.7), transparent); +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.36rem; + margin-top: 0.84rem; +} + +.tags a, +.tags-list a { + display: inline-block; + color: var(--text); + text-decoration: none; + background: var(--glass-panel); + border: 1px solid var(--glass-border); + border-radius: 999px; + padding: 0.16rem 0.54rem; + font-size: 0.75rem; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + color: var(--heading); + border-color: rgba(100, 210, 255, 0.58); + background: rgba(100, 210, 255, 0.19); +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.tag-count { + color: var(--text-muted); +} + +.pagination { + margin-top: 1.12rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.42rem; +} + +.pagination a { + display: inline-block; + color: var(--text); + text-decoration: none; + background: var(--glass-panel); + border: 1px solid var(--glass-border); + border-radius: 999px; + padding: 0.18rem 0.62rem; + font-size: 0.75rem; +} + +.pagination a:hover, +.pagination a:focus { + color: var(--heading); + border-color: rgba(100, 210, 255, 0.58); + background: rgba(100, 210, 255, 0.19); +} + +.page-info { + color: var(--text-muted); + font-size: 0.77rem; +} + +.related-posts { + margin-top: 1.22rem; + padding-top: 0.8rem; + border-top: 1px dashed var(--glass-border-strong); +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.04rem; +} + +.related-posts-list { + display: grid; + gap: 0.58rem; +} + +.related-post { + border: 1px solid var(--glass-border); + border-radius: var(--radius-sm); + background: var(--glass-panel); + padding: 0.5rem 0.64rem; +} + +.related-post h4 { + margin: 0 0 0.18rem; + font-size: 0.93rem; +} + +.related-post p { + margin: 0; + color: var(--text-muted); + font-size: 0.88rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.92rem 0; +} + +th, +td { + border: 1px solid var(--glass-border); + padding: 0.38rem 0.52rem; +} + +th { + color: var(--heading); + background: rgba(100, 210, 255, 0.14); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +footer { + padding: 17px 38px 24px; + border-top: 1px solid var(--glass-border); + color: var(--text-muted); + font-size: 0.77rem; + background: var(--glass-surface-strong); +} + +footer p { + margin: 0.3rem 0; +} + +footer a { + color: var(--link); +} + +footer a:hover, +footer a:focus { + color: var(--link-hover); +} + +@media (max-width: 900px) { + body { + padding: 14px; + } + + header, + main, + footer { + padding-left: 22px; + padding-right: 22px; + } + + .site-title, + header h1 { + font-size: clamp(1.7rem, 7vw, 2.25rem); + } +} + +@media (max-width: 640px) { + html { + font-size: 16px; + } + + body { + padding: 10px; + } + + .container { + border-radius: 16px; + } + + header, + main, + footer { + padding-left: 14px; + padding-right: 14px; + } + + nav a { + min-height: 38px; + } + + article.post, + article.page, + .posts-list article { + padding: 0.95rem 0.85rem; + } + + .meta, + .page-info, + .pagination a { + font-size: 0.74rem; + } +} diff --git a/themes/longform/style.css b/themes/longform/style.css index 3c2d7f8..98a8448 100644 --- a/themes/longform/style.css +++ b/themes/longform/style.css @@ -2,8 +2,32 @@ * Longform Theme for BSSG * Optimized for reading long articles with highly readable typography, * contained text width, minimal distractions, and reading progress bar + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining longform reading aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable progress bar animation */ + .progress-bar { + transition: none !important; + } +} + :root { /* Color scheme */ --background-color: #ffffff; @@ -20,10 +44,10 @@ --blockquote-bg: #f7fafc; --blockquote-border: #cbd5e1; - /* Typography */ - --font-serif: 'Merriweather', Georgia, 'Times New Roman', serif; - --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; - --font-mono: 'JetBrains Mono', 'Fira Code', 'Menlo', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-serif: Georgia, 'Times New Roman', Times, serif; + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, Arial, sans-serif; + --font-mono: 'Menlo', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* Sizing */ --content-width: 90%; @@ -125,6 +149,13 @@ header { color: var(--link-color); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + color: var(--link-color); +} + /* Site description */ header p { margin: var(--spacing-xs) 0 0; @@ -177,6 +208,17 @@ nav a.active::after { width: 100%; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + color: var(--link-color); +} + +nav a:focus::after { + width: 100%; +} + /* Content area - centered with improved alignment */ main { padding: 0 var(--content-padding) var(--spacing-xl); @@ -256,6 +298,12 @@ a:visited { color: var(--link-visited); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; +} + /* Lists */ ul, ol { margin: var(--spacing-md) 0; @@ -370,11 +418,19 @@ article .article-content { gap: 0.3em; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏱"; + content: "Time: "; font-size: 1.1em; } +/* Use icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱"; + } +} + /* Tags */ .tags { display: flex; @@ -398,6 +454,14 @@ article .article-content { background-color: var(--blockquote-bg); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + color: var(--link-color); + background-color: var(--blockquote-bg); +} + /* Tags list page */ .tags-list { list-style: none; @@ -498,17 +562,17 @@ main > h1 { border-bottom: none; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; } -.posts-list h3 a { +.posts-list h2 a { color: var(--heading-color); text-decoration: none; } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--link-color); } @@ -563,6 +627,15 @@ footer a:hover { border-color: var(--link-color); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-color); + color: var(--background-color); + border-color: var(--link-color); +} + /* Utilities */ .text-center { text-align: center; diff --git a/themes/macclassic/style.css b/themes/macclassic/style.css index 59ade03..bd03156 100644 --- a/themes/macclassic/style.css +++ b/themes/macclassic/style.css @@ -1,8 +1,34 @@ /* * Mac Classic Theme for BSSG * Styled after the nostalgic MacOS 9 look + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Mac Classic aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms */ + .site-title a:hover, + nav a:hover, + .mac-button:hover { + transform: none !important; + } +} + :root { /* Mac Classic color scheme - improved */ --bg-color: #dedede; @@ -29,27 +55,21 @@ --title-gradient-start: #000000; --title-gradient-end: #333333; - /* Typography */ - --font-main: 'Charcoal', 'Chicago', 'Geneva', 'Helvetica', sans-serif; - --font-headings: 'Charcoal', 'Chicago', 'Geneva', 'Helvetica', sans-serif; - --font-mono: 'Monaco', 'Courier', monospace; + /* Typography - Enhanced font fallbacks for Mac Classic look */ + --font-main: 'Lucida Grande', 'Geneva', 'Verdana', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-headings: 'Lucida Grande', 'Geneva', 'Verdana', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-mono: 'Monaco', 'Menlo', 'Consolas', 'Courier New', 'Liberation Mono', monospace; /* Sizing - increased width for better usage of screen space */ --content-width: 85%; --max-content-width: 960px; } -@font-face { - font-family: 'Chicago'; - src: local('Chicago'), url('https://cdn.jsdelivr.net/gh/dominiklohmann/chicago@v1.0.0/Chicago.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} + /* Base elements */ html { - font-size: 12px; + font-size: 14px; /* Increased from 12px for better readability */ } body { @@ -110,6 +130,23 @@ header::before { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8); } +/* TEXT BROWSER FALLBACK: Simple window controls */ +@supports not (position: absolute) { + header::before { + content: "[X]"; + position: static; + background: none; + border: none; + box-shadow: none; + border-radius: 0; + width: auto; + height: auto; + transform: none; + color: var(--text-color); + font-size: 10px; + } +} + /* Add zoom button */ header::after { content: ""; @@ -125,11 +162,28 @@ header::after { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8); } +/* TEXT BROWSER FALLBACK: Simple zoom control */ +@supports not (position: absolute) { + header::after { + content: "[+]"; + position: static; + background: none; + border: none; + box-shadow: none; + border-radius: 0; + width: auto; + height: auto; + transform: none; + color: var(--text-color); + font-size: 10px; + } +} + /* Site title with Mac Classic style */ .site-title { margin: 0; padding: 0; - font-size: 13px; + font-size: 15px; /* Increased from 13px */ font-weight: bold; text-align: center; flex: 1; @@ -155,10 +209,20 @@ header::after { border-color: #555555; } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background: linear-gradient(to bottom, #999999, #777777); + color: #ffffff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3); + border-color: #555555; +} + header h1 { margin: 0; padding: 0; - font-size: 13px; + font-size: 15px; /* Increased from 13px */ font-weight: bold; text-align: center; color: var(--text-color); @@ -199,9 +263,9 @@ nav::before { nav a { color: var(--text-color); text-decoration: none; - padding: 1px 8px; + padding: 2px 10px; /* Increased padding */ margin: 0 1px; - font-size: 12px; + font-size: 13px; /* Increased from 12px */ font-weight: bold; position: relative; display: inline-block; @@ -216,6 +280,15 @@ nav a:hover { text-shadow: none; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: #000000; + color: #ffffff; + text-shadow: none; +} + nav a.active { background-color: var(--accent-color); color: var(--text-color); @@ -242,23 +315,23 @@ h1, h2, h3, h4, h5, h6 { } h1 { - font-size: 18px; + font-size: 20px; /* Increased from 18px */ } h2 { - font-size: 16px; + font-size: 18px; /* Increased from 16px */ } h3 { - font-size: 14px; + font-size: 16px; /* Increased from 14px */ } h4 { - font-size: 12px; + font-size: 14px; /* Increased from 12px */ } article h1 { - font-size: 18px; + font-size: 20px; /* Increased from 18px */ border-bottom: 1px solid var(--border-color); padding-bottom: 5px; } @@ -286,6 +359,14 @@ a:hover { background-color: var(--highlight-yellow); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + text-decoration: none; + background-color: var(--highlight-yellow); +} + /* Classic Mac button style */ .mac-button { display: inline-block; @@ -313,6 +394,13 @@ a:hover { box-shadow: none; } +/* ACCESSIBILITY: Focus states for Mac buttons */ +.mac-button:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + .mac-button.primary { border-width: 2px; font-weight: bold; @@ -345,6 +433,18 @@ article .meta { font-size: 10px; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use Mac Classic style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags */ .tags { margin-top: 1rem; @@ -366,6 +466,13 @@ article .meta { background-color: var(--accent-color); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: var(--accent-color); +} + .tags-list { margin: 1rem 0; } @@ -470,7 +577,7 @@ footer { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 5px; } @@ -620,6 +727,13 @@ hr { background-color: var(--accent-color); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: var(--accent-color); +} + .pagination .current { background-color: var(--selection-bg); color: var(--selection-text); diff --git a/themes/macos9/style.css b/themes/macos9/style.css index e383a98..c54110e 100644 --- a/themes/macos9/style.css +++ b/themes/macos9/style.css @@ -1,8 +1,33 @@ /* * Mac OS 9 Theme for BSSG * Styled after the authentic Mac OS 9.2 look + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Mac OS 9 aesthetics */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms and opacity changes */ + .site-title a:hover::before, + .site-title a:focus::before { + opacity: 0 !important; + } +} + :root { /* Mac OS 9 color scheme */ --bg-color: #3a57a6; /* Mac OS 9 blue background */ @@ -29,26 +54,18 @@ --title-gradient-start: #000000; --title-gradient-end: #666666; - /* Typography */ - --font-main: 'Charcoal', 'Chicago', 'Geneva', 'Helvetica', sans-serif; - --font-headings: 'Charcoal', 'Chicago', 'Geneva', 'Helvetica', sans-serif; - --font-mono: 'Monaco', 'Courier', monospace; + /* Typography - Enhanced font fallbacks for Mac OS 9 look */ + --font-main: 'Lucida Grande', 'Geneva', 'Verdana', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-headings: 'Lucida Grande', 'Geneva', 'Verdana', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-mono: 'Monaco', 'Menlo', 'Consolas', 'Courier New', 'Liberation Mono', monospace; /* Sizing */ --content-width: 680px; } -@font-face { - font-family: 'Chicago'; - src: local('Chicago'), url('https://cdn.jsdelivr.net/gh/dominiklohmann/chicago@v1.0.0/Chicago.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} - /* Base elements */ html { - font-size: 12px; + font-size: 14px; /* Increased from 12px for better readability */ } body { @@ -105,11 +122,27 @@ header::before { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8); } +/* TEXT BROWSER FALLBACK: Simple window control */ +@supports not (position: absolute) { + header::before { + content: "[X]"; + position: static; + background: none; + border: none; + box-shadow: none; + border-radius: 0; + width: auto; + height: auto; + color: var(--text-color); + font-size: 10px; + } +} + /* Site title with Mac OS 9 style */ .site-title { margin: 0; padding: 0; - font-size: 13px; + font-size: 15px; /* Increased from 13px */ font-weight: bold; text-align: center; flex: 1; @@ -147,10 +180,21 @@ header::before { opacity: 1; } +.site-title a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + color: var(--selection-bg); + text-decoration: none; +} + +.site-title a:focus::before { + opacity: 1; +} + header h1 { margin: 0; padding: 0; - font-size: 13px; + font-size: 15px; /* Increased from 13px */ font-weight: bold; text-align: center; color: var(--text-color); @@ -174,9 +218,9 @@ nav { nav a { color: var(--text-color); text-decoration: none; - padding: 2px 10px; + padding: 3px 12px; /* Increased padding */ margin: 0 2px; - font-size: 13px; + font-size: 14px; /* Increased from 13px */ font-weight: normal; border: none; } @@ -193,6 +237,14 @@ nav a.active { border-radius: 0; } +nav a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: #3875d7; + color: #ffffff; + border-radius: 0; +} + /* Content area */ main { padding: 15px; @@ -249,16 +301,23 @@ a:hover { border-bottom: 1px solid currentColor; } +a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + text-decoration: underline; + border-bottom: 1px solid currentColor; +} + /* Mac OS 9 style button */ .mac-button { display: inline-block; - padding: 2px 10px; + padding: 3px 12px; /* Increased padding */ margin: 2px; background: var(--button-face); border: 1px solid #000000; border-radius: 0; font-family: var(--font-main); - font-size: 12px; + font-size: 13px; /* Increased from 12px */ text-align: center; color: var(--text-color); cursor: pointer; @@ -304,6 +363,18 @@ article .meta { border-left: 1px solid #cccccc; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use Mac OS 9 style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .tags { margin: 1em 0; } @@ -325,6 +396,13 @@ article .meta { color: #ffffff; } +.tags a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: #3875d7; + color: #ffffff; +} + .tags-list { list-style: none; padding: 0; @@ -423,7 +501,7 @@ footer { border-bottom: 1px solid var(--accent-color); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.3em; } @@ -555,6 +633,13 @@ hr { color: #ffffff; } +.pagination a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: #3875d7; + color: #ffffff; +} + .pagination .current { background-color: #3875d7; color: #ffffff; diff --git a/themes/mario/style.css b/themes/mario/style.css index c9a1a9e..5ccffcc 100644 --- a/themes/mario/style.css +++ b/themes/mario/style.css @@ -2,15 +2,38 @@ * Super Mario Bros Theme for BSSG * Inspired by the classic Nintendo game with iconic blue sky, green pipes, * brick blocks, question blocks, and classic Mario color palette + * IMPROVED: Better accessibility, performance, and text browser support */ -@font-face { - font-family: 'Press Start 2P'; - src: url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); - font-weight: normal; - font-style: normal; +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects for reduced motion */ + body { + background-image: linear-gradient( + var(--mario-ground) 0%, + var(--mario-ground) 25px, + var(--mario-sky-blue) 25px, + var(--mario-sky-blue) 100% + ) !important; + } + + /* Simplify animations */ + .site-title::before, + .pagination a::before { + animation: none !important; + } } +/* REMOVED: External font loading for better performance and text browser support */ +/* @font-face removed - using system fonts with pixel-style fallbacks */ + :root { /* Mario color palette */ --mario-red: #e52521; @@ -51,10 +74,10 @@ --nav-text: var(--mario-white); --nav-hover: var(--mario-yellow); - /* Typography */ - --font-headings: 'Press Start 2P', monospace; - --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - IMPROVED fallbacks for text browsers */ + --font-headings: 'Courier New', Courier, 'Lucida Console', 'Monaco', monospace; + --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'Courier New', Courier, 'Lucida Console', monospace; /* Sizing */ --content-width: 90%; @@ -68,11 +91,11 @@ --spacing-lg: 2rem; --spacing-xl: 2.5rem; - /* Transitions */ - --transition-fast: 0.2s; + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s; /* Reduced for better performance */ } -/* Base elements */ +/* Base elements - OPTIMIZED background */ html { font-size: 16px; } @@ -80,6 +103,7 @@ html { body { font-family: var(--font-main); background-color: var(--bg-color); + /* OPTIMIZED: Simplified cloud background for better performance */ background-image: /* Ground */ linear-gradient( @@ -88,40 +112,20 @@ body { var(--mario-sky-blue) 25px, var(--mario-sky-blue) 100% ), - /* Clouds */ + /* Simplified clouds - reduced complexity */ radial-gradient( circle at 20% 20%, var(--mario-white) 20px, transparent 20px ), radial-gradient( - circle at 25% 20%, + circle at 70% 15%, var(--mario-white) 25px, transparent 25px - ), - radial-gradient( - circle at 30% 20%, - var(--mario-white) 20px, - transparent 20px - ), - radial-gradient( - circle at 70% 10%, - var(--mario-white) 28px, - transparent 28px - ), - radial-gradient( - circle at 75% 10%, - var(--mario-white) 32px, - transparent 32px - ), - radial-gradient( - circle at 80% 10%, - var(--mario-white) 28px, - transparent 28px ); - background-repeat: repeat-x; - background-size: 100% 100%, 200px 200px, 200px 200px, 200px 200px, 300px 300px, 300px 300px, 300px 300px; - background-position: 0 calc(100% - 40px), 10% 30%, 15% 30%, 20% 30%, 60% 15%, 65% 15%, 70% 15%; + background-repeat: repeat-x, no-repeat, no-repeat; + background-size: 100% 100%, 200px 200px, 250px 250px; + background-position: 0 calc(100% - 40px), 10% 30%, 60% 20%; color: var(--text-color); margin: 0; padding: var(--spacing-lg); @@ -135,6 +139,7 @@ body { padding: 0; background-color: var(--container-bg); border: var(--border-width) solid var(--border-color); + /* OPTIMIZED: Simplified shadow */ box-shadow: var(--pixel-size) var(--pixel-size) 0 var(--mario-brick), calc(var(--pixel-size) * 2) calc(var(--pixel-size) * 2) 0 rgba(0, 0, 0, 0.2); @@ -145,7 +150,7 @@ body { border-top: calc(var(--pixel-size) * 4) solid var(--mario-brick); } -/* Mario pipe-style border on container */ +/* Mario pipe-style border on container - OPTIMIZED */ .container::before, .container::after { content: ""; @@ -170,7 +175,7 @@ body { bottom: 0; } -/* Header with Mario aesthetic */ +/* Header with Mario aesthetic - IMPROVED accessibility */ header { background-color: var(--header-bg); padding: var(--spacing-md) var(--spacing-md) var(--spacing-lg); @@ -179,7 +184,7 @@ header { position: relative; } -/* Question block title effect */ +/* Question block title effect - IMPROVED accessibility */ .site-title { margin: 0; padding: var(--spacing-md); @@ -188,48 +193,64 @@ header { position: relative; background-color: var(--mario-question); border: var(--pixel-size) solid var(--mario-brown); - box-shadow: var(--pixel-size) var(--pixel-size) 0 rgba(0, 0, 0, 0.5); + box-shadow: + inset calc(var(--pixel-size) * -1) calc(var(--pixel-size) * -1) 0 var(--mario-coin), + inset var(--pixel-size) var(--pixel-size) 0 var(--mario-white); font-family: var(--font-headings); + text-transform: uppercase; + letter-spacing: 2px; + border-radius: 4px; + transition: all var(--transition-fast) ease; } +/* OPTIMIZED: Simplified question mark animation */ .site-title::before { content: "?"; position: absolute; top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: var(--mario-white); - font-size: 1.8rem; - opacity: 0.3; - text-shadow: 1px 1px 0 var(--mario-brown); - z-index: 1; + right: calc(var(--pixel-size) * 2); + transform: translateY(-50%); + font-size: 1.2rem; + color: var(--mario-brown); + font-weight: bold; + animation: blink 2s infinite; +} + +/* FALLBACK: Text browser support for question mark */ +@supports not (content: "?") { + .site-title::before { + content: "?"; + } } .site-title a { - color: var(--mario-white); + color: var(--mario-brown); text-decoration: none; - text-shadow: - 1px 1px 0 var(--mario-brown), - -1px -1px 0 var(--mario-brown), - 1px -1px 0 var(--mario-brown), - -1px 1px 0 var(--mario-brown); - position: relative; - z-index: 2; + font-weight: bold; + display: block; + padding-right: calc(var(--pixel-size) * 6); + transition: all var(--transition-fast) ease; } .site-title a:hover { - color: var(--mario-yellow); - animation: jump 0.5s ease-in-out; + color: var(--mario-red); + transform: translateY(-2px); +} + +.site-title a:focus { + outline: 2px solid var(--mario-white); + outline-offset: 2px; + color: var(--mario-red); + transform: translateY(-2px); } /* Site description */ header p { - margin: var(--spacing-sm) auto 0; + margin: var(--spacing-md) 0 0; color: var(--mario-white); - font-size: 1rem; - max-width: 550px; - text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); - line-height: 1.7; + font-size: 0.9rem; + font-weight: bold; + text-shadow: 1px 1px 0 var(--mario-brick); } /* Pipe-style Navigation */ @@ -269,6 +290,17 @@ nav a.active { inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); } +nav a:focus { + outline: 3px solid var(--mario-yellow); + outline-offset: 2px; + color: var(--nav-hover); + background-color: var(--mario-green); + transform: translateY(var(--pixel-size)); + box-shadow: + 0 0 0 rgba(0, 0, 0, 0.5), + inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); +} + nav a.active::before { content: "►"; position: absolute; @@ -313,6 +345,7 @@ h2 { margin-top: var(--spacing-xl); } +.posts-list h2, h3 { font-size: 1.3rem; color: var(--mario-pipe-green); @@ -338,6 +371,13 @@ a:hover { border-bottom: 2px solid var(--link-hover); } +a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + color: var(--link-hover); + border-bottom: 2px solid var(--link-hover); +} + /* Coin icon for links only in content areas */ main p a::before { content: "•"; @@ -422,6 +462,14 @@ article .meta { border-bottom: none; } +.tags a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + background-color: var(--mario-red); + transform: translateY(-2px); + border-bottom: none; +} + .tags a::before { display: none; } @@ -458,6 +506,15 @@ article .meta { border-bottom: none; } +.tags-list a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + background-color: var(--mario-green); + color: var(--mario-yellow); + transform: scale(1.05); + border-bottom: none; +} + /* Images */ .featured-image, figure { @@ -533,7 +590,7 @@ blockquote { } blockquote::before { - content: """; + content: "\201C"; position: absolute; top: -0.5rem; left: 0.5rem; @@ -605,6 +662,13 @@ footer a:hover { border-bottom: 1px solid var(--mario-white); } +footer a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + color: var(--mario-white); + border-bottom: 1px solid var(--mario-white); +} + /* Pagination */ .pagination { display: flex; @@ -630,6 +694,15 @@ footer a:hover { border-bottom: var(--pixel-size) solid var(--mario-green); } +.pagination a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + background-color: var(--mario-green); + color: var(--mario-yellow); + transform: translateY(-2px); + border-bottom: var(--pixel-size) solid var(--mario-green); +} + .pagination a::before { display: none; } @@ -653,7 +726,7 @@ footer a:hover { } } -/* Responsive adjustments */ +/* IMPROVED: Responsive adjustments with mobile optimizations */ @media (max-width: 768px) { html { font-size: 15px; @@ -661,12 +734,21 @@ footer a:hover { body { padding: var(--spacing-md); + /* OPTIMIZED: Simplified background on mobile */ + background-image: linear-gradient( + var(--mario-ground) 0%, + var(--mario-ground) 25px, + var(--mario-sky-blue) 25px, + var(--mario-sky-blue) 100% + ); } .container { width: 100%; margin-left: 0; margin-right: 0; + /* OPTIMIZED: Simplified shadows on mobile */ + box-shadow: var(--pixel-size) var(--pixel-size) 0 var(--mario-brick); } main { @@ -681,22 +763,35 @@ footer a:hover { nav a { width: 80%; text-align: center; + /* OPTIMIZED: Simplified transitions on mobile */ + transition: background-color var(--transition-fast) ease; } .site-title { font-size: 1.4rem; } + + /* OPTIMIZED: Disable animations on mobile */ + .site-title::before { + animation: none; + } } @media (max-width: 480px) { body { padding: var(--spacing-xs); + /* OPTIMIZED: Remove all background images on small mobile */ + background-image: none; + background-color: var(--mario-sky-blue); } .container { width: 100%; border-radius: 0; box-shadow: none; + /* OPTIMIZED: Simplified borders on small mobile */ + border-left: none; + border-right: none; } main { @@ -714,6 +809,13 @@ footer a:hover { .site-title { font-size: 1.2rem; padding: var(--spacing-sm); + /* OPTIMIZED: Simplified styling on small mobile */ + letter-spacing: 1px; + } + + .site-title::before { + /* OPTIMIZED: Remove question mark on very small screens */ + display: none; } p { diff --git a/themes/material/style.css b/themes/material/style.css index ca9fa4c..e73f70f 100644 --- a/themes/material/style.css +++ b/themes/material/style.css @@ -1,6 +1,7 @@ /* * Android Material Design Theme for BSSG * Modern, clean Material Design styling inspired by Google's design language + * Enhanced with accessibility, performance, and compatibility improvements */ :root { @@ -28,10 +29,10 @@ --dp16: rgba(0, 0, 0, 0.15); --dp24: rgba(0, 0, 0, 0.16); - /* Typography */ - --font-main: 'Roboto', 'Segoe UI', 'Arial', sans-serif; - --font-headings: 'Roboto', 'Segoe UI', 'Arial', sans-serif; - --font-mono: 'Roboto Mono', monospace; + /* Typography - Enhanced with better fallbacks */ + --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + --font-headings: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; /* Sizing */ --content-width: 1000px; @@ -49,6 +50,26 @@ --radius: var(--border-radius); } +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + :root { + --transition: 0.01s; + } + + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + .featured-image img, + .material-fab, + article { + transform: none !important; + } +} + /* Material Design pattern for body */ body { font-family: var(--font-main); @@ -71,132 +92,169 @@ body { overflow: hidden; } -/* App bar with primary color */ +/* Clean Material Design header */ header { background-color: var(--primary-color); color: var(--on-primary); - padding: 16px; + padding: 20px 24px; position: relative; - font-size: 1.25rem; - display: flex; - align-items: center; - justify-content: space-between; - box-shadow: 0 4px 6px var(--dp4); + box-shadow: 0 2px 4px var(--dp2), 0 4px 8px var(--dp4); z-index: 2; } -/* Site title with Material-style gradient */ +/* Header content wrapper */ +.header-content { + max-width: var(--content-width); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; +} + +/* Site title - clean and simple */ .site-title { margin: 0; padding: 0; - font-size: 1.5rem; - font-weight: 500; + font-size: 1.75rem; + font-weight: 600; font-family: var(--font-headings); - background: linear-gradient(120deg, var(--on-primary) 0%, rgba(255, 255, 255, 0.8) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + color: var(--on-primary); + letter-spacing: -0.02em; } .site-title a { text-decoration: none; - background: linear-gradient(120deg, var(--on-primary) 0%, rgba(255, 255, 255, 0.8) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + color: var(--on-primary); transition: all var(--transition); + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 4px; + padding: 4px 8px; + margin: -4px -8px; } .site-title a:hover { text-decoration: none; - background: linear-gradient(120deg, rgba(255, 255, 255, 0.9) 0%, var(--secondary-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: rgba(255, 255, 255, 0.9); transform: translateY(-1px); } -/* Menu icons for Material Design */ -.menu-controls { - display: flex; - gap: 16px; +.site-title a:focus { + outline-color: rgba(255, 255, 255, 0.5); } -.material-icon { - width: 24px; - height: 24px; +/* Site description */ +.site-description { + margin: 4px 0 0 0; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); + font-weight: 400; + line-height: 1.3; +} + +/* Header actions - simplified */ +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.header-button { + background: none; + border: none; + color: var(--on-primary); + padding: 8px; + border-radius: 50%; + cursor: pointer; + transition: background-color 0.2s; + outline: 2px solid transparent; + outline-offset: 2px; + width: 40px; + height: 40px; display: flex; align-items: center; justify-content: center; - color: var(--on-primary); - cursor: pointer; - border-radius: 50%; - transition: background-color 0.2s; } -.material-icon:hover { +.header-button:hover { background-color: rgba(255, 255, 255, 0.1); } -.material-menu::before { +.header-button:focus { + outline-color: rgba(255, 255, 255, 0.5); +} + +.header-button::before { + font-size: 18px; +} + +.header-menu::before { content: "☰"; - font-size: 18px; } -.material-more::before { - content: "⋮"; - font-size: 18px; +.header-search::before { + content: "🔍"; } -header h1 { - margin: 0; - padding: 0; - font-size: 1.25rem; - font-weight: 500; - font-family: var(--font-headings); - color: var(--on-primary); - background: linear-gradient(120deg, var(--on-primary) 0%, rgba(255, 255, 255, 0.8) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +/* Text browser fallbacks */ +@supports not (content: "☰") { + .header-menu::before { + content: "Menu"; + font-size: 10px; + } + + .header-search::before { + content: "Search"; + font-size: 10px; + } } /* Navigation in Material UI style */ nav { display: flex; - flex-wrap: nowrap; - background-color: var(--primary-color); + flex-wrap: wrap; + background-color: var(--surface); padding: 0; - overflow-x: auto; + overflow: visible; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + border-bottom: 1px solid var(--dp1); + justify-content: center; } nav a { - color: var(--on-primary); + color: var(--primary-color); text-decoration: none; - padding: 15px 20px; + padding: 15px 16px; margin: 0; font-weight: 500; - font-size: 0.95rem; + font-size: 0.9rem; position: relative; display: inline-block; text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; transition: all 0.3s; + flex: 1; + text-align: center; + min-width: 0; + /* Enhanced focus outline for accessibility */ + outline: 2px solid transparent; + outline-offset: -2px; } nav a:hover, nav a:focus { - color: var(--on-primary); - background-color: rgba(255, 255, 255, 0.1); + color: var(--primary-variant); + background-color: rgba(98, 0, 238, 0.08); +} + +nav a:focus { + outline-color: var(--primary-color); } /* Active tab with indicator */ nav a.active { - color: var(--on-primary); + color: var(--primary-variant); + background-color: rgba(98, 0, 238, 0.12); } nav a.active::after { @@ -206,7 +264,7 @@ nav a.active::after { left: 0; width: 100%; height: 2px; - background-color: var(--on-primary); + background-color: var(--primary-color); } /* Media query for responsive design */ @@ -220,12 +278,17 @@ nav a.active::after { nav { flex-direction: row; flex-wrap: wrap; + justify-content: center; } nav a { - flex-grow: 1; + flex: 1; text-align: center; - padding: 12px 10px; + padding: 12px 8px; + font-size: 0.8rem; + min-width: 0; + white-space: normal; + word-break: break-word; } main { @@ -249,6 +312,7 @@ nav a.active::after { font-size: 1.75rem; } + .posts-list h2, h3 { font-size: 1.35rem; } @@ -288,6 +352,7 @@ nav a.active::after { font-size: 1.5rem; } + .posts-list h2, h3 { font-size: 1.25rem; } @@ -329,11 +394,26 @@ nav a.active::after { } header { - padding: 12px; + padding: 16px 20px; + } + + .header-content { + flex-direction: column; + align-items: flex-start; + gap: 8px; } .site-title { - font-size: 1.25rem; + font-size: 1.5rem; + } + + .site-description { + font-size: 0.85rem; + } + + .header-actions { + align-self: flex-end; + margin-top: -32px; } main { @@ -348,6 +428,7 @@ nav a.active::after { font-size: 1.35rem; } + .posts-list h2, h3 { font-size: 1.15rem; } @@ -412,6 +493,8 @@ nav a.active::after { nav a { padding: 10px 6px; font-size: 0.75rem; + white-space: normal; + word-break: break-word; } h1 { @@ -422,6 +505,7 @@ nav a.active::after { font-size: 1.2rem; } + .posts-list h2, h3 { font-size: 1.05rem; } @@ -467,7 +551,7 @@ h1, h2, h3, h4, h5, h6 { line-height: 1.2; } -/* Decorative elements for headings */ +/* Decorative elements for headings with text browser fallbacks */ h1::before, h2::before, h3::before, h4::before { content: "#"; display: inline-block; @@ -480,6 +564,14 @@ h1::before, h2::before, h3::before, h4::before { opacity: 0; } +/* Text browser fallback for heading decorations */ +@supports not (transform: translateX(-5px)) { + h1::before, h2::before, h3::before, h4::before { + content: ""; + display: none; + } +} + h1:hover::before, h2:hover::before, h3:hover::before, h4:hover::before { transform: translateX(0); opacity: 0.6; @@ -495,6 +587,7 @@ h2 { letter-spacing: -0.01em; } +.posts-list h2, h3 { font-size: 1.5rem; } @@ -508,10 +601,13 @@ p { line-height: 1.6; } -/* Links */ +/* Links with enhanced focus states */ a { color: var(--primary-color); text-decoration: none; + outline: 2px solid transparent; + outline-offset: 2px; + transition: outline-color var(--transition); } a:visited { @@ -522,6 +618,10 @@ a:hover { text-decoration: underline; } +a:focus { + outline-color: var(--primary-color); +} + /* Material Design buttons */ .material-button { display: inline-block; @@ -537,11 +637,19 @@ a:hover { cursor: pointer; transition: box-shadow 0.2s, background-color 0.2s; box-shadow: 0 2px 4px var(--dp2); + border: none; + outline: 2px solid transparent; + outline-offset: 2px; } .material-button:hover { box-shadow: 0 4px 8px var(--dp4); background-color: var(--primary-variant); + text-decoration: none; +} + +.material-button:focus { + outline-color: var(--primary-color); } .material-button.secondary { @@ -553,7 +661,11 @@ a:hover { background-color: var(--secondary-variant); } -/* Floating action button */ +.material-button.secondary:focus { + outline-color: var(--secondary-color); +} + +/* Floating action button with text browser fallback */ .material-fab { position: fixed; bottom: 24px; @@ -571,17 +683,32 @@ a:hover { z-index: 10; transition: box-shadow 0.2s, transform 0.2s; cursor: pointer; + border: none; + outline: 2px solid transparent; + outline-offset: 2px; } .material-fab::before { content: "+"; } +/* Text browser fallback for FAB */ +@supports not (content: "+") { + .material-fab::before { + content: "Add"; + font-size: 12px; + } +} + .material-fab:hover { box-shadow: 0 8px 12px var(--dp8), 0 3px 16px var(--dp4); transform: translateY(-2px); } +.material-fab:focus { + outline-color: var(--secondary-color); +} + /* Articles in Material Design style */ article { margin-bottom: 24px; @@ -617,6 +744,14 @@ article .meta { margin-right: 4px; } +/* Text browser fallback for reading time icon */ +@supports not (content: "⏱️") { + .reading-time::before { + content: "Time: "; + margin-right: 4px; + } +} + /* Featured Images in Material Design style */ .featured-image { margin: 16px 0; @@ -696,6 +831,8 @@ article .meta { border-radius: 16px; text-decoration: none; transition: background-color 0.2s; + outline: 2px solid transparent; + outline-offset: 2px; } .tags a:hover { @@ -703,6 +840,10 @@ article .meta { text-decoration: none; } +.tags a:focus { + outline-color: var(--primary-color); +} + code { font-family: var(--font-mono); background-color: rgba(0, 0, 0, 0.05); @@ -758,12 +899,19 @@ footer { text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500; + outline: 2px solid transparent; + outline-offset: 2px; + transition: color var(--transition), outline-color var(--transition); } .bottom-nav-item:hover { color: var(--primary-color); } +.bottom-nav-item:focus { + outline-color: var(--primary-color); +} + /* Pagination in Material Design style */ .pagination { display: flex; @@ -780,6 +928,8 @@ footer { text-decoration: none; border-radius: 4px; transition: background-color 0.2s; + outline: 2px solid transparent; + outline-offset: 2px; } .pagination a:hover { @@ -787,6 +937,10 @@ footer { text-decoration: none; } +.pagination a:focus { + outline-color: var(--primary-color); +} + .pagination .page-info { color: rgba(0, 0, 0, 0.6); font-size: 0.875rem; @@ -803,12 +957,18 @@ footer { margin: 4px; font-size: 0.875rem; transition: background-color 0.2s; + outline: 2px solid transparent; + outline-offset: 2px; } .tags-list .tag:hover { background-color: rgba(98, 0, 238, 0.12); } +.tags-list .tag:focus { + outline-color: var(--primary-color); +} + .tag-count { width: 20px; height: 20px; @@ -817,6 +977,9 @@ footer { font-size: 0.75rem; border-radius: 50%; margin-left: 8px; + display: flex; + align-items: center; + justify-content: center; } /* For posts lists in archives */ diff --git a/themes/microfiche/style.css b/themes/microfiche/style.css new file mode 100644 index 0000000..31858a9 --- /dev/null +++ b/themes/microfiche/style.css @@ -0,0 +1,498 @@ +/* + * Microfiche Theme for BSSG + * Monochrome projection look with scanlines and archival framing. + */ + +:root { + --bg-color: #0f1110; + --screen-color: #171a18; + --panel-color: #1d211e; + --text-color: #d8ddd7; + --muted-text: #9ca39b; + --heading-color: #edf2ea; + --link-color: #e3e8e0; + --link-hover: #ffffff; + --border-color: #4e564f; + --accent-color: #b8c1b5; + --tag-bg: #2a2f2b; + --tag-text: #d5dbd2; + --code-bg: #202522; + --quote-bg: #222723; + --overlay-line: rgba(232, 238, 230, 0.04); + --overlay-noise: rgba(240, 245, 236, 0.035); + --content-width: 900px; + --radius: 7px; + --shadow: 0 12px 24px rgba(0, 0, 0, 0.48); + --font-body: "IBM Plex Sans", "Lucida Grande", "Helvetica Neue", Arial, sans-serif; + --font-heading: "Franklin Gothic Medium", "Arial Narrow", "Trebuchet MS", sans-serif; + --font-ui: "IBM Plex Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; + --font-mono: "IBM Plex Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 17px; +} + +body { + margin: 0; + color: var(--text-color); + background: radial-gradient(circle at 50% 25%, #1a1f1b 0%, #0e110f 62%); + font-family: var(--font-body); + line-height: 1.7; + position: relative; +} + +body::before, +body::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; +} + +body::before { + background: repeating-linear-gradient( + to bottom, + transparent 0, + transparent 3px, + var(--overlay-line) 4px + ); + z-index: 1; +} + +body::after { + background-image: + radial-gradient(var(--overlay-noise) 0.65px, transparent 0.65px), + radial-gradient(var(--overlay-noise) 0.65px, transparent 0.65px); + background-size: 3px 3px, 4px 4px; + background-position: 0 0, 2px 1px; + z-index: 2; +} + +::selection { + background: #dce2d8; + color: #101311; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 1.9rem 1.1rem 2.6rem; + position: relative; + z-index: 3; + background: linear-gradient(180deg, rgba(27, 30, 28, 0.9) 0%, rgba(18, 21, 19, 0.92) 100%); + border-left: 1px solid var(--border-color); + border-right: 1px solid var(--border-color); + box-shadow: var(--shadow); +} + +header { + margin-bottom: 1.8rem; + padding: 1rem 0 0.95rem; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, #202521 0%, #171b18 100%); +} + +.site-title, +header h1 { + margin: 0; + color: var(--heading-color); + font-family: var(--font-heading); + font-weight: 600; + font-size: clamp(1.9rem, 3.8vw, 2.7rem); + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +header p { + margin: 0.42rem 0 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.74rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +header, +main, +footer { + padding-left: 0.85rem; + padding-right: 0.85rem; +} + +nav { + margin-top: 0.82rem; + display: flex; + flex-wrap: wrap; + gap: 0.45rem 0.5rem; +} + +nav a { + display: inline-block; + color: var(--tag-text); + background: #242926; + border: 1px solid #5b655d; + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 0.18rem 0.5rem; +} + +nav a:hover, +nav a:focus { + background: #303732; + color: #f4f8f2; +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.4rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--panel-color); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 1.1rem 1rem 0.95rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--heading-color); + font-family: var(--font-heading); + margin: 1.15rem 0 0.7rem; + line-height: 1.3; + letter-spacing: 0.03em; +} + +h1 { font-size: clamp(1.75rem, 3.4vw, 2.25rem); } +h2 { font-size: clamp(1.4rem, 2.7vw, 1.75rem); } +h3 { font-size: clamp(1.18rem, 2.2vw, 1.4rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.95rem; +} + +a { + color: var(--link-color); + text-decoration-thickness: 1px; + text-decoration-style: dotted; + text-underline-offset: 0.14em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.8rem; +} + +.meta { + margin: 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.72rem; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.reading-time { + margin-top: 0.25rem; +} + +.summary { + margin-top: 0.68rem; + color: #b8beb6; +} + +.post-content { + margin-top: 0.7rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 1rem 0; +} + +img { + display: block; + max-width: 100%; + height: auto; + border: 1px solid #5b635c; + filter: grayscale(100%) contrast(1.06) brightness(0.95); +} + +.image-caption, +figcaption { + margin-top: 0.35rem; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.71rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +blockquote { + margin: 1rem 0; + padding: 0.65rem 0.85rem; + border-left: 3px solid #7f887f; + background: var(--quote-bg); + color: #ced5cd; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.84rem; +} + +code { + background: var(--code-bg); + border: 1px solid #3f4741; + padding: 0.08rem 0.28rem; + border-radius: 3px; +} + +pre { + background: var(--code-bg); + border: 1px solid #3f4741; + border-radius: 4px; + padding: 0.8rem; + overflow-x: auto; +} + +pre code { + background: none; + border: 0; + padding: 0; +} + +hr { + border: 0; + border-top: 1px dashed #556058; + margin: 1.2rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.36rem; + margin-top: 0.9rem; +} + +.tags a, +.tags-list a { + display: inline-block; + text-decoration: none; + color: var(--tag-text); + background: var(--tag-bg); + border: 1px solid #606a62; + border-radius: 3px; + padding: 0.18rem 0.45rem; + font-family: var(--font-ui); + font-size: 0.72rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #333a35; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.44rem; +} + +.tag-count { + color: var(--muted-text); +} + +.posts-list article + article { + margin-top: 0.9rem; +} + +.posts-list h2 { + margin-top: 0; +} + +.pagination { + margin-top: 1.3rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +.pagination a { + display: inline-block; + text-decoration: none; + color: var(--text-color); + border: 1px solid #68726a; + background: #2a2f2b; + padding: 0.2rem 0.55rem; + font-family: var(--font-ui); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.07em; +} + +.pagination a:hover, +.pagination a:focus { + background: #38403a; + color: #f0f5ee; +} + +.page-info { + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.73rem; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.related-posts { + margin-top: 1.45rem; + padding-top: 0.95rem; + border-top: 1px dashed #5e675f; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.05rem; +} + +.related-posts-list { + display: grid; + gap: 0.65rem; +} + +.related-post { + border: 1px solid #5a645c; + background: #242925; + padding: 0.55rem 0.66rem; +} + +.related-post h4 { + margin: 0 0 0.24rem; + font-size: 0.95rem; +} + +.related-post p { + margin: 0; + color: #b7beb5; + font-size: 0.9rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.95rem 0; +} + +th, +td { + border: 1px solid #505850; + padding: 0.4rem 0.52rem; +} + +th { + background: #2b302d; + font-family: var(--font-ui); + font-size: 0.73rem; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +footer { + margin-top: 1.8rem; + border-top: 1px solid var(--border-color); + padding-top: 0.8rem; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.71rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +footer p { + margin: 0.34rem 0; +} + +footer a { + color: inherit; +} + +footer a:hover, +footer a:focus { + color: #f5f9f1; +} + +@media (max-width: 820px) { + .container { + padding: 1.25rem 0.75rem 2rem; + } + + header, + main, + footer { + padding-left: 0.6rem; + padding-right: 0.6rem; + } + + article.post, + article.page, + .posts-list article { + padding: 0.92rem 0.8rem 0.82rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.04em; + } +} diff --git a/themes/mid-century/style.css b/themes/mid-century/style.css index 60d94ec..b2ea0fe 100644 --- a/themes/mid-century/style.css +++ b/themes/mid-century/style.css @@ -2,8 +2,31 @@ * Mid-Century Modern Theme for BSSG * Inspired by 1950s-60s design aesthetic with clean lines, * organic shapes, and distinctive color palette + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic mid-century modern aesthetics */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms and skew effects */ + .site-title::before { + transform: none !important; + } +} + :root { /* Mid-century color scheme */ --teak: #B27C4C; @@ -34,10 +57,10 @@ --footer-text: var(--cream); --border-color: var(--teak); - /* Typography */ - --font-main: 'Futura', 'Trebuchet MS', Arial, sans-serif; - --font-headings: 'Futura', 'Century Gothic', 'Gill Sans', sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for mid-century modern look */ + --font-main: 'Avenir Next', 'Avenir', 'Trebuchet MS', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-headings: 'Avenir Next', 'Avenir', 'Century Gothic', 'Gill Sans', 'Futura', 'Trebuchet MS', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Courier New', 'Liberation Mono', monospace; /* Sizing */ --content-width: 90%; @@ -113,6 +136,21 @@ header { z-index: -1; } +/* TEXT BROWSER FALLBACK: Simple decoration for site title */ +@supports not (position: absolute) { + .site-title::before { + content: "◆ "; + position: static; + background: none; + color: var(--tertiary-accent); + width: auto; + height: auto; + transform: none; + z-index: auto; + font-size: 1rem; + } +} + .site-title a { color: var(--header-text); text-decoration: none; @@ -122,6 +160,12 @@ header { color: var(--accent-color); } +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); +} + /* Site description */ header p { margin: 0.5rem 0 0; @@ -175,6 +219,16 @@ nav a.active::after { width: 50%; } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--nav-hover-bg); +} + +nav a:focus::after { + width: 50%; +} + /* Content area */ main { padding: var(--content-padding); @@ -210,6 +264,16 @@ h1::after { font-size: 1.5rem; } +/* TEXT BROWSER FALLBACK: Simple star for h1 */ +@supports not (position: absolute) { + h1::after { + content: " *"; + position: static; + color: var(--accent-color); + font-size: 1rem; + } +} + h2 { font-size: 2rem; color: var(--accent-color); @@ -246,6 +310,13 @@ a:hover { border-bottom: 2px solid var(--link-hover); } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + border-bottom: 2px solid var(--link-hover); +} + /* Articles with mid-century styling */ article { margin-bottom: 4rem; @@ -305,6 +376,19 @@ article .meta { transform: translateY(-50%); } +/* TEXT BROWSER FALLBACK: Simple "Time: " prefix for reading time */ +@supports not (position: absolute) { + .reading-time { + padding-left: 0; + } + + .reading-time::before { + content: "Time: "; + position: static; + transform: none; + } +} + /* Tags */ .tags { display: flex; @@ -330,6 +414,14 @@ article .meta { border: none; } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--cream); + border: none; +} + /* Tags list page */ .tags-list { list-style-type: none; @@ -364,6 +456,14 @@ article .meta { border: none; } +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--cream); + border: none; +} + /* Footer */ footer { background-color: var(--footer-bg); @@ -412,6 +512,14 @@ footer a:hover { border: none; } +footer a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); + text-decoration: underline; + border: none; +} + /* Pagination */ .pagination { display: flex; @@ -441,6 +549,15 @@ footer a:hover { border: none; } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + transform: translateY(-2px); + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1); + border: none; +} + /* Add improved image handling */ img { max-width: 100%; @@ -513,7 +630,7 @@ img { transform: translateX(5px); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; } diff --git a/themes/minimal/style.css b/themes/minimal/style.css index a942747..6598089 100644 --- a/themes/minimal/style.css +++ b/themes/minimal/style.css @@ -1,8 +1,26 @@ /* * Minimal Theme for BSSG * A simple brutalist design + * Enhanced with accessibility and compatibility improvements */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + .featured-image:hover img, + .site-title a:hover, + .pagination a:hover { + transform: none !important; + } +} + :root { /* Minimal palette */ --bg-color: #ffffff; @@ -91,6 +109,12 @@ a:hover { text-decoration: none; } +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-decoration: none; +} + /* Header */ header { margin-bottom: 3rem; @@ -98,44 +122,83 @@ header { padding-bottom: 1rem; } -/* Site title with subtle gradient */ +/* Site title with enhanced accessibility */ .site-title { font-weight: 700; color: var(--header-color); margin: 0; font-size: 2rem; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } } .site-title a { text-decoration: none; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: var(--header-color); transition: all var(--transition); } +/* Progressive enhancement for gradient text on links */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } + + .site-title a:hover { + background: linear-gradient(120deg, var(--link-color) 0%, var(--link-visited) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); + } + + .site-title a:focus { + background: linear-gradient(120deg, var(--link-color) 0%, var(--link-visited) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); + } +} + +/* Fallback for browsers without gradient text support */ .site-title a:hover { + color: var(--link-color); +} + +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; text-decoration: none; - background: linear-gradient(120deg, var(--link-color) 0%, var(--link-visited) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - transform: translateY(-1px); + color: var(--link-color); } header h1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0.5rem; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: var(--header-color); +} + +/* Progressive enhancement for header h1 gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + header h1 { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } } header p { @@ -173,6 +236,15 @@ nav a:hover::after { width: 100%; } +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +nav a:focus::after { + width: 100%; +} + /* Article */ article { margin-bottom: 3.5rem; @@ -212,6 +284,15 @@ article .meta { font-size: 0.9rem; } +/* Text browser fallback for reading time icon */ +@supports not (content: "⏱️") { + .reading-time::before { + content: "Time: "; + margin-right: 0.3rem; + font-size: 0.9rem; + } +} + /* Featured Images */ .featured-image { margin: 1.5rem 0; @@ -324,6 +405,12 @@ pre code { background-color: var(--border-color); } +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--border-color); +} + .tags-list { margin: 1.25rem 0; } @@ -353,6 +440,16 @@ footer { text-align: center; } +footer a { + outline: 2px solid transparent; + outline-offset: 2px; + transition: outline-color var(--transition); +} + +footer a:focus { + outline-color: var(--link-color); +} + /* Date headers for indexes */ .date-header { color: var(--date-color); @@ -370,11 +467,21 @@ footer { margin-bottom: 1.75rem; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } +.posts-list h2 a { + outline: 2px solid transparent; + outline-offset: 2px; + transition: outline-color var(--transition); +} + +.posts-list h2 a:focus { + outline-color: var(--link-color); +} + .summary { margin-top: 0.75rem; } @@ -446,6 +553,13 @@ li { transform: translateY(-1px); } +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--border-color); + transform: translateY(-1px); +} + .pagination .page-info { color: var(--date-color); font-size: 0.95rem; diff --git a/themes/mondrian/style.css b/themes/mondrian/style.css index fbddb1f..3405132 100644 --- a/themes/mondrian/style.css +++ b/themes/mondrian/style.css @@ -2,8 +2,26 @@ * Mondrian Theme for BSSG * Inspired by Piet Mondrian's De Stijl artwork with * geometric patterns, primary colors, and black grid lines + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Mondrian geometric aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Mondrian color scheme */ --mondrian-red: #D40920; @@ -32,10 +50,10 @@ --footer-text: var(--mondrian-white); --border-color: var(--mondrian-black); - /* Typography */ - --font-main: 'Helvetica', 'Arial', sans-serif; - --font-headings: 'Helvetica', 'Arial', sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for Mondrian geometric look */ + --font-main: 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', sans-serif; + --font-headings: 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', sans-serif; + --font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Courier New', 'Liberation Mono', monospace; /* Sizing */ --content-width: 85%; @@ -94,6 +112,23 @@ header::after { z-index: 0; } +/* TEXT BROWSER FALLBACK: Simple decoration for header */ +@supports not (position: absolute) { + header::after { + content: "■"; + position: static; + background: none; + border: none; + color: var(--mondrian-red); + width: auto; + height: auto; + z-index: auto; + font-size: 1.5rem; + display: inline-block; + margin-left: 1rem; + } +} + /* Site title */ .site-title { font-family: var(--font-headings); @@ -118,6 +153,13 @@ header::after { color: var(--accent-color); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); +} + /* Site description */ header p { margin: 0.75rem auto 0; @@ -172,6 +214,14 @@ nav a.active { color: var(--nav-hover-text); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--nav-hover-bg); + color: var(--nav-hover-text); +} + /* Content area with Mondrian-inspired layout */ main { padding: var(--content-padding); @@ -215,6 +265,19 @@ h2::after { background-color: var(--mondrian-yellow); } +/* TEXT BROWSER FALLBACK: Simple decoration for h2 */ +@supports not (position: absolute) { + h2::after { + content: " ▬"; + position: static; + background: none; + color: var(--mondrian-yellow); + width: auto; + height: auto; + font-size: 1rem; + } +} + h3 { font-size: 1.6rem; margin-top: 2rem; @@ -241,6 +304,14 @@ a:hover { text-decoration: underline; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + article { margin-bottom: 4rem; position: relative; @@ -272,6 +343,18 @@ article .meta { white-space: nowrap; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use Mondrian style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .tags { display: flex; flex-wrap: wrap; @@ -299,6 +382,15 @@ article .meta { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--mondrian-blue); + color: var(--mondrian-white); + transform: translateY(-2px); +} + .tags-list { list-style: none; padding: 0; @@ -333,6 +425,16 @@ article .meta { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--mondrian-blue); + color: var(--mondrian-white); + border-color: var(--mondrian-blue); + transform: translateY(-2px); +} + footer { background-color: var(--footer-bg); color: var(--footer-text); @@ -385,6 +487,14 @@ footer a:hover { text-decoration: none; } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--mondrian-yellow); + outline-offset: 2px; + color: var(--mondrian-yellow); + text-decoration: none; +} + .pagination { display: flex; justify-content: center; @@ -411,6 +521,16 @@ footer a:hover { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--mondrian-blue); + color: var(--mondrian-white); + border-color: var(--mondrian-blue); + transform: translateY(-2px); +} + /* Featured images - Mondrian style */ .featured-image, .index-image, @@ -459,7 +579,7 @@ figcaption, margin-bottom: 1rem; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; text-align: left; diff --git a/themes/msdos/style.css b/themes/msdos/style.css index 445d9ae..be5f5bc 100644 --- a/themes/msdos/style.css +++ b/themes/msdos/style.css @@ -1,8 +1,33 @@ /* * MS-DOS Theme for BSSG * Classic blue command line interface reminiscent of MS-DOS + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic MS-DOS aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable DOS-specific animations */ + .site-title a, + .dos-loading, + .dos-loading::after { + animation: none !important; + } +} + :root { /* MS-DOS classic colors */ --bg-color: #000088; @@ -20,10 +45,10 @@ --title-gradient-start: #ffffff; --title-gradient-end: #aaaaaa; - /* Typography */ - --font-main: 'Courier New', monospace; - --font-headings: 'Courier New', monospace; - --font-mono: 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for MS-DOS terminal look */ + --font-main: 'Consolas', 'Monaco', 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace; + --font-headings: 'Consolas', 'Monaco', 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace; + --font-mono: 'Consolas', 'Monaco', 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace; /* Sizing */ --content-width: 820px; @@ -98,6 +123,13 @@ header { animation: none; } +/* TEXT BROWSER FALLBACK: Simple prompt for site title */ +@supports not (animation: none) { + .site-title a::before { + content: "> "; + } +} + .site-title a:hover { animation: none; background: linear-gradient(to right, @@ -108,6 +140,19 @@ header { color: transparent; } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + animation: none; + background: linear-gradient(to right, + var(--title-gradient-start), + var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + .site-title a:hover::after { content: "█"; animation: blink 1s step-end infinite; @@ -178,6 +223,14 @@ nav a:hover, nav a:focus { color: var(--menu-highlight-text); } +/* ACCESSIBILITY: Enhanced focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--menu-highlight-bg); + color: var(--menu-highlight-text); +} + /* Selected menu item */ nav a.active { background-color: var(--menu-highlight-bg); @@ -211,6 +264,7 @@ h2 { margin-top: calc(var(--spacing-unit) * 3); } +.posts-list h2, h3 { font-size: 1.1rem; margin-top: calc(var(--spacing-unit) * 2.5); @@ -242,6 +296,14 @@ p:after { color: var(--text-color); } +/* TEXT BROWSER FALLBACK: Simple cursor for paragraphs */ +@supports not (animation: blink) { + p:after { + content: "_"; + animation: none; + } +} + @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } @@ -262,6 +324,13 @@ a:hover { text-decoration: underline; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-decoration: underline; +} + /* Articles with command prompt style */ article { margin-bottom: calc(var(--spacing-unit) * 3); @@ -276,6 +345,13 @@ article::before { margin-right: calc(var(--spacing-unit)); } +/* TEXT BROWSER FALLBACK: Simple prompt for articles */ +@supports not (position: relative) { + article::before { + content: "* "; + } +} + article:last-child { margin-bottom: calc(var(--spacing-unit)); border-bottom: none; @@ -323,6 +399,15 @@ article .meta { text-decoration: none; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--menu-highlight-bg); + color: var(--menu-highlight-text); + text-decoration: none; +} + .tags a::before { content: "["; } @@ -417,6 +502,15 @@ footer::before { text-decoration: none; } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--menu-highlight-bg); + color: var(--menu-highlight-text); + text-decoration: none; +} + .pagination .page-info { margin: 0 calc(var(--spacing-unit) * 1.25); font-size: 0.95rem; diff --git a/themes/museum-label/style.css b/themes/museum-label/style.css new file mode 100644 index 0000000..e9145ec --- /dev/null +++ b/themes/museum-label/style.css @@ -0,0 +1,446 @@ +/* + * Museum Label Theme for BSSG + * Gallery-inspired typography: serif reading text with restrained sans metadata. + */ + +:root { + --bg-color: #fbfaf8; + --paper-color: #ffffff; + --text-color: #1d1d1b; + --muted-text: #5a5a56; + --heading-color: #111110; + --link-color: #314f68; + --link-hover: #1f3446; + --border-color: #d9d4cb; + --accent-color: #8d7754; + --tag-bg: #f4efe5; + --tag-text: #54452e; + --quote-border: #b6aa95; + --code-bg: #f5f5f3; + --shadow: 0 3px 10px rgba(0, 0, 0, 0.06); + --radius: 6px; + --content-width: 860px; + --font-body: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif; + --font-heading: Baskerville, "Times New Roman", Times, serif; + --font-ui: "Gill Sans", "Avenir Next", "Trebuchet MS", Arial, sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 18px; +} + +body { + margin: 0; + color: var(--text-color); + background-color: var(--bg-color); + background-image: linear-gradient(180deg, #fffdf9 0%, #f7f4ee 100%); + font-family: var(--font-body); + line-height: 1.7; +} + +::selection { + background-color: #e7dbc4; + color: #1a1a18; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 2.25rem 1.4rem 3rem; +} + +header { + border-top: 2px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + padding: 1.35rem 0 1.15rem; + margin-bottom: 2.1rem; +} + +.site-title, +header h1 { + margin: 0; + color: var(--heading-color); + font-family: var(--font-heading); + font-size: clamp(2rem, 4.2vw, 2.9rem); + font-weight: 600; + letter-spacing: 0.02em; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +header p { + margin: 0.45rem 0 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +nav { + display: flex; + flex-wrap: wrap; + gap: 0.8rem 1rem; + margin-top: 1rem; +} + +nav a { + color: var(--muted-text); + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.74rem; + letter-spacing: 0.13em; + text-transform: uppercase; + border-bottom: 1px solid transparent; + padding-bottom: 0.1rem; +} + +nav a:hover, +nav a:focus { + color: var(--heading-color); + border-bottom-color: var(--heading-color); +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.8rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--paper-color); + border: 1px solid var(--border-color); + box-shadow: var(--shadow); + border-radius: var(--radius); + padding: 1.45rem 1.25rem 1.2rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--heading-color); + font-family: var(--font-heading); + line-height: 1.3; + margin: 1.35rem 0 0.85rem; +} + +h1 { font-size: clamp(1.8rem, 3.8vw, 2.45rem); } +h2 { font-size: clamp(1.45rem, 3vw, 1.85rem); } +h3 { font-size: clamp(1.2rem, 2.2vw, 1.45rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 1rem; +} + +a { + color: var(--link-color); + text-decoration-thickness: 1px; + text-underline-offset: 0.15em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.9rem; +} + +.meta { + margin: 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.73rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.reading-time { + margin-top: 0.35rem; +} + +.summary { + margin-top: 0.85rem; + color: #3d3d39; +} + +.post-content { + margin-top: 0.8rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 1.1rem 0; +} + +img { + display: block; + max-width: 100%; + height: auto; + border: 1px solid var(--border-color); + border-radius: 2px; +} + +.image-caption, +figcaption { + margin-top: 0.45rem; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +blockquote { + margin: 1.2rem 0; + border-left: 3px solid var(--quote-border); + background: #faf7f1; + padding: 0.75rem 0.95rem; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.87rem; +} + +code { + background: var(--code-bg); + padding: 0.1rem 0.3rem; + border-radius: 2px; +} + +pre { + background: var(--code-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 0.9rem; + overflow-x: auto; +} + +pre code { + padding: 0; + background: none; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + margin-top: 1.1rem; +} + +.tags a, +.tags-list a { + display: inline-block; + color: var(--tag-text); + background: var(--tag-bg); + border: 1px solid #d8cfbe; + border-radius: 999px; + padding: 0.2rem 0.56rem; + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.73rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #e8dcc7; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.tag-count { + color: var(--muted-text); +} + +.posts-list article + article { + margin-top: 1rem; +} + +.posts-list h2 { + margin-top: 0; +} + +.pagination { + margin: 1.6rem 0 0.6rem; + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + align-items: center; +} + +.pagination a { + display: inline-block; + border: 1px solid var(--border-color); + border-radius: 999px; + padding: 0.3rem 0.75rem; + text-decoration: none; + color: var(--heading-color); + font-family: var(--font-ui); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.pagination a:hover, +.pagination a:focus { + background: #f1ece2; +} + +.page-info { + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.related-posts { + margin-top: 1.8rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.1rem; +} + +.related-posts-list { + display: grid; + gap: 0.7rem; +} + +.related-post { + border-left: 3px solid var(--accent-color); + background: var(--tag-bg); + padding: 0.65rem 0.75rem; +} + +.related-post h4 { + margin: 0 0 0.3rem; +} + +.related-post p { + margin: 0; + color: #4a4a44; + font-size: 0.95rem; +} + +footer { + margin-top: 2.2rem; + border-top: 1px solid var(--border-color); + padding-top: 1rem; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.72rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +footer p { + margin: 0.4rem 0; +} + +footer a { + color: var(--muted-text); +} + +footer a:hover, +footer a:focus { + color: var(--heading-color); +} + +table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; +} + +th, +td { + border: 1px solid var(--border-color); + padding: 0.45rem 0.6rem; +} + +th { + font-family: var(--font-ui); + font-size: 0.78rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +hr { + border: 0; + border-top: 1px solid var(--border-color); + margin: 1.4rem 0; +} + +@media (max-width: 760px) { + html { + font-size: 17px; + } + + .container { + padding: 1.4rem 0.9rem 2.3rem; + } + + article.post, + article.page, + .posts-list article { + padding: 1.1rem 0.95rem; + } + + nav { + gap: 0.45rem 0.7rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.06em; + } +} diff --git a/themes/mynotes/style.css b/themes/mynotes/style.css new file mode 100644 index 0000000..38b37a1 --- /dev/null +++ b/themes/mynotes/style.css @@ -0,0 +1,573 @@ +/* + * MyNotes Theme for BSSG + * Quiet, warm, text-first journal aesthetic. + */ + +@import url("https://fonts.googleapis.com/css2?family=Literata:opsz,wght@7..72,400;7..72,500;7..72,600&family=Source+Sans+3:wght@400;500;600&display=swap"); + +:root { + --bg: #1c1917; + --bg-soft: #221e1b; + --surface: #26211e; + --text: #e8e0d4; + --text-soft: #d2c7b5; + --muted: #afa08b; + --accent: #c89452; + --accent-strong: #ddb175; + --accent-subtle: rgba(200, 148, 82, 0.32); + --border: #4a4037; + --quote-bg: #24201d; + --quote-border: #a7773d; + --code-bg: #171411; + --code-text: #f1e8db; + --code-border: #3d342c; + --shadow-soft: 0 3px 16px rgba(0, 0, 0, 0.24); + --content-width: 700px; + --font-body: "Literata", Georgia, "Times New Roman", serif; + --font-ui: "Source Sans 3", system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: light) { + :root { + --bg: #f5f0e8; + --bg-soft: #f1e9de; + --surface: #fcf8f1; + --text: #2c2520; + --text-soft: #3b322c; + --muted: #726459; + --accent: #a96d2f; + --accent-strong: #8d5520; + --accent-subtle: rgba(169, 109, 47, 0.26); + --border: #d9cdbd; + --quote-bg: #f3ebdf; + --quote-border: #b67a3f; + --code-bg: #2a241f; + --code-text: #f5ecdd; + --code-border: #3a3129; + --shadow-soft: 0 2px 12px rgba(60, 42, 22, 0.12); + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 19px; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + color: var(--text); + background: + radial-gradient(circle at 15% 8%, rgba(201, 150, 91, 0.1), transparent 34%), + linear-gradient(180deg, var(--bg-soft) 0%, var(--bg) 100%); + font-family: var(--font-body); + line-height: 1.7; +} + +::selection { + background: var(--accent-subtle); + color: var(--text); +} + +.container { + max-width: calc(var(--content-width) + 2.8rem); + margin: 0 auto; + padding: 2.2rem 1.4rem 3.2rem; +} + +header { + border-bottom: 1px solid var(--border); + margin-bottom: 2.3rem; + padding-bottom: 1.15rem; +} + +.site-title, +header h1 { + margin: 0; + color: var(--text); + font-family: var(--font-body); + font-weight: 600; + font-size: clamp(2rem, 4vw, 2.75rem); + line-height: 1.2; + letter-spacing: 0.01em; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: var(--accent-strong); +} + +header p { + margin: 0.45rem 0 0; + color: var(--muted); + font-size: 0.95rem; + font-style: italic; + line-height: 1.5; +} + +nav { + margin-top: 1rem; + display: flex; + flex-wrap: wrap; + gap: 0.45rem 0.75rem; +} + +nav a { + display: inline-block; + padding: 0.28rem 0.1rem; + color: var(--text-soft); + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.82rem; + text-transform: uppercase; + letter-spacing: 0.09em; + border-bottom: 1px solid transparent; +} + +nav a:hover, +nav a:focus { + color: var(--accent-strong); + border-bottom-color: var(--accent-subtle); +} + +main { + max-width: var(--content-width); + margin: 0 auto; + min-height: 68vh; +} + +article { + margin-bottom: 2.2rem; +} + +article.post, +article.page { + padding-bottom: 1rem; +} + +.posts-list article { + margin: 0; + padding: 0 0 1.55rem; + border-bottom: 1px solid var(--border); +} + +.posts-list article + article { + margin-top: 1.55rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--text); + font-family: var(--font-body); + line-height: 1.3; + margin: 1.45rem 0 0.8rem; + letter-spacing: 0.01em; + font-weight: 600; +} + +h1 { + font-size: clamp(1.95rem, 3.3vw, 2.45rem); +} + +h2 { + font-size: clamp(1.55rem, 2.7vw, 1.92rem); +} + +h3 { + font-size: clamp(1.25rem, 2vw, 1.48rem); +} + +article.post > h1, +article.page > h1, +.posts-list h2 { + margin-top: 0; +} + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 1.25rem; +} + +li + li { + margin-top: 0.38rem; +} + +a { + color: var(--accent); + text-decoration-line: underline; + text-decoration-thickness: 1px; + text-underline-offset: 0.16em; + text-decoration-color: rgba(200, 148, 82, 0.55); + transition: color 0.14s ease, text-decoration-color 0.14s ease; +} + +a:hover, +a:focus { + color: var(--accent-strong); + text-decoration-color: rgba(200, 148, 82, 0.95); +} + +.page-meta { + margin-bottom: 1rem; +} + +.meta { + margin: 0; + color: var(--muted); + font-family: var(--font-ui); + font-size: 0.83rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.reading-time { + margin-top: 0.28rem; +} + +.summary { + color: var(--text-soft); + margin-top: 0.6rem; +} + +.posts-list .summary { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.post-content { + margin-top: 0.75rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 1rem 0 1.05rem; +} + +.index-image { + display: none; +} + +img { + display: block; + max-width: 100%; + height: auto; + border-radius: 3px; + box-shadow: var(--shadow-soft); +} + +.image-caption, +figcaption { + margin-top: 0.45rem; + color: var(--muted); + font-size: 0.85rem; + font-style: italic; + line-height: 1.5; +} + +blockquote { + margin: 1.5rem 0; + padding: 0.9rem 1rem; + background: var(--quote-bg); + border-left: 3px solid var(--quote-border); + color: var(--text-soft); + font-style: italic; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.83rem; +} + +code { + color: var(--code-text); + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: 4px; + padding: 0.08rem 0.3rem; +} + +pre { + margin: 1.2rem 0; + color: var(--code-text); + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: 6px; + padding: 0.9rem 0.95rem; + overflow-x: auto; +} + +pre code { + border: 0; + background: transparent; + padding: 0; + color: inherit; +} + +hr { + border: 0; + margin: 2.2rem 0; + text-align: center; +} + +hr::before { + content: "\00b7\00b7\00b7"; + color: var(--muted); + letter-spacing: 0.6rem; + font-size: 1rem; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.36rem; + margin-top: 1.2rem; +} + +.tags a, +.tags-list a { + display: inline-block; + color: var(--text-soft); + background: color-mix(in srgb, var(--accent) 10%, transparent); + border: 1px solid var(--border); + border-radius: 999px; + padding: 0.14rem 0.52rem; + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.74rem; + letter-spacing: 0.04em; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + color: var(--accent-strong); + border-color: var(--accent-subtle); + background: color-mix(in srgb, var(--accent) 16%, transparent); +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.tag-count { + color: var(--muted); +} + +.pagination { + margin-top: 1.8rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.45rem; + font-family: var(--font-ui); +} + +.pagination a { + display: inline-block; + color: var(--text-soft); + background: color-mix(in srgb, var(--surface) 70%, transparent); + border: 1px solid var(--border); + border-radius: 999px; + padding: 0.16rem 0.62rem; + text-decoration: none; + font-size: 0.78rem; +} + +.pagination a:hover, +.pagination a:focus { + color: var(--accent-strong); + border-color: var(--accent-subtle); +} + +.page-info { + color: var(--muted); + font-size: 0.78rem; + letter-spacing: 0.03em; +} + +.related-posts { + margin-top: 2rem; + padding-top: 1.1rem; + border-top: 1px dashed var(--border); +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.1rem; +} + +.related-posts-list { + display: grid; + gap: 0.8rem; +} + +.related-post { + border-left: 2px solid var(--quote-border); + background: color-mix(in srgb, var(--quote-bg) 85%, transparent); + padding: 0.55rem 0.72rem; + border-radius: 4px; +} + +.related-post h4 { + margin: 0 0 0.25rem; + font-size: 0.98rem; +} + +.related-post p { + margin: 0; + color: var(--muted); + font-size: 0.88rem; + line-height: 1.55; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 1.25rem 0; +} + +th, +td { + border: 1px solid var(--border); + padding: 0.46rem 0.56rem; +} + +th { + text-align: left; + font-family: var(--font-ui); + font-size: 0.79rem; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted); +} + +footer { + max-width: var(--content-width); + margin: 2.5rem auto 0; + padding-top: 0.95rem; + border-top: 1px solid var(--border); + color: var(--muted); + font-family: var(--font-ui); + font-size: 0.77rem; + line-height: 1.5; +} + +footer p { + margin: 0.34rem 0; +} + +footer p:last-child { + display: none; +} + +footer a { + color: var(--accent); +} + +footer a:hover, +footer a:focus { + color: var(--accent-strong); +} + +@media (max-width: 760px) { + html { + font-size: 17px; + } + + .container { + padding: 1.4rem 1rem 2.4rem; + } + + header { + margin-bottom: 1.7rem; + padding-bottom: 0.95rem; + } + + nav { + gap: 0.24rem 0.6rem; + } + + nav a { + padding: 0.35rem 0.04rem; + font-size: 0.8rem; + letter-spacing: 0.06em; + } + + article { + margin-bottom: 1.8rem; + } + + .posts-list article { + padding-bottom: 1.2rem; + } +} + +@media (max-width: 520px) { + html { + font-size: 16px; + } + + .container { + padding: 1.1rem 0.82rem 2rem; + } + + nav { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.3rem 0.55rem; + } + + nav a { + min-height: 40px; + display: flex; + align-items: center; + justify-content: flex-start; + } + + .site-title, + header h1 { + font-size: clamp(1.72rem, 8.4vw, 2.2rem); + } + + .meta, + .pagination a, + .page-info { + font-size: 0.76rem; + letter-spacing: 0.02em; + } +} diff --git a/themes/nes/style.css b/themes/nes/style.css index a7c3540..f279201 100644 --- a/themes/nes/style.css +++ b/themes/nes/style.css @@ -2,13 +2,31 @@ * NES Theme for BSSG * Retro theme inspired by Nintendo Entertainment System, * using the NES color palette and pixel art aesthetics + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic NES aesthetics */ -@font-face { - font-family: 'Press Start 2P'; - src: url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); - font-weight: normal; - font-style: normal; +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable NES-specific animations */ + .site-title a:hover, + .site-title a:focus { + animation: none !important; + } } :root { @@ -48,10 +66,10 @@ --footer-text: var(--nes-light-gray); --code-bg: var(--nes-dark-gray); - /* Typography */ - --font-main: 'Press Start 2P', monospace; - --font-headings: 'Press Start 2P', monospace; - --font-mono: 'Press Start 2P', monospace; + /* Typography - Enhanced font fallbacks for NES pixelated look */ + --font-main: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + --font-headings: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + --font-mono: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; /* Sizing */ --content-width: 88%; @@ -168,6 +186,13 @@ header { animation: blink 0.5s step-end infinite alternate; } +.site-title a:focus { + outline: 3px solid var(--nes-light-yellow); + outline-offset: 3px; + color: var(--nes-light-yellow); + animation: blink 0.5s step-end infinite alternate; +} + /* Site description */ header p { margin: var(--spacing-sm) auto 0; @@ -214,6 +239,17 @@ nav a.active { inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); } +nav a:focus { + outline: 3px solid var(--nes-light-yellow); + outline-offset: 2px; + color: var(--nes-light-yellow); + background-color: var(--nes-blue); + transform: translateY(var(--pixel-size)); + box-shadow: + 0 0 0 rgba(0, 0, 0, 0.5), + inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); +} + nav a.active::before { content: "►"; position: absolute; @@ -284,6 +320,13 @@ a:hover { animation: blink 0.5s step-end infinite alternate; } +a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + color: var(--link-hover); + animation: blink 0.5s step-end infinite alternate; +} + a::before { content: ""; position: absolute; @@ -359,9 +402,17 @@ article .meta { gap: 0.5em; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏱"; - font-size: 1.2em; + content: "Time: "; +} + +/* Use NES-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱"; + font-size: 1.2em; + } } /* Tags */ @@ -389,6 +440,15 @@ article .meta { transform: translateY(2px); } +.tags a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + background-color: var(--nes-green); + color: var(--nes-black); + animation: none; + transform: translateY(2px); +} + .tags a:hover::before { content: ""; display: none; @@ -430,6 +490,17 @@ article .meta { inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); } +.tags-list a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + background-color: var(--nes-green); + color: var(--nes-black); + transform: translateY(var(--pixel-size)); + box-shadow: + 0 0 0 rgba(0, 0, 0, 0.5), + inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); +} + /* Featured images */ .featured-image, .index-image, @@ -508,7 +579,7 @@ blockquote { } blockquote::before { - content: """; + content: "\201C"; position: absolute; top: 0.1em; left: var(--spacing-sm); @@ -536,18 +607,25 @@ blockquote p:last-child { transform: scale(1.01); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.1rem; margin-bottom: var(--spacing-sm); } -.posts-list h3 a { +.posts-list h2 a { color: var(--nes-light-blue); text-decoration: none; } -.posts-list h3 a:hover { +.posts-list h2 a:hover { + color: var(--nes-light-yellow); +} + +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list h2 a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; color: var(--nes-light-yellow); } @@ -601,6 +679,12 @@ footer a:hover { color: var(--nes-light-yellow); } +footer a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + color: var(--nes-light-yellow); +} + /* Pagination */ .pagination { display: flex; @@ -631,6 +715,16 @@ footer a:hover { animation: none; } +.pagination a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + background-color: var(--nes-blue); + color: var(--nes-light-yellow); + transform: translateY(var(--pixel-size)); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.5); + animation: none; +} + .pagination a:hover::before { content: ""; display: none; diff --git a/themes/netbsd/style.css b/themes/netbsd/style.css new file mode 100644 index 0000000..b58c1e1 --- /dev/null +++ b/themes/netbsd/style.css @@ -0,0 +1,530 @@ +/* + * NetBSD Theme for BSSG + * NetBSD-recognizable orange engineering look with flag-inspired geometry. + */ + +:root { + --netbsd-navy: #003c78; + --netbsd-deep: #002a54; + --netbsd-sky: #2d5d90; + --netbsd-orange: #f4821f; + --paper: #fbfdff; + --paper-2: #f1f5f9; + --text: #163046; + --muted: #506578; + --border: #cfdae4; + --link: #004a90; + --link-hover: #00366a; + --tag-bg: #edf4fb; + --tag-text: #184b78; + --quote-bg: #f2f7fc; + --code-bg: #edf3f8; + --radius: 8px; + --shadow: 0 12px 28px rgba(0, 28, 52, 0.2); + --content-width: 920px; + --font-body: "Source Sans 3", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --font-heading: "Avenir Next", "Segoe UI", "Trebuchet MS", sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 17px; +} + +body { + margin: 0; + color: var(--text); + font-family: var(--font-body); + line-height: 1.72; + background: + radial-gradient(circle at 0 0, rgba(244, 130, 31, 0.1) 0 18%, transparent 19%), + linear-gradient(180deg, #f3f8fc 0%, #e7eef5 100%); +} + +::selection { + background: #cfe2f3; + color: #0f2a3e; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 0 1.2rem 2.7rem; + background: linear-gradient(180deg, #ffffff 0%, var(--paper) 100%); + border-left: 1px solid #c6d2dd; + border-right: 1px solid #c6d2dd; + box-shadow: var(--shadow); + min-height: 100vh; +} + +header { + margin: 0 -1.2rem 1.95rem; + padding: 1rem 1.2rem 0.95rem; + color: #f2f7fb; + background: + linear-gradient(135deg, rgba(244, 130, 31, 0.2) 0 14%, transparent 15%), + linear-gradient(180deg, var(--netbsd-navy) 0%, var(--netbsd-deep) 100%); + border-top: 6px solid var(--netbsd-orange); + border-bottom: 1px solid #0e2942; + position: relative; + overflow: hidden; +} + +header::before { + content: "Of course it runs NetBSD!"; + position: absolute; + right: 18px; + top: 12px; + background: linear-gradient(180deg, #0f447a 0%, #06315a 100%); + color: #ffe1be; + border: 1px solid rgba(255, 255, 255, 0.4); + border-bottom-color: var(--netbsd-orange); + border-radius: 4px; + padding: 0.16rem 0.55rem; + font-family: var(--font-mono); + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: none; + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.25); +} + +header::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 3px; + background: linear-gradient(90deg, transparent, var(--netbsd-orange), transparent); +} + +.site-title, +header h1 { + margin: 0; + color: #f4f8fc; + font-family: var(--font-heading); + font-size: clamp(1.9rem, 3.6vw, 2.65rem); + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; + line-height: 1.2; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: #ffd7b3; +} + +header p { + margin: 0.42rem 0 0; + color: #c7d9e8; + font-family: var(--font-mono); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +nav { + margin-top: 0.92rem; + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +nav a { + display: inline-block; + color: #e9f2f9; + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.22); + border-bottom: 2px solid rgba(244, 130, 31, 0.55); + border-radius: 5px; + padding: 0.24rem 0.6rem; + text-decoration: none; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +nav a:hover, +nav a:focus { + background: rgba(244, 130, 31, 0.18); + border-color: rgba(255, 255, 255, 0.45); + color: #fff; +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.4rem; +} + +article.post, +article.page, +.posts-list article { + background: #fff; + border: 1px solid var(--border); + border-top: 4px solid var(--netbsd-navy); + border-radius: var(--radius); + padding: 1.14rem 1rem 0.95rem; +} + +article.post::before, +article.page::before, +.posts-list article::before { + content: ""; + display: block; + height: 2px; + margin: -0.25rem -1rem 0.85rem; + background: linear-gradient(90deg, transparent, rgba(244, 130, 31, 0.62), transparent); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #102f48; + font-family: var(--font-heading); + margin: 1.08rem 0 0.68rem; + line-height: 1.3; + letter-spacing: 0.01em; +} + +h1 { font-size: clamp(1.7rem, 3.1vw, 2.22rem); } +h2 { font-size: clamp(1.38rem, 2.5vw, 1.78rem); } +h3 { font-size: clamp(1.14rem, 1.9vw, 1.38rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.92rem; +} + +a { + color: var(--link); + text-decoration-thickness: 1px; + text-underline-offset: 0.14em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.74rem; +} + +.meta { + margin: 0; + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.reading-time { + margin-top: 0.24rem; +} + +.summary { + margin-top: 0.72rem; + color: #39576f; +} + +.post-content { + margin-top: 0.64rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 0.94rem 0; +} + +img { + max-width: 100%; + height: auto; + display: block; + border: 1px solid #b9c8d6; + border-radius: 4px; +} + +.image-caption, +figcaption { + margin-top: 0.34rem; + color: #61778a; + font-size: 0.78rem; + font-style: italic; +} + +blockquote { + margin: 0.98rem 0; + padding: 0.68rem 0.85rem; + border-left: 4px solid var(--netbsd-sky); + background: var(--quote-bg); + color: #2d4a61; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.84rem; +} + +code { + background: var(--code-bg); + border: 1px solid #d3dfeb; + border-radius: 3px; + padding: 0.08rem 0.26rem; +} + +pre { + background: var(--code-bg); + border: 1px solid #d0dde8; + border-radius: 6px; + padding: 0.78rem; + overflow-x: auto; +} + +pre code { + background: none; + border: 0; + padding: 0; +} + +hr { + border: 0; + border-top: 1px solid #d2dee8; + margin: 1.1rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.34rem; + margin-top: 0.84rem; +} + +.tags a, +.tags-list a { + display: inline-block; + text-decoration: none; + color: var(--tag-text); + background: var(--tag-bg); + border: 1px solid #cadcec; + border-radius: 999px; + padding: 0.17rem 0.52rem; + font-family: var(--font-mono); + font-size: 0.71rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #deebf8; + border-color: #b8d0e7; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.42rem; +} + +.tag-count { + color: #61798f; +} + +.posts-list article + article { + margin-top: 0.82rem; +} + +.posts-list h2 { + margin-top: 0; +} + +.pagination { + margin-top: 1.2rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.44rem; +} + +.pagination a { + display: inline-block; + color: #17364f; + text-decoration: none; + border: 1px solid #c0d2e1; + border-radius: 5px; + padding: 0.22rem 0.6rem; + background: #eff5fb; + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.pagination a:hover, +.pagination a:focus { + background: var(--netbsd-navy); + color: #fff; + border-color: #1f4e7e; +} + +.page-info { + color: #5f7488; + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.related-posts { + margin-top: 1.3rem; + padding-top: 0.8rem; + border-top: 1px dashed #c2d2df; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.05rem; +} + +.related-posts-list { + display: grid; + gap: 0.6rem; +} + +.related-post { + border: 1px solid #c5d5e4; + border-left: 4px solid var(--netbsd-orange); + background: #f6fafe; + border-radius: 5px; + padding: 0.52rem 0.64rem; +} + +.related-post h4 { + margin: 0 0 0.2rem; + font-size: 0.94rem; +} + +.related-post p { + margin: 0; + color: #4c6980; + font-size: 0.9rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.9rem 0; +} + +th, +td { + border: 1px solid #d0dde8; + padding: 0.38rem 0.5rem; +} + +th { + background: #edf4fb; + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +footer { + margin-top: 1.78rem; + padding: 0.84rem 0 0.08rem; + border-top: 2px solid #d1dbe5; + color: #577084; + font-family: var(--font-mono); + font-size: 0.71rem; + text-transform: uppercase; + letter-spacing: 0.05em; + position: relative; +} + +footer p { + margin: 0.32rem 0; +} + +footer a { + color: var(--link); +} + +footer a:hover, +footer a:focus { + color: var(--link-hover); +} + +@media (max-width: 840px) { + .container { + padding: 0 0.82rem 2rem; + } + + header { + margin: 0 -0.82rem 1.45rem; + padding: 0.88rem 0.82rem 0.8rem; + } + + header::before { + right: 10px; + top: 10px; + padding: 0.12rem 0.42rem; + font-size: 0.55rem; + letter-spacing: 0.01em; + } + + nav { + padding-right: 0; + } + + article.post, + article.page, + .posts-list article { + padding: 0.96rem 0.8rem 0.8rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.02em; + } +} diff --git a/themes/newspaper/style.css b/themes/newspaper/style.css index e35b90e..fc64a97 100644 --- a/themes/newspaper/style.css +++ b/themes/newspaper/style.css @@ -1,8 +1,28 @@ /* * Newspaper Theme for BSSG * A traditional newspaper-inspired design + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Fixed gradient text accessibility with progressive enhancement + * - Optimized performance while maintaining authentic newspaper aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Improved newspaper palette */ --bg-color: #f7f4ed; @@ -22,12 +42,12 @@ --title-gradient-start: #000000; --title-gradient-end: #444444; - /* Typography - Added more authentic newspaper fonts */ - --font-main: 'Libre Baskerville', 'Georgia', 'Times New Roman', serif; - --font-headings: 'Playfair Display', 'Times New Roman', serif; - --font-mono: 'Courier New', monospace; - --font-meta: 'Georgia', 'Times New Roman', serif; - --font-masthead: 'Old English Text MT', 'Times New Roman', serif; + /* Typography - Enhanced font fallbacks for newspaper look */ + --font-main: 'Georgia', 'Times New Roman', 'Times', 'Baskerville', 'Palatino', 'Palatino Linotype', 'Book Antiqua', serif; + --font-headings: 'Times New Roman', 'Times', 'Georgia', 'Baskerville', 'Palatino', 'Palatino Linotype', 'Book Antiqua', serif; + --font-mono: 'Courier New', 'Courier', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; + --font-meta: 'Georgia', 'Times New Roman', 'Times', 'Baskerville', 'Palatino', serif; + --font-masthead: 'Times New Roman', 'Times', 'Georgia', 'Baskerville', 'Palatino', serif; /* Sizing */ --content-width: 920px; @@ -41,8 +61,7 @@ --spacing-xl: 2.5rem; } -/* Import Fonts */ -@import url('https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&display=swap'); + /* Base elements */ html { @@ -141,6 +160,13 @@ a:hover { color: #000; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: #000; +} + /* Enhanced header/masthead */ header { border-bottom: none; @@ -205,6 +231,13 @@ header::after { text-shadow: 2px 2px 3px rgba(0,0,0,0.1); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-shadow: 2px 2px 3px rgba(0,0,0,0.1); +} + /* Fallback for browsers that don't support background-clip */ @supports not (background-clip: text) { .site-title a { @@ -278,6 +311,13 @@ nav a:hover { border-bottom: 2px solid var(--border-color); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + border-bottom: 2px solid var(--border-color); +} + /* Article */ article { margin-bottom: var(--spacing-xl); @@ -325,6 +365,18 @@ article .meta { margin-left: var(--spacing-sm); } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use newspaper-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Code */ code { font-family: var(--font-mono); @@ -374,6 +426,13 @@ pre code { background-color: #d9d6cd; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: #d9d6cd; +} + .tags-list { margin: var(--spacing-md) 0; text-align: center; @@ -470,7 +529,7 @@ footer::after { margin-bottom: var(--spacing-lg); } -.posts-list h3 { +.posts-list h2 { margin-top: var(--spacing-xs); font-size: 1.6rem; text-transform: uppercase; @@ -479,13 +538,20 @@ footer::after { text-align: center; } -.posts-list h3 a { +.posts-list h2 a { color: var(--header-color); text-decoration: none; transition: color 0.2s; } -.posts-list h3 a:hover { +.posts-list h2 a:hover { + color: var(--link-color); +} + +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list h2 a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; color: var(--link-color); } @@ -544,6 +610,13 @@ li { background-color: var(--tag-bg); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-bg); +} + .pagination .page-info { display: inline-block; padding: var(--spacing-xs) var(--spacing-md); diff --git a/themes/nextstep/style.css b/themes/nextstep/style.css index e1ddf00..46411ef 100644 --- a/themes/nextstep/style.css +++ b/themes/nextstep/style.css @@ -1,8 +1,27 @@ /* * NeXTSTEP Theme for BSSG * Recreating the iconic NeXTSTEP GUI from the late 80s/early 90s - the predecessor to macOS + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Fixed gradient text accessibility with progressive enhancement + * - Optimized performance while maintaining authentic NeXTSTEP aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* NeXTSTEP color scheme */ --bg-color: #bfbfbf; @@ -24,10 +43,10 @@ --title-gradient-start: #ffffff; --title-gradient-end: #cccccc; - /* Typography */ - --font-main: 'Helvetica', 'Arial', sans-serif; - --font-headings: 'Helvetica', 'Arial', sans-serif; - --font-mono: 'Courier', monospace; + /* Typography - Enhanced font fallbacks for NeXTSTEP look */ + --font-main: 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', 'Cantarell', sans-serif; + --font-headings: 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', 'Cantarell', sans-serif; + --font-mono: 'Courier New', 'Courier', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; /* Sizing */ --content-width: 820px; @@ -96,8 +115,9 @@ header { cursor: pointer; } +/* TEXT BROWSER FALLBACK: Simple text for window controls */ .next-minimize::after { - content: "_"; + content: "-"; color: black; position: relative; top: -3px; @@ -110,11 +130,22 @@ header { } .next-close::after { - content: "×"; + content: "X"; color: black; font-weight: bold; } +/* Use NeXTSTEP-style symbols when CSS positioning is supported (modern browsers) */ +@supports (position: relative) { + .next-minimize::after { + content: "_"; + } + + .next-close::after { + content: "×"; + } +} + /* NeXTSTEP site title with gradient */ .site-title { margin: 0; @@ -154,6 +185,20 @@ header { } } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--title-gradient-start); + outline-offset: 2px; + text-decoration: none; + background: linear-gradient(to bottom, + var(--title-gradient-start), + var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.9); +} + header h1 { margin: 0; padding: 0; @@ -167,9 +212,8 @@ nav { background-color: var(--window-bg); border-bottom: var(--border-width) solid var(--border-color); display: flex; - flex-wrap: nowrap; + flex-wrap: wrap; padding: 0; - overflow-x: auto; } nav a { @@ -190,10 +234,19 @@ nav a:hover, nav a:focus { color: var(--title-text); } -/* Selected menu item */ +/* ACCESSIBILITY: Enhanced focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Selected menu item - Enhanced contrast for better visibility */ nav a.active { background-color: var(--title-bar); color: var(--title-text); + font-weight: bold; + border-left: 2px solid var(--title-text); + border-right: 2px solid var(--title-text); } /* Content area */ @@ -222,6 +275,7 @@ h2 { font-size: 1.6rem; } +.posts-list h2, h3 { font-size: 1.4rem; } @@ -253,6 +307,13 @@ a:hover { text-decoration: underline; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-decoration: underline; +} + /* NeXTSTEP style button */ .next-button { background-color: var(--button); @@ -272,6 +333,14 @@ a:hover { color: var(--title-text); } +/* ACCESSIBILITY: Focus states for NeXTSTEP buttons */ +.next-button:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--title-bar); + color: var(--title-text); +} + /* Articles with borders */ article { margin-bottom: var(--spacing-xl); @@ -298,6 +367,18 @@ article .meta { font-style: italic; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use NeXTSTEP-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .tags { display: flex; flex-wrap: wrap; @@ -320,6 +401,15 @@ article .meta { text-decoration: none; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--title-bar); + color: var(--title-text); + text-decoration: none; +} + .tags-list { list-style-type: none; padding: 0; @@ -448,11 +538,28 @@ footer { text-decoration: none; } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--title-bar); + color: var(--title-text); + text-decoration: none; +} + .pagination .page-info { margin: 0 var(--spacing-sm); font-size: 0.9rem; } +/* Media query for medium screens */ +@media (max-width: 1024px) { + nav a { + padding: var(--spacing-xs) var(--spacing-sm); + font-size: 0.85rem; + } +} + /* Media query for responsive design */ @media (max-width: 768px) { .container { @@ -462,16 +569,17 @@ footer { } nav { - flex-direction: row; - flex-wrap: wrap; + flex-direction: column; + flex-wrap: nowrap; } nav a { border-right: none; border-bottom: var(--border-width) solid var(--border-color); - flex: 1 0 auto; - text-align: center; - padding: var(--spacing-sm) var(--spacing-xs); + width: 100%; + text-align: left; + padding: var(--spacing-sm) var(--spacing-sm); + white-space: normal; } footer { diff --git a/themes/nordic-clean/style.css b/themes/nordic-clean/style.css index c677233..b8bae61 100644 --- a/themes/nordic-clean/style.css +++ b/themes/nordic-clean/style.css @@ -2,6 +2,12 @@ * Nordic Clean Theme for BSSG * Inspired by Scandinavian design principles: * Minimalism, airiness, natural colors, and clean typography + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks + * - Optimized performance while maintaining design */ :root { @@ -37,10 +43,10 @@ --footer-text: var(--dark-gray); --border-color: var(--light-gray); - /* Typography */ - --font-main: 'Helvetica Neue', Arial, sans-serif; - --font-headings: 'Helvetica Neue', Arial, sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-headings: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-mono: 'Courier New', 'Courier', monospace; /* Sizing */ --content-width: 820px; @@ -54,6 +60,45 @@ --spacing-xl: 3rem; --spacing-xxl: 4rem; --border-radius: 4px; + + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s; + --transition-base: 0.2s; + --transition-slow: 0.3s; +} + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + nav a::after { + transition: none !important; + } + + .featured-image img, + .index-image img, + .tag-image img, + .archive-image img { + transition: none !important; + } + + .featured-image:hover img, + .index-image:hover img, + .tag-image:hover img, + .archive-image:hover img { + transform: none !important; + } + + .tags-list a:hover, + .pagination a:hover { + transform: none !important; + } } /* Base elements */ @@ -109,13 +154,21 @@ header { .site-title a { color: var(--header-text); text-decoration: none; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .site-title a:hover { color: var(--accent-color); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Site description */ header p { margin: var(--spacing-sm) 0 0; @@ -140,7 +193,9 @@ nav a { font-size: 0.95rem; position: relative; padding-bottom: 2px; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a::after { @@ -153,7 +208,7 @@ nav a::after { background-color: var(--accent-color); transform: scaleX(0); transform-origin: bottom right; - transition: transform 0.3s ease; + transition: transform var(--transition-slow) ease; } nav a:hover { @@ -171,6 +226,12 @@ nav a.active { font-weight: 500; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Content area */ main { min-height: 60vh; @@ -216,8 +277,10 @@ p { a { color: var(--link-color); text-decoration: none; - transition: color 0.2s ease, border-bottom 0.2s ease; + transition: color var(--transition-base) ease, border-bottom var(--transition-base) ease; border-bottom: 1px solid transparent; + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:visited { @@ -229,6 +292,12 @@ a:hover { border-bottom: 1px solid var(--link-hover); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Articles with ample whitespace */ article { margin-bottom: var(--spacing-xl); @@ -254,13 +323,25 @@ article .meta { align-items: center; } -/* Reading time */ +/* Reading time - TEXT BROWSER FALLBACK */ .reading-time { position: relative; display: inline-flex; align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags */ .tags { display: flex; @@ -275,7 +356,9 @@ article .meta { font-size: 0.8rem; border-radius: var(--border-radius); border: none; - transition: background-color 0.2s ease, color 0.2s ease; + transition: background-color var(--transition-base) ease, color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags a:hover { @@ -284,6 +367,12 @@ article .meta { border: none; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Tags list page */ .tags-list { list-style-type: none; @@ -307,7 +396,9 @@ article .meta { border-radius: var(--border-radius); text-decoration: none; border: none; - transition: all 0.2s ease; + transition: all var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags-list a:hover { @@ -317,6 +408,12 @@ article .meta { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Footer */ footer { margin-top: var(--spacing-xl); @@ -336,7 +433,9 @@ footer a { color: var(--secondary-accent); text-decoration: none; border: none; - transition: color 0.2s ease, border-bottom 0.2s ease; + transition: color var(--transition-base) ease, border-bottom var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } footer a:hover { @@ -344,6 +443,12 @@ footer a:hover { border-bottom: 1px solid var(--accent-color); } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Pagination */ .pagination { display: flex; @@ -362,7 +467,9 @@ footer a:hover { font-size: 0.9rem; border-radius: var(--border-radius); border: none; - transition: all 0.2s ease; + transition: all var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a:hover { @@ -372,6 +479,12 @@ footer a:hover { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + .pagination .page-info { color: var(--secondary-accent); font-size: 0.9rem; @@ -400,7 +513,7 @@ footer a:hover { height: auto; border-radius: var(--border-radius); margin: 0 auto; - transition: transform 0.3s ease; + transition: transform var(--transition-slow) ease; } .featured-image:hover img, @@ -431,7 +544,7 @@ footer a:hover { padding-bottom: 0; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; margin-bottom: var(--spacing-sm); @@ -441,6 +554,12 @@ footer a:hover { margin-bottom: var(--spacing-md); } +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Blockquote styling */ blockquote { margin: var(--spacing-lg) 0; diff --git a/themes/openbsd/style.css b/themes/openbsd/style.css new file mode 100644 index 0000000..9d3f3cf --- /dev/null +++ b/themes/openbsd/style.css @@ -0,0 +1,530 @@ +/* + * OpenBSD Theme for BSSG + * Puffy-inspired yellow/black visual language with bold outlines. + */ + +:root { + --obsd-yellow: #d7c06f; + --obsd-yellow-deep: #b79a4a; + --obsd-black: #121212; + --obsd-ink: #212121; + --obsd-paper: #f8f4e7; + --obsd-paper-2: #efe6c9; + --text: #1f2326; + --muted: #5f6468; + --border: #2a2a2a; + --link: #0d4f9e; + --link-hover: #08396f; + --tag-bg: #e9ddb5; + --tag-text: #2b2b2b; + --quote-bg: #f1e8cb; + --code-bg: #ece1bd; + --radius: 9px; + --shadow: 0 14px 30px rgba(0, 0, 0, 0.22); + --content-width: 900px; + --font-body: "Verdana", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --font-heading: "Trebuchet MS", "Verdana", "Segoe UI", sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 17px; +} + +body { + margin: 0; + color: var(--text); + font-family: var(--font-body); + line-height: 1.7; + background: + radial-gradient(circle at 2px 2px, rgba(18, 18, 18, 0.12) 1px, transparent 1px), + linear-gradient(180deg, #eee5c8 0%, var(--obsd-yellow) 52%, #c5af66 100%); + background-size: 12px 12px, 100% 100%; +} + +::selection { + background: #101010; + color: #f5edc9; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 0 1rem 2.5rem; + background: linear-gradient(180deg, #fbf8ef 0%, var(--obsd-paper) 100%); + border-left: 3px solid var(--obsd-black); + border-right: 3px solid var(--obsd-black); + box-shadow: var(--shadow); + min-height: 100vh; +} + +header { + margin: 0 -1rem 1.8rem; + padding: 1rem 1rem 0.92rem; + background: + repeating-linear-gradient( + -45deg, + #2c2c2c 0 8px, + #202020 8px 16px + ); + color: #fff5ce; + border-top: 8px solid #000; + border-bottom: 4px solid #000; + position: relative; + overflow: hidden; +} + +header::before { + content: "OpenBSD: Secure By Default"; + position: absolute; + right: 12px; + top: 12px; + background: linear-gradient(180deg, #e9d896 0%, #c8ad5f 100%); + color: #141414; + border: 2px solid #000; + border-radius: 6px; + padding: 0.16rem 0.54rem; + font-family: var(--font-mono); + font-size: 0.58rem; + font-weight: 700; + text-transform: none; + letter-spacing: 0.01em; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.28); +} + +header::after { + content: ""; + position: absolute; + inset: auto 0 0; + height: 3px; + background: linear-gradient(90deg, transparent, #e7d189, transparent); +} + +.site-title, +header h1 { + margin: 0; + color: #f2e4b8; + font-family: var(--font-heading); + font-size: clamp(1.9rem, 3.7vw, 2.6rem); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + line-height: 1.2; + text-shadow: + -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: #fff; +} + +header p { + margin: 0.4rem 0 0; + color: #e4d093; + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +nav { + margin-top: 0.88rem; + display: flex; + flex-wrap: wrap; + gap: 0.44rem; +} + +nav a { + display: inline-block; + color: #1a1a1a; + background: linear-gradient(180deg, #e8d790 0%, #bfa357 100%); + border: 2px solid #000; + border-radius: 999px; + padding: 0.2rem 0.58rem; + text-decoration: none; + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 700; +} + +nav a:hover, +nav a:focus { + background: linear-gradient(180deg, #f2e5ba 0%, #cbb06a 100%); + transform: translateY(-1px); +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.35rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--obsd-paper); + border: 2px solid var(--obsd-black); + border-radius: var(--radius); + box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.22); + padding: 1.06rem 0.95rem 0.9rem; + position: relative; +} + +article.post::before, +article.page::before, +.posts-list article::before { + content: none; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #111; + font-family: var(--font-heading); + margin: 1rem 0 0.64rem; + line-height: 1.3; +} + +h1 { font-size: clamp(1.7rem, 3vw, 2.22rem); } +h2 { font-size: clamp(1.36rem, 2.4vw, 1.76rem); } +h3 { font-size: clamp(1.12rem, 1.9vw, 1.34rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.9rem; +} + +a { + color: var(--link); + text-decoration-thickness: 1px; + text-underline-offset: 0.13em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.72rem; +} + +.meta { + margin: 0; + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.reading-time { + margin-top: 0.22rem; +} + +.summary { + margin-top: 0.68rem; + color: #434a50; +} + +.post-content { + margin-top: 0.62rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 0.9rem 0; +} + +img { + max-width: 100%; + height: auto; + display: block; + border: 2px solid #111; + border-radius: 6px; +} + +.image-caption, +figcaption { + margin-top: 0.32rem; + color: #5f676e; + font-size: 0.77rem; + font-style: italic; +} + +blockquote { + margin: 0.95rem 0; + padding: 0.65rem 0.8rem; + border-left: 5px solid #111; + background: var(--quote-bg); + color: #2e3438; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.83rem; +} + +code { + background: var(--code-bg); + border: 1px solid #b89d55; + border-radius: 3px; + padding: 0.08rem 0.24rem; +} + +pre { + background: var(--code-bg); + border: 2px solid #111; + border-radius: 6px; + padding: 0.75rem; + overflow-x: auto; +} + +pre code { + background: none; + border: 0; + padding: 0; +} + +hr { + border: 0; + border-top: 2px dashed #3b3b3b; + margin: 1.08rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.34rem; + margin-top: 0.82rem; +} + +.tags a, +.tags-list a { + display: inline-block; + text-decoration: none; + color: var(--tag-text); + background: var(--tag-bg); + border: 2px solid #111; + border-radius: 999px; + padding: 0.16rem 0.5rem; + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 700; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #e4d091; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.tag-count { + color: #3f3f3f; +} + +.posts-list article + article { + margin-top: 0.78rem; +} + +.posts-list h2 { + margin-top: 0; +} + +.pagination { + margin-top: 1.12rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.42rem; +} + +.pagination a { + display: inline-block; + color: #131313; + text-decoration: none; + border: 2px solid #000; + border-radius: 999px; + padding: 0.2rem 0.56rem; + background: linear-gradient(180deg, #e8d994 0%, #c2a85e 100%); + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.03em; + font-weight: 700; +} + +.pagination a:hover, +.pagination a:focus { + background: linear-gradient(180deg, #f1e4bc 0%, #cfb471 100%); +} + +.page-info { + color: #52595f; + font-family: var(--font-mono); + font-size: 0.71rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.related-posts { + margin-top: 1.2rem; + padding-top: 0.74rem; + border-top: 2px dashed #222; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.02rem; +} + +.related-posts-list { + display: grid; + gap: 0.56rem; +} + +.related-post { + border: 2px solid #111; + border-left-width: 6px; + border-radius: 6px; + background: #f3ead1; + padding: 0.5rem 0.62rem; +} + +.related-post h4 { + margin: 0 0 0.18rem; + font-size: 0.93rem; +} + +.related-post p { + margin: 0; + color: #474e53; + font-size: 0.89rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.88rem 0; +} + +th, +td { + border: 2px solid #111; + padding: 0.36rem 0.48rem; +} + +th { + background: #ddca88; + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +footer { + margin-top: 1.72rem; + padding: 0.82rem 0 0.1rem; + border-top: 3px solid #111; + color: #4f565c; + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05em; + position: relative; +} + +footer p { + margin: 0.31rem 0; +} + +footer a { + color: var(--link); +} + +footer a:hover, +footer a:focus { + color: var(--link-hover); +} + +@media (max-width: 840px) { + .container { + padding: 0 0.78rem 1.95rem; + } + + header { + margin: 0 -0.78rem 1.42rem; + padding: 0.85rem 0.78rem 0.74rem; + } + + header::before { + right: 10px; + top: 10px; + padding: 0.12rem 0.42rem; + font-size: 0.5rem; + letter-spacing: 0; + } + + nav { + padding-right: 0; + } + + article.post, + article.page, + .posts-list article { + padding: 0.92rem 0.78rem 0.76rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.02em; + } +} diff --git a/themes/osx/style.css b/themes/osx/style.css index 65e23d3..fd1f2a5 100644 --- a/themes/osx/style.css +++ b/themes/osx/style.css @@ -3,6 +3,34 @@ * Styled after macOS Sonoma aesthetics */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable all hover transforms */ + .site-title a:hover, + .site-title a:focus, + .hover-float:hover, + .posts-list article:hover, + .tags a:hover, + .tags a:focus, + .pagination a:hover, + .pagination a:focus, + .mac-button:hover, + .featured-image:hover, + .index-image:hover, + .tag-image:hover, + .archive-image:hover { + transform: none !important; + } +} + :root { /* macOS Sonoma color scheme */ --bg-color: #f5f5f7; @@ -65,14 +93,29 @@ body { margin: var(--spacing-xl) auto; padding: var(--spacing-xl) var(--spacing-lg); background-color: var(--window-bg); - backdrop-filter: blur(var(--frosted-blur)); - -webkit-backdrop-filter: blur(var(--frosted-blur)); box-shadow: var(--card-shadow); border-radius: var(--radius); border: 1px solid var(--border-color); animation: fadeIn 0.3s ease-out; } +/* Progressive enhancement for backdrop-filter */ +@supports (backdrop-filter: blur(10px)) { + .container { + backdrop-filter: blur(10px); /* Reduced from 20px for better performance */ + -webkit-backdrop-filter: blur(10px); + } +} + +/* Disable backdrop-filter on mobile for better performance */ +@media (max-width: 768px) { + .container { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + background-color: var(--window-bg-solid); /* Use solid background on mobile */ + } +} + /* Header styling */ header { margin-bottom: var(--spacing-xl); @@ -115,6 +158,19 @@ header { background-clip: text; } +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 3px; + text-decoration: none; + transform: translateY(-1px); + text-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + background: linear-gradient(110deg, + var(--title-gradient-end), + var(--title-gradient-start)); + -webkit-background-clip: text; + background-clip: text; +} + /* Fallback for browsers that don't support background-clip */ @supports not (background-clip: text) { .site-title a { @@ -180,7 +236,14 @@ nav a:hover { color: var(--accent-color); } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); +} + nav a:hover::after, +nav a:focus::after, nav a.active::after { width: 100%; } @@ -247,6 +310,13 @@ a:hover { text-decoration: underline; } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + /* Buttons */ .mac-button { display: inline-block; @@ -340,6 +410,15 @@ article .meta { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--border-color); + transform: translateY(-2px); + text-decoration: none; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -423,16 +502,16 @@ footer { border-color: var(--border-color); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: var(--spacing-xs); } -.posts-list h3 a { +.posts-list h2 a { color: var(--header-color); } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--accent-color); text-decoration: none; } @@ -583,6 +662,14 @@ hr { transform: translateY(-2px); } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--code-bg); + border-color: var(--accent-color); + transform: translateY(-2px); +} + .pagination .page-info { color: var(--text-secondary); font-size: 0.9rem; diff --git a/themes/reader-mode/style.css b/themes/reader-mode/style.css index b807367..4d14d96 100644 --- a/themes/reader-mode/style.css +++ b/themes/reader-mode/style.css @@ -3,6 +3,12 @@ * Simulates browser reader mode with emphasis on text, * white/sepia background, very readable serif font, * and almost no graphic elements + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks + * - Optimized performance while maintaining reading focus */ :root { @@ -18,10 +24,10 @@ --blockquote-bg: #f4ece0; --code-bg: #f4ece0; - /* Typography */ + /* Typography - ENHANCED fallbacks for text browsers */ --font-serif: 'Georgia', 'Times New Roman', serif; --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; - --font-mono: 'Consolas', 'Menlo', monospace; + --font-mono: 'Consolas', 'Menlo', 'Courier New', 'Courier', monospace; /* Sizing for optimal reading */ --content-width: 90%; @@ -34,6 +40,25 @@ --spacing-xxl: 4rem; --line-height: 1.7; --border-radius: 2px; + + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s; + --transition-base: 0.2s; +} + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + html { + scroll-behavior: auto !important; + } } /* Base elements */ @@ -42,6 +67,13 @@ html { scroll-behavior: smooth; } +/* ACCESSIBILITY: Respect reduced motion preference for scroll behavior */ +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} + body { font-family: var(--font-serif); background-color: var(--background-color); @@ -82,13 +114,21 @@ header { .site-title a { color: var(--heading-color); text-decoration: none; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .site-title a:hover { color: var(--link-color); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Site description */ header p { margin: var(--spacing-xs) 0 0; @@ -117,8 +157,10 @@ nav a { color: var(--muted-color); text-decoration: none; padding: var(--spacing-xs) 0; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; position: relative; + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a:hover, @@ -129,6 +171,12 @@ nav a.active { text-underline-offset: 0.2em; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Content area */ main { min-height: 70vh; @@ -196,7 +244,9 @@ a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.2em; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:hover { @@ -207,6 +257,12 @@ a:visited { color: var(--link-visited); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Lists */ ul, ol { margin: var(--spacing-md) auto var(--spacing-lg); @@ -314,18 +370,27 @@ article .meta, line-height: 1.5; } -/* Reading time */ +/* Reading time - TEXT BROWSER FALLBACK */ .reading-time { display: flex; align-items: center; gap: 0.3em; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "•"; + content: "Time: "; font-style: normal; } +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "•"; + font-style: normal; + } +} + /* Simplified tags */ .tags { display: flex; @@ -339,13 +404,21 @@ article .meta, color: var(--muted-color); font-style: normal; font-size: 0.85rem; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags a:hover { color: var(--link-color); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Tags list page */ .tags-list { list-style: none; @@ -366,7 +439,9 @@ article .meta, color: var(--text-color); text-decoration: none; font-size: 0.9rem; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags-list a:hover { @@ -374,6 +449,12 @@ article .meta, text-decoration: underline; } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Minimal featured images */ .featured-image, .index-image, @@ -444,23 +525,31 @@ figcaption { border-bottom: none; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; text-align: center; margin-bottom: var(--spacing-sm); } -.posts-list h3 a { +.posts-list h2 a { color: var(--heading-color); text-decoration: none; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--link-color); text-decoration: underline; } +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list h2 a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Minimal footer */ footer { margin-top: var(--spacing-xxl); @@ -480,13 +569,21 @@ footer p { footer a { color: var(--muted-color); text-decoration: underline; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } footer a:hover { color: var(--link-color); } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Pagination - simplified */ .pagination { display: flex; @@ -502,7 +599,9 @@ footer a:hover { color: var(--muted-color); text-decoration: none; padding: var(--spacing-xs) var(--spacing-sm); - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a:hover { @@ -510,6 +609,12 @@ footer a:hover { text-decoration: underline; } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + .pagination .page-info { color: var(--muted-color); font-style: italic; @@ -543,7 +648,9 @@ footer a:hover { cursor: pointer; border-radius: var(--border-radius); color: var(--muted-color); - transition: all 0.2s ease; + transition: all var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .theme-toggle button:hover { @@ -557,6 +664,12 @@ footer a:hover { font-weight: bold; } +/* ACCESSIBILITY: Focus states for theme toggle buttons */ +.theme-toggle button:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Dark mode */ .theme-dark { --background-color: #1a1a1a; diff --git a/themes/skeuomorphic/style.css b/themes/skeuomorphic/style.css index 7e1e78c..5192c52 100644 --- a/themes/skeuomorphic/style.css +++ b/themes/skeuomorphic/style.css @@ -2,8 +2,34 @@ * Skeuomorphic Theme for BSSG * Featuring realistic textures, 3D effects, and shadows * Inspired by early iOS (pre-iOS 7) and realistic UI elements + * IMPROVED: Better accessibility, performance, and text browser support */ +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects */ + .container, .inner-container, header, nav a, .skeu-button, .pagination a { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important; + } + + /* Simplify backgrounds for reduced motion */ + body, .inner-container, header { + background-image: none !important; + } + + /* Simplify hover effects */ + nav a:hover, .skeu-button:hover, .pagination a:hover { + transform: none !important; + } +} + :root { /* Skeuomorphic color scheme */ --leather-dark: #8B2E00; @@ -34,10 +60,10 @@ --title-gradient-middle: #f8f3e9; --title-gradient-end: #c8b8a5; - /* Typography */ - --font-main: 'Helvetica Neue', Helvetica, Arial, sans-serif; - --font-headings: 'Helvetica Neue', Helvetica, Arial, sans-serif; - --font-mono: 'Menlo', 'Courier New', monospace; + /* Typography - IMPROVED fallbacks for text browsers */ + --font-main: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; + --font-headings: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; + --font-mono: 'Menlo', 'SF Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', 'Courier New', monospace; /* Sizing */ --content-width: 820px; @@ -52,17 +78,29 @@ --spacing-xl: 30px; --spacing-xxl: 40px; - /* Transition speeds */ - --transition-fast: 0.2s; - --transition-medium: 0.3s; - --transition-slow: 0.5s; + /* Transition speeds - OPTIMIZED for performance */ + --transition-fast: 0.15s; /* Reduced for better performance */ + --transition-medium: 0.2s; /* Reduced for better performance */ + --transition-slow: 0.3s; /* Reduced for better performance */ } -/* Base elements */ +/* Base elements - OPTIMIZED background */ body { font-family: var(--font-main); background-color: var(--leather-medium); - background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%239C602F' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E"); + /* OPTIMIZED: Simplified pattern for better performance */ + background-image: linear-gradient(45deg, + var(--leather-medium) 25%, + transparent 25%, + transparent 75%, + var(--leather-medium) 75%), + linear-gradient(45deg, + var(--leather-medium) 25%, + transparent 25%, + transparent 75%, + var(--leather-medium) 75%); + background-size: 20px 20px; + background-position: 0 0, 10px 10px; color: var(--text-color); margin: 0; padding: 20px; @@ -70,20 +108,21 @@ body { font-size: 14px; } -/* Container styled as a leather-bound book */ +/* Container styled as a leather-bound book - OPTIMIZED */ .container { max-width: var(--content-width); margin: var(--spacing-lg) auto; background-color: var(--leather-dark); border-radius: var(--border-radius); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.5); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); overflow: hidden; position: relative; border: 1px solid var(--leather-dark); padding: var(--spacing-lg); } -/* Leather binding effect with stitching */ +/* OPTIMIZED: Simplified leather binding effect */ .container::before { content: ""; position: absolute; @@ -93,7 +132,7 @@ body { width: 2px; background-color: var(--stitching); z-index: 2; - box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); /* Reduced shadow */ } .container::after { @@ -103,42 +142,45 @@ body { bottom: 0; left: 15px; width: 20px; - background: repeating-linear-gradient( - 0deg, - var(--stitching), - var(--stitching) 5px, - transparent 5px, - transparent 15px - ); + /* SIMPLIFIED: Basic pattern instead of complex repeating gradient */ + background: linear-gradient(to bottom, + var(--stitching) 0%, + transparent 20%, + transparent 80%, + var(--stitching) 100%); z-index: 1; - opacity: 0.5; + opacity: 0.3; /* Reduced opacity for performance */ } -/* Inner container for content with paper texture */ +/* Inner container for content with paper texture - OPTIMIZED */ .inner-container { background-color: var(--paper-bg); border-radius: var(--small-radius); padding: 1px; position: relative; margin-left: var(--spacing-lg); - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); - background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23d1cec7' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E"); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + /* SIMPLIFIED: Basic texture pattern */ + background-image: radial-gradient(circle at 20% 50%, rgba(209, 206, 199, 0.1) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(209, 206, 199, 0.1) 0%, transparent 50%); } -/* Polished wood header */ +/* Polished wood header - OPTIMIZED */ header { background: linear-gradient(to bottom, var(--wood-light) 0%, var(--wood-medium) 50%, var(--wood-dark) 100%); color: white; padding: var(--spacing-md) var(--spacing-lg); margin: 0; position: relative; - text-shadow: 0 2px 3px rgba(0, 0, 0, 0.5); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); /* Reduced shadow */ border-bottom: 1px solid var(--wood-dark); border-radius: var(--small-radius) var(--small-radius) 0 0; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); } -/* Wood grain texture for header */ +/* OPTIMIZED: Simplified wood grain texture */ header::before { content: ""; position: absolute; @@ -146,12 +188,16 @@ header::before { right: 0; bottom: 0; left: 0; - background-image: url("data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h200v200H0V0zm160 160h40v40h-40v-40zm-20-40h20v20h-20v-20zm40 20h20v20h-20v-20zM20 120h20v20H20v-20zm20-40h20v20H40V80zm60 40h20v20h-20v-20zm-40-40h20v20H60V80zm60 0h20v20h-20V80zm20-40h20v20h-20V40zm-20 0h20v20h-20V40zM40 40h20v20H40V40zm40-20h20v20H80V20zm40 0h20v20h-20V20zm20 20h20v20H20V20zm80 160h20v20h-20v-20z' fill='%23825333' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E"); + /* SIMPLIFIED: Basic wood grain pattern */ + background-image: linear-gradient(90deg, + transparent 0%, + rgba(130, 83, 51, 0.1) 50%, + transparent 100%); border-radius: var(--small-radius) var(--small-radius) 0 0; - opacity: 0.5; + opacity: 0.3; /* Reduced opacity */ } -/* Embossed site title with gradient and beveled 3D effect */ +/* Site title with improved accessibility - IMPROVED */ .site-title { margin: 0; padding: 0; @@ -162,24 +208,41 @@ header::before { } .site-title a { - color: var(--text-color); + color: var(--text-color); /* Fallback for text browsers */ text-decoration: none; - background: linear-gradient(to bottom, - var(--title-gradient-start), - var(--title-gradient-middle) 50%, - var(--title-gradient-end)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; display: inline-block; position: relative; - text-shadow: - 0 1px 0 rgba(255, 255, 255, 0.8), - 0 -1px 1px rgba(0, 0, 0, 0.5); + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6), 0 -1px 1px rgba(0, 0, 0, 0.3); letter-spacing: 0.03em; + transition: all var(--transition-medium); } -/* Beveled edge effect */ +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(to bottom, + var(--title-gradient-start), + var(--title-gradient-middle) 50%, + var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + +.site-title a:hover { + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8), 0 -1px 1px rgba(0, 0, 0, 0.5); + transform: translateY(-1px); /* Reduced transform for better performance */ +} + +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8), 0 -1px 1px rgba(0, 0, 0, 0.5); + transform: translateY(-1px); +} + +/* OPTIMIZED: Simplified beveled edge effect */ .site-title a::before { content: ""; position: absolute; @@ -190,37 +253,25 @@ header::before { z-index: -1; border-radius: 4px; background: var(--wood-medium); - box-shadow: - inset 0 2px 3px rgba(255, 255, 255, 0.4), - inset 0 -2px 3px rgba(0, 0, 0, 0.4); + /* SIMPLIFIED: Basic bevel effect */ + box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.3), inset 0 -1px 2px rgba(0, 0, 0, 0.3); opacity: 0; - transition: opacity 0.3s ease; + transition: opacity var(--transition-medium); } .site-title a:hover::before { - opacity: 1; + opacity: 0.5; /* Reduced opacity for better performance */ } -/* Fallback for browsers that don't support background-clip */ +/* FALLBACK: For browsers that don't support background-clip */ @supports not (background-clip: text) { .site-title a { - color: var(--wood-light); - text-shadow: - 0 1px 0 rgba(255, 255, 255, 0.3), - 0 -1px 1px rgba(0, 0, 0, 0.5); + color: var(--text-color) !important; + background: none !important; } } -header h1 { - margin: 0; - padding: 0; - font-size: 1.8rem; - font-weight: normal; - position: relative; - z-index: 1; -} - -/* Navigation with iOS-style tabs */ +/* Navigation with iOS-style tabs - IMPROVED accessibility */ nav { background: linear-gradient(to bottom, var(--paper-bg) 0%, var(--paper-dark) 100%); padding: var(--spacing-sm) var(--spacing-sm) 5px; @@ -229,7 +280,8 @@ nav { overflow-x: auto; justify-content: space-around; border-bottom: 1px solid var(--wood-dark); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); position: relative; } @@ -244,7 +296,8 @@ nav a { display: inline-block; border-radius: 5px; background: linear-gradient(to bottom, var(--button-top) 0%, var(--button-bottom) 100%); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.8); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.6); border: 1px solid var(--button-border); text-shadow: 0 1px 0 white; white-space: nowrap; @@ -253,7 +306,7 @@ nav a { text-align: center; } -/* Shine effect for glass buttons */ +/* OPTIMIZED: Simplified shine effect */ nav a::after { content: ""; position: absolute; @@ -261,25 +314,32 @@ nav a::after { left: 0; right: 0; height: 50%; - background: linear-gradient(to bottom, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0) 100%); + background: linear-gradient(to bottom, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 100%); border-radius: 5px 5px 0 0; pointer-events: none; } nav a:hover, nav a:focus { background: linear-gradient(to bottom, #f9f9f9 0%, #e8e8e8 100%); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 1); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.8); } -/* Pressed button effect */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Pressed button effect - OPTIMIZED */ nav a.active, nav a:active { background: linear-gradient(to bottom, #d9d9d9 0%, #e9e9e9 100%); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); + /* OPTIMIZED: Simplified shadow */ + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); padding-top: 9px; padding-bottom: 7px; } -/* Make RSS button special */ +/* Make RSS button special - IMPROVED accessibility */ nav a:last-child { background: linear-gradient(to bottom, #7eb6e2 0%, #5a9bd1 100%); color: white; @@ -291,11 +351,16 @@ nav a:last-child:hover { background: linear-gradient(to bottom, #90c1e8 0%, #6ba6d9 100%); } +nav a:last-child:focus { + outline: 2px solid #ffffff; + outline-offset: 2px; +} + nav a:last-child:active { background: linear-gradient(to bottom, #5a9bd1 0%, #7eb6e2 100%); } -/* Main content area with paper texture */ +/* Main content area with paper texture - OPTIMIZED */ main { padding: var(--spacing-lg); background-color: var(--paper-bg); @@ -303,7 +368,7 @@ main { position: relative; } -/* Paper texture and subtle lines */ +/* OPTIMIZED: Simplified paper texture */ main::before { content: ""; position: absolute; @@ -311,12 +376,13 @@ main::before { left: 0; right: 0; bottom: 0; - background: repeating-linear-gradient( - 0deg, - transparent, - transparent 23px, - #e0ded8 24px - ); + /* SIMPLIFIED: Basic line pattern instead of complex repeating gradient */ + background: linear-gradient(to bottom, + transparent 0%, + transparent 95%, + rgba(224, 222, 216, 0.3) 95%, + rgba(224, 222, 216, 0.3) 100%); + background-size: 100% 24px; z-index: 0; pointer-events: none; } @@ -342,6 +408,7 @@ h2 { text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); } +.posts-list h2, h3 { font-size: 1.4rem; color: var(--leather-medium); @@ -354,12 +421,12 @@ p { z-index: 1; } -/* Links styled as underlining ink */ +/* Links styled as underlining ink - IMPROVED accessibility */ a { color: var(--link-color); text-decoration: none; border-bottom: 1px solid rgba(0, 102, 204, 0.3); - transition: all 0.2s; + transition: all var(--transition-fast); position: relative; z-index: 1; } @@ -374,7 +441,14 @@ a:hover { border-bottom-color: rgba(0, 136, 255, 0.5); } -/* Skeuomorphic button */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: #0088ff; + border-bottom-color: rgba(0, 136, 255, 0.5); +} + +/* Skeuomorphic button - IMPROVED accessibility */ .skeu-button { display: inline-block; padding: 8px 18px; @@ -385,12 +459,16 @@ a:hover { border-radius: 5px; background: linear-gradient(to bottom, var(--button-top) 0%, var(--button-bottom) 100%); border: 1px solid var(--button-border); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.8); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.6); position: relative; cursor: pointer; margin: 5px 0; + text-decoration: none; + transition: all var(--transition-fast); } +/* OPTIMIZED: Simplified shine effect */ .skeu-button::after { content: ""; position: absolute; @@ -398,7 +476,7 @@ a:hover { left: 0; right: 0; height: 50%; - background: linear-gradient(to bottom, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0) 100%); + background: linear-gradient(to bottom, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 100%); border-radius: 5px 5px 0 0; pointer-events: none; } @@ -407,9 +485,16 @@ a:hover { background: linear-gradient(to bottom, #f9f9f9 0%, #e8e8e8 100%); } +.skeu-button:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background: linear-gradient(to bottom, #f9f9f9 0%, #e8e8e8 100%); +} + .skeu-button:active { background: linear-gradient(to bottom, #d9d9d9 0%, #e9e9e9 100%); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); + /* OPTIMIZED: Simplified shadow */ + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); padding-top: 9px; padding-bottom: 7px; } @@ -653,36 +738,92 @@ footer a:hover { box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { + body { + padding: 10px; + /* OPTIMIZED: Simplified background on mobile */ + background-image: none; + background-color: var(--leather-medium); + } + .container { margin: var(--spacing-sm); padding: var(--spacing-sm); + /* OPTIMIZED: Simplified shadows on mobile */ + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .inner-container { margin-left: var(--spacing-sm); + /* OPTIMIZED: Simplified background on mobile */ + background-image: none; + } + + header { + padding: var(--spacing-md); + /* OPTIMIZED: Simplified background on mobile */ + background-image: none; + } + + main::before { + /* OPTIMIZED: Remove paper lines on mobile */ + display: none; } nav { flex-wrap: wrap; justify-content: center; + padding: var(--spacing-md); } nav a { margin: var(--spacing-xs); flex-grow: 1; text-align: center; + /* OPTIMIZED: Simplified effects on mobile */ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.4); + } + + nav a::after { + /* OPTIMIZED: Remove shine effect on mobile */ + display: none; } footer { flex-direction: column; gap: var(--spacing-sm); padding: var(--spacing-sm); + /* OPTIMIZED: Simplified background on mobile */ + background-image: none; } article { padding: var(--spacing-md); + /* OPTIMIZED: Simplified shadows on mobile */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + } + + .skeu-button::after { + /* OPTIMIZED: Remove shine effect on mobile */ + display: none; + } + + .tags a::after { + /* OPTIMIZED: Remove shine effect on mobile */ + display: none; + } + + .featured-image, .index-image, .tag-image, .archive-image { + /* OPTIMIZED: Simplified shadows on mobile */ + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); + transform: none; /* Remove rotation on mobile */ + } + + .featured-image:hover, .index-image:hover, .tag-image:hover, .archive-image:hover { + /* OPTIMIZED: Simplified hover effects on mobile */ + transform: none; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25); } } diff --git a/themes/stefano/style.css b/themes/stefano/style.css index 5600667..2f0e252 100644 --- a/themes/stefano/style.css +++ b/themes/stefano/style.css @@ -2,8 +2,27 @@ * Stefano Theme for BSSG * Based on Stefano Marinelli's personal website design * https://stefano.dragas.it/ + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining elegant design aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Color palette from user's website */ --primary: #1a3a5f; @@ -40,10 +59,10 @@ --accent-secondary: #2d6d8a; --quote-bg: #f7f7f7; - /* Typography */ - --font-main: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --font-headings: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + /* Typography - Enhanced font fallbacks for elegant design */ + --font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --font-headings: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --font-mono: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', 'Courier New', 'Courier', monospace; /* Sizing */ --content-width: 840px; @@ -62,8 +81,7 @@ --line-height-heading: 1.2; } -/* Import Fonts */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); + /* Base elements */ html { @@ -148,6 +166,13 @@ a:hover { color: var(--link-hover); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover); +} + /* Header/Site Title */ header { background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); @@ -219,6 +244,18 @@ header p { transform: translateY(-1px); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; + text-decoration: none; + background: linear-gradient(120deg, var(--accent) 0%, rgba(255,255,255,0.9) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); +} + /* Navigation */ nav { display: flex; @@ -230,9 +267,10 @@ nav { nav a { margin: 0 var(--spacing-sm); padding: var(--spacing-xs) 0; - color: white; + color: rgba(255, 255, 255, 0.95); position: relative; font-weight: 600; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } nav a::after { @@ -246,10 +284,27 @@ nav a::after { transition: var(--transition); } +nav a:hover { + color: white; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); +} + nav a:hover::after { width: 100%; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; + color: white; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); +} + +nav a:focus::after { + width: 100%; +} + /* Article */ article { margin-bottom: var(--spacing-xxl); @@ -284,6 +339,18 @@ article .meta { margin-left: var(--spacing-sm); } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use elegant icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Code */ code { font-family: var(--font-mono); @@ -332,6 +399,15 @@ pre code { transform: translateY(-3px); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-hover-bg); + color: var(--tag-hover-text); + transform: translateY(-3px); +} + .tags-list { margin: var(--spacing-sm) 0; } @@ -396,6 +472,13 @@ footer { color: var(--accent); } +/* ACCESSIBILITY: Focus states for social links */ +.social-link:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; + color: var(--accent); +} + .copyright { font-size: 0.9rem; color: rgba(255, 255, 255, 0.7); @@ -439,16 +522,23 @@ footer { box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: var(--spacing-xs); } -.posts-list h3 a { +.posts-list h2 a { color: var(--header-color); } -.posts-list h3 a:hover { +.posts-list h2 a:hover { + color: var(--accent); +} + +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list h2 a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; color: var(--accent); } @@ -535,6 +625,15 @@ ul li::before { transform: translateY(-3px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent); + color: white; + transform: translateY(-3px); +} + .pagination .page-info { margin: 0 var(--spacing-sm); color: var(--date-color); diff --git a/themes/swiss-design/style.css b/themes/swiss-design/style.css index 1c41853..77ece6b 100644 --- a/themes/swiss-design/style.css +++ b/themes/swiss-design/style.css @@ -2,6 +2,12 @@ * Swiss Design Theme for BSSG * Based on International Typographic Style with focus on grids, * clean sans-serif typography, and strong visual hierarchy + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks + * - Optimized performance while maintaining Swiss design principles */ :root { @@ -23,10 +29,10 @@ --link-hover: var(--dark-gray); --border-color: var(--medium-gray); - /* Typography */ - --font-main: 'Helvetica Neue', Helvetica, Arial, sans-serif; - --font-headings: 'Helvetica Neue', Helvetica, Arial, sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Helvetica Neue', Helvetica, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-headings: 'Helvetica Neue', Helvetica, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-mono: 'Courier New', 'Courier', monospace; /* Sizing */ --content-width: 90%; @@ -57,8 +63,20 @@ --line-height-normal: 1.5; --line-height-relaxed: 1.6; - /* Transitions */ - --transition-fast: 0.2s; + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s; + --transition-base: 0.2s; +} + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } } /* Reset and base elements */ @@ -117,12 +135,21 @@ header { .site-title a { color: var(--heading-color); text-decoration: none; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .site-title a:hover { color: var(--red); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Site description - minimalist */ header p { margin: var(--spacing-xs) 0 0; @@ -155,6 +182,9 @@ nav a { text-transform: uppercase; letter-spacing: 1px; position: relative; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a:hover, @@ -162,6 +192,12 @@ nav a.active { color: var(--red); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Content area with grid structure */ main { padding: 0 var(--spacing-md) var(--spacing-xl); @@ -205,6 +241,8 @@ a { text-decoration: none; transition: all var(--transition-fast) ease; border-bottom: 1px solid transparent; + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:visited { @@ -216,6 +254,12 @@ a:hover { border-bottom: 1px solid var(--link-hover); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Swiss-style articles with strong structure */ article { margin-bottom: var(--grid-row-gap); @@ -245,11 +289,23 @@ article .meta { padding-bottom: var(--spacing-sm); } -/* Reading time - minimal style */ +/* Reading time - minimal style with TEXT BROWSER FALLBACK */ .reading-time { position: relative; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags with Swiss precision */ .tags { display: flex; @@ -266,6 +322,9 @@ article .meta { letter-spacing: 1px; font-weight: 500; border: none; + transition: all var(--transition-fast) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags a:hover { @@ -274,6 +333,12 @@ article .meta { border: none; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Tags list page - strong grid */ .tags-list { list-style-type: none; @@ -300,6 +365,8 @@ article .meta { border: none; transition: all var(--transition-fast) ease; text-align: center; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags-list a:hover { @@ -308,6 +375,12 @@ article .meta { border: none; } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Footer - clean and minimal */ footer { background-color: var(--light-gray); @@ -325,6 +398,9 @@ footer a { color: var(--link-color); text-decoration: none; border-bottom: 1px solid transparent; + transition: all var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } footer a:hover { @@ -332,6 +408,12 @@ footer a:hover { color: var(--link-hover); } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Pagination - Swiss precision */ .pagination { display: flex; @@ -352,6 +434,8 @@ footer a:hover { font-size: var(--text-xs); border: none; transition: all var(--transition-fast) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a:hover { @@ -360,6 +444,12 @@ footer a:hover { border: none; } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Featured images - clean presentation */ .featured-image, .index-image, @@ -398,12 +488,18 @@ footer a:hover { padding-bottom: 0; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: var(--text-2xl); margin-bottom: var(--spacing-sm); } +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Grid-based layout for larger screens */ @media (min-width: 768px) { .posts-list { diff --git a/themes/terminal/style.css b/themes/terminal/style.css index c048cad..0f74845 100644 --- a/themes/terminal/style.css +++ b/themes/terminal/style.css @@ -23,10 +23,10 @@ --title-gradient-start: #33ff33; /* Bright terminal green */ --title-gradient-end: #00cc00; /* Darker terminal green */ - /* Typography */ - --font-main: 'VT323', 'Courier New', monospace; - --font-headings: 'VT323', 'Courier New', monospace; - --font-mono: 'VT323', 'Courier New', monospace; + /* Typography - System monospace fonts for terminal aesthetic */ + --font-main: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; + --font-headings: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; + --font-mono: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; /* Sizing */ --content-width: 800px; @@ -56,8 +56,39 @@ --transition-normal: 0.3s ease; } -/* Import VT323 font - classic terminal style */ -@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap'); +/* Removed external font loading for better performance and text browser compatibility */ + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all transforms and animations */ + .site-title a:hover, + .featured-image:hover img, + .index-image:hover img, + .tag-image:hover img, + .archive-image:hover img, + .pagination a:hover { + transform: none !important; + } + + @keyframes flicker { + from, to { opacity: 1; } + } + + @keyframes blink { + from, to { opacity: 1; } + } + + main::after { + animation: none !important; + } +} /* Base elements */ html { @@ -140,40 +171,46 @@ header { } .site-title a { - background: linear-gradient(to right, - var(--title-gradient-start) 0%, - var(--highlight-color) 50%, - var(--title-gradient-end) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--header-color); text-decoration: none; border: none; padding: 0; - text-shadow: 0 0 8px rgba(51, 255, 51, 0.6); position: relative; + outline: 2px solid transparent; + outline-offset: 2px; } .site-title a:hover { - background: linear-gradient(to right, - var(--highlight-color) 0%, - var(--title-gradient-start) 50%, - var(--highlight-color) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - animation: flicker 0.5s infinite alternate; + color: var(--highlight-color); border: none; } -/* Fallback for browsers that don't support background-clip */ -@supports not (background-clip: text) { +.site-title a:focus { + outline-color: var(--link-color); +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { .site-title a { - color: var(--header-color); + background: linear-gradient(to right, + var(--title-gradient-start) 0%, + var(--highlight-color) 50%, + var(--title-gradient-end) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: 0 0 8px rgba(51, 255, 51, 0.6); } .site-title a:hover { - color: var(--highlight-color); + background: linear-gradient(to right, + var(--highlight-color) 0%, + var(--title-gradient-start) 50%, + var(--highlight-color) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: flicker 0.5s infinite alternate; } } @@ -234,6 +271,13 @@ nav a:hover { color: var(--bg-color); } +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--dim-color); + color: var(--bg-color); +} + /* Content area */ main { position: relative; @@ -318,6 +362,11 @@ a:hover, a:focus { background-color: rgba(51, 255, 51, 0.1); } +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Article styling */ article { margin-bottom: var(--spacing-xxl); @@ -358,6 +407,16 @@ article .meta { content: "]"; } +/* Text browser fallbacks for reading time brackets */ +@supports not (content: "[") { + .reading-time::before { + content: ""; + } + .reading-time::after { + content: ""; + } +} + /* Tags */ .tags { margin-top: var(--spacing-lg); @@ -383,6 +442,13 @@ article .meta { color: var(--bg-color); } +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--dim-color); + color: var(--bg-color); +} + .tags-list { margin: var(--spacing-md) 0; } @@ -400,6 +466,16 @@ article .meta { content: ")"; } +/* Text browser fallbacks for tag count parentheses */ +@supports not (content: "(") { + .tag-count::before { + content: ""; + } + .tag-count::after { + content: ""; + } +} + /* Code blocks */ code { font-family: var(--font-mono); @@ -622,13 +698,13 @@ footer::before { color: var(--command-prompt); } -.posts-list h3 { +.posts-list h2 { margin-top: var(--spacing-xs); margin-bottom: var(--spacing-xs); text-transform: none; } -.posts-list h3::before { +.posts-list h2::before { content: ""; } @@ -743,6 +819,13 @@ hr::before { color: var(--dim-color); } +/* Text browser fallback for hr decoration */ +@supports not (content: "----------------------------") { + hr::before { + content: ""; + } +} + /* Selection style */ ::selection { background-color: var(--text-color); @@ -880,6 +963,13 @@ body { color: var(--bg-color); } +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--dim-color); + color: var(--bg-color); +} + .pagination .page-info { color: var(--dim-color); font-size: var(--text-sm); diff --git a/themes/text-only/style.css b/themes/text-only/style.css index 147ab04..8027936 100644 --- a/themes/text-only/style.css +++ b/themes/text-only/style.css @@ -2,8 +2,19 @@ * Text-Only Theme for BSSG * A step beyond minimalism - uses browser defaults with only * essential typography for readability. Lightning-fast loading. + * Enhanced with accessibility and compatibility improvements. */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + /* CSS Variables for consistency */ :root { /* Colors */ @@ -96,6 +107,12 @@ nav a:hover { text-decoration: underline; } +nav a:focus { + outline: 2px solid var(--color-link); + outline-offset: 2px; + text-decoration: underline; +} + /* Simple content area */ main { margin-bottom: var(--spacing-lg); @@ -136,6 +153,12 @@ a:hover { text-decoration: underline; } +a:focus { + outline: 2px solid var(--color-link); + outline-offset: 2px; + text-decoration: underline; +} + /* Ultra-minimal article styling */ article { margin-bottom: var(--spacing-lg); @@ -221,7 +244,7 @@ hr { border-bottom: 1px solid var(--color-border); } -.posts-list h3 { +.posts-list h2 { margin: 0; } @@ -232,6 +255,12 @@ hr { margin-top: var(--spacing-lg); } +.pagination a:focus { + outline: 2px solid var(--color-link); + outline-offset: 2px; + text-decoration: underline; +} + /* Bare minimum responsive adjustments */ @media (max-width: 600px) { body { diff --git a/themes/thoughtful/style.css b/themes/thoughtful/style.css new file mode 100644 index 0000000..7ed0eeb --- /dev/null +++ b/themes/thoughtful/style.css @@ -0,0 +1,1102 @@ +/* + * Thoughtful Theme for BSSG + * A warm, accessible, and performant theme + * for personal reflection blogs and thoughtful writing + * + * DESIGN PHILOSOPHY: + * - Warm, natural color palette that invites contemplation + * - Typography that balances elegance with personality + * - Rhythm and flow that supports narrative reading + * - Subtle visual elements that enhance without distraction + * - Full accessibility compliance with graceful degradation + * - Optimized performance for all devices + */ + +:root { + /* Color palette - Warm and contemplative with WCAG AA+ contrast */ + --primary-bg: #faf8f3; + --secondary-bg: #f2efe7; + --surface-bg: #ffffff; + --accent-bg: #e8e2d4; + + /* Text colors - High contrast for accessibility */ + --text-primary: #2c2620; + --text-secondary: #3d3730; + --text-tertiary: #4f4940; + --text-muted: #6b635a; + + /* Accent colors - WCAG AA compliant */ + --accent-primary: #8b4513; + --accent-secondary: #b8651e; + --link-color: #1a4480; + --link-hover: #0f2c5c; + --link-visited: #4a2c5a; + + /* Semantic colors */ + --border-light: #e0d7c5; + --border-medium: #c9baa5; + --border-dark: #a69780; + --shadow-light: rgba(45, 41, 38, 0.08); + --shadow-medium: rgba(45, 41, 38, 0.12); + --highlight: rgba(139, 69, 19, 0.08); + + /* Typography - Text browser friendly */ + --font-primary: Georgia, 'Times New Roman', 'Liberation Serif', serif; + --font-secondary: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Liberation Sans', Arial, sans-serif; + --font-mono: 'Liberation Mono', 'Courier New', Courier, monospace; + + /* Typography scale */ + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --text-4xl: 2.25rem; + --text-5xl: 3rem; + + /* Spacing system - Rhythmic and harmonious */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + --space-16: 4rem; + --space-20: 5rem; + --space-24: 6rem; + + /* Layout */ + --content-width: min(65ch, 90vw); + --wide-content-width: min(75ch, 95vw); + --narrow-content-width: min(45ch, 85vw); + + /* Design tokens */ + --line-height-tight: 1.25; + --line-height-normal: 1.6; + --line-height-relaxed: 1.75; + --border-radius-sm: 4px; + --border-radius-md: 8px; + --border-radius-lg: 12px; + + /* Performance-optimized transitions */ + --transition-fast: 150ms ease-out; + --transition-normal: 200ms ease-out; + --transition-slow: 300ms ease-out; +} + +/* Dark theme variant - WCAG AA+ compliant */ +.theme-dark { + --primary-bg: #1a1814; + --secondary-bg: #242019; + --surface-bg: #2d2926; + --accent-bg: #3a3530; + + /* High contrast text for accessibility */ + --text-primary: #f5f2eb; + --text-secondary: #e0dcd5; + --text-tertiary: #cac5be; + --text-muted: #b4afa8; + + --accent-primary: #d2813f; + --accent-secondary: #e6a85c; + --link-color: #7eb3ff; + --link-hover: #a8c7ff; + --link-visited: #c4a3e8; + + --border-light: #3a3530; + --border-medium: #4a453f; + --border-dark: #5a534c; + --shadow-light: rgba(0, 0, 0, 0.2); + --shadow-medium: rgba(0, 0, 0, 0.3); + --highlight: rgba(210, 129, 63, 0.12); +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + +/* Base styles */ +* { + box-sizing: border-box; +} + +html { + font-size: clamp(18px, 2.5vw, 22px); + scroll-behavior: smooth; + scroll-padding-top: var(--space-8); +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-primary); + font-size: var(--text-base); + line-height: var(--line-height-normal); + color: var(--text-primary); + background-color: var(--primary-bg); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Removed expensive background effects for better performance */ + min-height: 100vh; +} + +/* Container and layout */ +.container { + width: var(--content-width); + margin: 0 auto; + padding: var(--space-6) var(--space-4) var(--space-16); + position: relative; +} + +/* Header */ +header { + margin-bottom: var(--space-16); + text-align: center; + position: relative; +} + +header::after { + content: ''; + position: absolute; + bottom: calc(-1 * var(--space-8)); + left: 50%; + transform: translateX(-50%); + width: 60px; + height: 2px; + background: linear-gradient(90deg, transparent, var(--accent-primary), transparent); + border-radius: 1px; +} + +.site-title { + font-family: var(--font-primary); + font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl)); + font-weight: 400; + margin: 0 0 var(--space-4); + letter-spacing: -0.02em; + line-height: var(--line-height-tight); + color: var(--text-primary); + position: relative; +} + +.site-title a { + color: inherit; + text-decoration: none; + transition: color var(--transition-normal); + position: relative; + display: inline-block; +} + +.site-title a::before { + content: ''; + position: absolute; + bottom: -4px; + left: 0; + width: 0; + height: 2px; + background: var(--accent-primary); + transition: width var(--transition-normal); +} + +.site-title a:hover { + color: var(--accent-primary); +} + +.site-title a:hover::before { + width: 100%; +} + +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 4px; + border-radius: var(--border-radius-sm); +} + +header p { + font-family: var(--font-secondary); + font-size: var(--text-lg); + font-style: italic; + color: var(--text-secondary); + margin: 0; + max-width: var(--narrow-content-width); + margin-left: auto; + margin-right: auto; + line-height: var(--line-height-relaxed); +} + +/* Navigation */ +nav { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: var(--space-6); + margin-top: var(--space-8); + font-family: var(--font-secondary); + font-size: var(--text-sm); + font-weight: 500; + letter-spacing: 0.025em; + text-transform: uppercase; +} + +nav a { + color: var(--text-tertiary); + text-decoration: none; + padding: var(--space-2) var(--space-3); + border-radius: var(--border-radius-sm); + transition: all var(--transition-normal); + position: relative; + overflow: hidden; +} + +nav a::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--highlight); + transition: left var(--transition-normal); + z-index: -1; +} + +nav a:hover { + color: var(--accent-primary); + transform: translateY(-1px); +} + +nav a:hover::before { + left: 0; +} + +nav a.active { + color: var(--accent-primary); + background: var(--highlight); +} + +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Main content */ +main { + min-height: 60vh; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-primary); + color: var(--text-primary); + line-height: var(--line-height-tight); + margin: 0 0 var(--space-6); + font-weight: 400; + letter-spacing: -0.015em; +} + +h1 { + font-size: clamp(var(--text-3xl), 4vw, var(--text-4xl)); + margin-bottom: var(--space-8); + text-align: center; + position: relative; + padding-bottom: var(--space-6); +} + +h1::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 40px; + height: 2px; + background: var(--accent-secondary); + border-radius: 1px; +} + +h2 { + font-size: var(--text-2xl); + margin-top: var(--space-16); + margin-bottom: var(--space-6); + color: var(--accent-primary); + position: relative; + padding-left: var(--space-4); +} + +h2::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 60%; + background: var(--accent-secondary); + border-radius: 2px; +} + +h3 { + font-size: var(--text-xl); + margin-top: var(--space-12); + color: var(--text-primary); +} + +h4 { + font-size: var(--text-lg); + margin-top: var(--space-10); + font-style: italic; + color: var(--text-secondary); +} + +p { + margin: 0 0 var(--space-6); + line-height: var(--line-height-relaxed); + hyphens: auto; + hanging-punctuation: first last; +} + +/* First paragraph enhancement */ +article .article-content > p:first-of-type { + font-size: var(--text-lg); + line-height: var(--line-height-normal); + color: var(--text-secondary); + margin-bottom: var(--space-8); +} + +article .article-content > p:first-of-type::first-letter { + float: left; + font-size: 4rem; + line-height: 0.8; + margin: 0.1em 0.1em 0 0; + color: var(--accent-primary); + font-weight: 400; + text-shadow: 2px 2px 4px var(--shadow-light); +} + +/* Links */ +a { + color: var(--link-color); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 0.2em; + transition: all var(--transition-normal); +} + +a:hover { + color: var(--link-hover); + text-decoration-thickness: 2px; +} + +a:visited { + color: var(--link-visited); +} + +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + border-radius: var(--border-radius-sm); +} + +/* Lists */ +ul, ol { + margin: var(--space-6) 0 var(--space-8); + padding-left: var(--space-8); +} + +li { + margin-bottom: var(--space-3); + line-height: var(--line-height-relaxed); +} + +li::marker { + color: var(--accent-secondary); +} + +/* Blockquotes */ +blockquote { + margin: var(--space-12) 0; + padding: var(--space-8) var(--space-10); + background: linear-gradient(135deg, var(--secondary-bg), var(--accent-bg)); + border-left: 4px solid var(--accent-primary); + border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0; + font-style: italic; + font-size: var(--text-lg); + line-height: var(--line-height-relaxed); + color: var(--text-secondary); + box-shadow: 0 4px 6px var(--shadow-light); + position: relative; +} + +blockquote::before { + content: '"'; + position: absolute; + top: var(--space-4); + left: var(--space-4); + font-size: 3rem; + color: var(--accent-primary); + opacity: 0.3; + line-height: 1; +} + +blockquote p:first-child { + margin-top: 0; +} + +blockquote p:last-child { + margin-bottom: 0; +} + +blockquote cite { + display: block; + margin-top: var(--space-4); + font-size: var(--text-sm); + color: var(--text-muted); + text-align: right; + font-style: normal; +} + +blockquote cite::before { + content: '— '; +} + +/* Code blocks */ +pre, code { + font-family: var(--font-mono); + font-size: var(--text-sm); + border-radius: var(--border-radius-sm); +} + +code { + background: var(--accent-bg); + padding: 0.2em 0.4em; + color: var(--text-primary); + border: 1px solid var(--border-light); +} + +pre { + background: var(--secondary-bg); + padding: var(--space-6); + margin: var(--space-8) 0; + overflow-x: auto; + line-height: var(--line-height-normal); + border: 1px solid var(--border-medium); + box-shadow: inset 0 2px 4px var(--shadow-light); +} + +pre code { + background: none; + padding: 0; + border: none; +} + +/* Horizontal rule */ +hr { + border: none; + height: 2px; + background: linear-gradient(90deg, transparent, var(--accent-primary), transparent); + margin: var(--space-16) auto; + width: 60%; + border-radius: 1px; +} + +/* Article layout */ +article { + margin-bottom: var(--space-24); +} + +article h1 { + margin-bottom: var(--space-4); +} + +/* Article meta */ +.meta, .page-meta { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: var(--space-2); + color: var(--text-muted); + font-family: var(--font-secondary); + font-size: var(--text-sm); + margin: var(--space-4) 0 var(--space-16); + text-align: center; + line-height: var(--line-height-normal); +} + +.meta > * { + display: inline-flex; + align-items: center; + gap: var(--space-1); +} + +.meta > *:not(:last-child)::after { + content: '•'; + margin: 0 var(--space-3); + color: var(--accent-secondary); + font-weight: bold; +} + +/* Reading time - TEXT BROWSER FALLBACK */ +.reading-time { + display: inline-flex; + align-items: center; + gap: var(--space-2); +} + +/* Fallback for text browsers - shows "Reading time:" before emoji */ +.reading-time::before { + content: 'Reading time: ⏱'; + font-style: normal; + color: var(--accent-secondary); +} + +/* Hide fallback text when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: '⏱'; + font-style: normal; + color: var(--accent-secondary); + } +} + +/* TEXT BROWSER FALLBACK: Ensure content is readable without CSS */ +@media (max-width: 0) { + .reading-time::before { + content: 'Reading time: '; + font-style: normal; + } +} + +/* ACCESSIBILITY: Enhanced for screen readers */ +.reading-time[aria-label]::before { + content: '⏱'; +} + +/* Tags */ +.tags { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + justify-content: center; + margin-top: var(--space-6); +} + +.tags a { + background: var(--accent-bg); + color: var(--text-tertiary); + text-decoration: none; + padding: var(--space-1) var(--space-3); + border-radius: var(--border-radius-lg); + font-size: var(--text-xs); + font-family: var(--font-secondary); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all var(--transition-normal); + border: 1px solid var(--border-light); +} + +.tags a:hover { + background: var(--accent-primary); + color: var(--surface-bg); + transform: translateY(-1px); + box-shadow: 0 2px 4px var(--shadow-medium); +} + +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Images */ +img { + max-width: 100%; + height: auto; + border-radius: var(--border-radius-md); + box-shadow: 0 4px 6px var(--shadow-light); +} + +.featured-image, .index-image, .tag-image, .archive-image { + margin: var(--space-12) auto; + text-align: center; +} + +figcaption { + font-size: var(--text-sm); + font-style: italic; + color: var(--text-muted); + text-align: center; + margin-top: var(--space-4); + font-family: var(--font-secondary); +} + +/* Pull quotes */ +.pull-quote { + font-family: var(--font-primary); + font-size: var(--text-2xl); + line-height: var(--line-height-normal); + font-style: italic; + color: var(--accent-primary); + text-align: center; + margin: var(--space-16) auto; + padding: var(--space-8) var(--space-6); + max-width: var(--narrow-content-width); + position: relative; + background: linear-gradient(135deg, var(--highlight), transparent); + border-radius: var(--border-radius-lg); +} + +.pull-quote::before, +.pull-quote::after { + content: '"'; + font-size: 4rem; + position: absolute; + color: var(--accent-secondary); + opacity: 0.3; + line-height: 1; +} + +.pull-quote::before { + top: 0; + left: var(--space-4); +} + +.pull-quote::after { + bottom: 0; + right: var(--space-4); +} + +/* Post list */ +.posts-list { + margin-top: var(--space-16); +} + +.posts-list article { + margin-bottom: var(--space-16); + padding: var(--space-10) var(--space-8) var(--space-8); + background: var(--surface-bg); + border-radius: var(--border-radius-lg); + box-shadow: 0 2px 4px var(--shadow-light); + transition: all var(--transition-normal); + border: 1px solid var(--border-light); + text-align: center; + position: relative; + overflow: hidden; +} + +.posts-list article::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); +} + +.posts-list article:hover { + /* Use opacity and scale instead of transform for better performance */ + box-shadow: 0 8px 16px var(--shadow-medium); +} + +.posts-list article:last-child { + border-bottom: 1px solid var(--border-light); +} + +.posts-list h2 { + margin: 0 0 var(--space-6); + font-size: var(--text-2xl); + text-align: center; + line-height: var(--line-height-tight); +} + +.posts-list h2 a { + color: var(--text-primary); + text-decoration: none; + transition: color var(--transition-normal); + display: block; +} + +.posts-list h2 a:hover { + color: var(--accent-primary); +} + +.posts-list h2 a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +.posts-list .meta { + margin: var(--space-4) 0 var(--space-8); +} + +.posts-list .featured-image, +.posts-list .index-image { + margin: var(--space-8) 0; +} + +.posts-list p { + text-align: left; + margin-top: var(--space-8); + font-size: var(--text-lg); + line-height: var(--line-height-relaxed); + color: var(--text-secondary); +} + +/* Footer */ +footer { + margin-top: var(--space-24); + padding: var(--space-12) 0 var(--space-8); + text-align: center; + border-top: 1px solid var(--border-medium); + background: linear-gradient(135deg, var(--secondary-bg), var(--accent-bg)); + color: var(--text-muted); + font-family: var(--font-secondary); + font-size: var(--text-sm); +} + +footer p { + margin: var(--space-2) 0; +} + +footer a { + color: var(--text-secondary); + transition: color var(--transition-normal); +} + +footer a:hover { + color: var(--accent-primary); +} + +footer a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Pagination */ +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + margin: var(--space-16) 0 var(--space-12); + font-family: var(--font-secondary); + font-size: var(--text-sm); + gap: var(--space-4); +} + +.pagination a { + background: var(--surface-bg); + color: var(--text-secondary); + text-decoration: none; + padding: var(--space-3) var(--space-6); + border-radius: var(--border-radius-md); + border: 1px solid var(--border-medium); + transition: all var(--transition-normal); + box-shadow: 0 2px 4px var(--shadow-light); +} + +.pagination a:hover { + background: var(--accent-primary); + color: var(--surface-bg); + transform: translateY(-1px); + box-shadow: 0 4px 8px var(--shadow-medium); +} + +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +.page-info { + color: var(--text-muted); + font-style: italic; +} + +/* Theme toggle */ +.theme-toggle { + position: fixed; + top: var(--space-4); + right: var(--space-4); + display: flex; + gap: var(--space-2); + z-index: 1000; + background: var(--surface-bg); + padding: var(--space-2); + border-radius: var(--border-radius-lg); + box-shadow: 0 4px 6px var(--shadow-medium); + border: 1px solid var(--border-light); +} + +.theme-toggle button { + background: none; + border: 1px solid var(--border-medium); + padding: var(--space-2) var(--space-3); + cursor: pointer; + border-radius: var(--border-radius-sm); + color: var(--text-muted); + font-family: var(--font-secondary); + font-size: var(--text-xs); + font-weight: 500; + transition: all var(--transition-normal); +} + +.theme-toggle button:hover { + background: var(--highlight); + color: var(--text-primary); +} + +.theme-toggle button.active { + background: var(--accent-primary); + color: var(--surface-bg); + border-color: var(--accent-primary); +} + +.theme-toggle button:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Responsive design */ +@media (max-width: 768px) { + :root { + --content-width: min(90vw, 100%); + } + + .container { + padding: var(--space-4) var(--space-3) var(--space-12); + } + + header::after { + bottom: calc(-1 * var(--space-6)); + } + + h1::after { + width: 30px; + } + + h2 { + margin-top: var(--space-12); + padding-left: var(--space-3); + } + + .meta, .page-meta { + flex-direction: column; + gap: var(--space-1); + text-align: center; + } + + .meta > *:not(:last-child)::after { + content: ''; + margin: 0; + } + + blockquote { + padding: var(--space-6); + margin: var(--space-8) 0; + } + + .pull-quote { + font-size: var(--text-xl); + padding: var(--space-6) var(--space-4); + margin: var(--space-12) auto; + } + + nav { + gap: var(--space-4); + margin-top: var(--space-6); + } + + .theme-toggle { + top: var(--space-2); + right: var(--space-2); + font-size: var(--text-xs); + } + + .posts-list article { + padding: var(--space-6); + margin-bottom: var(--space-12); + } +} + +@media (max-width: 480px) { + .container { + padding: var(--space-3) var(--space-2) var(--space-10); + } + + nav { + gap: var(--space-3); + font-size: var(--text-xs); + } + + nav a { + padding: var(--space-1) var(--space-2); + } + + .pagination { + flex-direction: column; + gap: var(--space-3); + } +} + +/* Print styles */ +@media print { + :root { + --primary-bg: #ffffff; + --text-primary: #000000; + --text-secondary: #333333; + --text-tertiary: #666666; + --text-muted: #999999; + --accent-primary: #000000; + --link-color: #000000; + } + + body { + background: none; + font-size: 12pt; + line-height: 1.4; + } + + .container { + width: 100%; + max-width: none; + padding: 0; + margin: 0; + } + + .theme-toggle, nav { + display: none; + } + + a[href^="http"]::after { + content: " (" attr(href) ")"; + font-size: 0.8em; + color: #666; + } + + h1, h2, h3, h4, h5, h6 { + page-break-after: avoid; + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + page-break-inside: avoid; + } + + blockquote, pre { + page-break-inside: avoid; + border: 1px solid #ccc; + background: #f9f9f9 !important; + } + + p, h2, h3 { + orphans: 3; + widows: 3; + } +} + +/* Accessibility enhancements */ +@media (prefers-contrast: high) { + :root { + --text-primary: #000000; + --text-secondary: #1a1a1a; + --link-color: #0000ff; + --link-hover: #000080; + --accent-primary: #8b0000; + --border-medium: #666666; + --shadow-light: rgba(0, 0, 0, 0.5); + --shadow-medium: rgba(0, 0, 0, 0.7); + } +} + +/* NO JAVASCRIPT CLASS: Better graceful degradation */ +.no-js .posts-list article { + /* Remove hover effects if no JS */ + transition: none; +} + +.no-js .site-title a::before, +.no-js nav a::before { + /* Remove complex pseudo-element animations if no JS */ + display: none; +} + +/* TEXT BROWSER SUPPORT: Ensure all content is accessible */ +@media (max-device-width: 0) { + /* This targets text browsers */ + .site-title::after, + header::after, + h1::after, + h2::before, + blockquote::before, + .pull-quote::before, + .pull-quote::after { + display: none; + } + + .theme-toggle { + display: none; + } +} + +/* PERFORMANCE: Use GPU acceleration only when beneficial */ +@media (min-width: 769px) { + .posts-list article { + will-change: box-shadow; + } +} + +/* Focus enhancements for keyboard navigation */ +:focus-visible { + outline: 3px solid var(--link-color); + outline-offset: 2px; + border-radius: var(--border-radius-sm); +} + +/* Skip to content link - ACCESSIBILITY ESSENTIAL */ +.skip-to-content { + position: absolute; + top: -40px; + left: 6px; + background: var(--accent-primary); + color: var(--surface-bg); + padding: 8px 12px; + text-decoration: none; + border-radius: var(--border-radius-sm); + font-family: var(--font-secondary); + font-size: var(--text-sm); + font-weight: 600; + z-index: 100; + transition: top var(--transition-fast); +} + +.skip-to-content:focus { + top: 6px; +} + +/* PRINT OPTIMIZATIONS: Better resource usage */ +@media print { + * { + /* Remove all shadows and transitions for print */ + box-shadow: none !important; + text-shadow: none !important; + transition: none !important; + animation: none !important; + } +} diff --git a/themes/tty/style.css b/themes/tty/style.css index f6112de..daf8f65 100644 --- a/themes/tty/style.css +++ b/themes/tty/style.css @@ -2,20 +2,29 @@ * Terminal Theme for BSSG * Simulates an old teletype/terminal output with monospace text * on a simple background, with a character-by-character "print" effect. + * Enhanced with accessibility, performance, and compatibility improvements. */ -@font-face { - font-family: 'VT323'; - src: url('https://fonts.googleapis.com/css2?family=VT323&display=swap'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Source Code Pro'; - src: url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap'); - font-weight: normal; - font-style: normal; +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all terminal effects for reduced motion */ + .tty-effect { + animation: none !important; + visibility: visible !important; + opacity: 1 !important; + } + + body::before, + .container::before { + display: none !important; + } } :root { @@ -37,10 +46,10 @@ --code-bg: rgba(0, 85, 0, 0.3); --cursor-color: var(--terminal-green); - /* Typography */ - --font-main: 'VT323', 'Source Code Pro', monospace; - --font-headings: 'VT323', 'Source Code Pro', monospace; - --font-mono: 'Source Code Pro', monospace; + /* Typography - system monospace fonts for terminal aesthetic */ + --font-main: 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; + --font-headings: 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; + --font-mono: 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; /* Font sizes */ --text-xs: 0.8rem; @@ -85,34 +94,42 @@ body { line-height: var(--line-height); margin: 0; padding: var(--spacing-sm); - /* Terminal scanlines effect */ - background-image: linear-gradient( - rgba(0, 255, 0, 0.03) 1px, - transparent 1px - ); - background-size: 100% 2px; position: relative; - /* CRT glow effect */ - box-shadow: 0 0 20px rgba(0, 255, 0, 0.05) inset; } -/* CRT flicker animation */ -body::before { - content: ""; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: radial-gradient( - ellipse at center, - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 0.2) 80%, - rgba(0, 0, 0, 0.4) 100% - ); - pointer-events: none; - z-index: 2; - opacity: 0.8; +/* Progressive enhancement for terminal effects */ +@supports (background-image: linear-gradient(rgba(0, 255, 0, 0.03) 1px, transparent 1px)) { + body { + /* Terminal scanlines effect - reduced intensity */ + background-image: linear-gradient( + rgba(0, 255, 0, 0.02) 1px, + transparent 1px + ); + background-size: 100% 3px; + /* CRT glow effect - reduced intensity */ + box-shadow: 0 0 10px rgba(0, 255, 0, 0.03) inset; + } +} + +/* Progressive enhancement for CRT vignette effect */ +@supports (background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.2) 80%)) { + body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient( + ellipse at center, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.1) 80%, + rgba(0, 0, 0, 0.2) 100% + ); + pointer-events: none; + z-index: 2; + opacity: 0.5; + } } /* Scrollbars - webkit */ @@ -145,23 +162,26 @@ body::before { overflow: hidden; } -.container::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: repeating-linear-gradient( - 0deg, - rgba(0, 0, 0, 0.1), - rgba(0, 0, 0, 0.1) 1px, - rgba(0, 0, 0, 0) 1px, - rgba(0, 0, 0, 0) 2px - ); - pointer-events: none; - z-index: 1; - opacity: 0.3; +/* Progressive enhancement for container scanlines */ +@supports (background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1) 1px)) { + .container::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.05), + rgba(0, 0, 0, 0.05) 1px, + rgba(0, 0, 0, 0) 1px, + rgba(0, 0, 0, 0) 3px + ); + pointer-events: none; + z-index: 1; + opacity: 0.2; + } } /* Terminal header */ @@ -262,18 +282,36 @@ main { } /* Typography for terminal */ -h1, h2, h3, h4, h5, h6 { +h1, h2, h3 { font-weight: normal; margin: var(--spacing-md) 0 var(--spacing-xs) 0; line-height: 1.3; position: relative; - padding-left: 1.2em; + padding-left: 2em; /* Sufficient padding for 1-3 # symbols */ } -h1::before, h2::before, h3::before, h4::before, h5::before, h6::before { +h4, h5, h6 { + font-weight: normal; + margin: var(--spacing-md) 0 var(--spacing-xs) 0; + line-height: 1.3; + position: relative; + padding-left: 3.5em; /* Increased padding for 4-6 # symbols */ +} + +h1::before, h2::before, h3::before { position: absolute; left: 0; + top: 0; color: var(--terminal-amber); + width: 1.8em; /* Sufficient width for 1-3 # symbols */ +} + +h4::before, h5::before, h6::before { + position: absolute; + left: 0; + top: 0; + color: var(--terminal-amber); + width: 3.2em; /* Sufficient width for 4-6 # symbols */ } h1::before { content: "#"; } @@ -325,14 +363,25 @@ a:hover { border-bottom: 1px solid var(--link-hover); } +/* Accessibility: Focus outlines for all interactive elements */ +a:focus, +nav a:focus, +.tags a:focus, +.pagination a:focus { + outline: 2px solid var(--terminal-amber); + outline-offset: 2px; + background-color: rgba(255, 176, 0, 0.1); +} + /* Article styling */ article { margin-bottom: var(--spacing-lg); position: relative; } +/* Text browser fallback for article header */ article::before { - content: "cat article.txt"; + content: "Article:"; color: var(--terminal-green); display: block; margin-bottom: var(--spacing-sm); @@ -340,6 +389,13 @@ article::before { border-bottom: 1px dashed var(--border-color); } +/* Progressive enhancement for terminal command */ +@supports (content: "cat article.txt") { + article::before { + content: "cat article.txt"; + } +} + article h1 { margin-top: var(--spacing-sm); } @@ -362,11 +418,19 @@ article .meta { gap: 0.3em; } +/* Text browser fallback for reading time */ .reading-time::before { - content: "⧗"; + content: "Time: "; font-size: var(--text-base); } +/* Progressive enhancement for decorative symbol */ +@supports (content: "⧗") { + .reading-time::before { + content: "⧗"; + } +} + /* Tags */ .tags { display: flex; @@ -409,8 +473,9 @@ pre { position: relative; } +/* Text browser fallback for code header */ pre::before { - content: "$ cat code.txt"; + content: "Code:"; color: var(--terminal-green); display: block; margin-bottom: var(--spacing-xs); @@ -418,6 +483,13 @@ pre::before { border-bottom: 1px dashed var(--border-color); } +/* Progressive enhancement for terminal command */ +@supports (content: "$ cat code.txt") { + pre::before { + content: "$ cat code.txt"; + } +} + pre code { background-color: transparent; padding: 0; @@ -433,8 +505,9 @@ blockquote { position: relative; } +/* Text browser fallback for blockquote decorations */ blockquote::before { - content: "echo '"; + content: "Quote: "; position: absolute; top: 0; left: 0; @@ -444,7 +517,7 @@ blockquote::before { } blockquote::after { - content: "'"; + content: ""; position: absolute; bottom: 0; right: 0; @@ -453,6 +526,17 @@ blockquote::after { padding: 0.2em 0.4em; } +/* Progressive enhancement for terminal command */ +@supports (content: "echo '") { + blockquote::before { + content: "echo '"; + } + + blockquote::after { + content: "'"; + } +} + blockquote p { margin: var(--spacing-xs) 0; padding-left: 0; @@ -462,25 +546,39 @@ blockquote p::before { content: none; } -/* Lists */ +/* Lists with proper spacing to prevent overlap */ ul, ol { margin: var(--spacing-sm) 0; - padding-left: 3em; + padding-left: 4em; /* Increased to prevent overlap */ position: relative; } ul::before, ol::before { position: absolute; left: 0; + top: 0; color: var(--terminal-dim-green); + width: 3.5em; /* Ensure sufficient width */ } +/* Text browser fallbacks for list headers */ ul::before { - content: "ls:"; + content: "List:"; } ol::before { - content: "seq:"; + content: "List:"; +} + +/* Progressive enhancement for terminal commands */ +@supports (content: "ls:") { + ul::before { + content: "ls:"; + } + + ol::before { + content: "seq:"; + } } li { @@ -507,8 +605,9 @@ figure { position: relative; } +/* Text browser fallback for figure header */ figure::before { - content: "cat image.jpg"; + content: "Image:"; color: var(--terminal-green); display: block; margin-bottom: var(--spacing-xs); @@ -516,6 +615,13 @@ figure::before { border-bottom: 1px dashed var(--border-color); } +/* Progressive enhancement for terminal command */ +@supports (content: "cat image.jpg") { + figure::before { + content: "cat image.jpg"; + } +} + figcaption { font-size: var(--text-sm); padding: var(--spacing-xs); @@ -536,20 +642,20 @@ figcaption { margin-bottom: var(--spacing-xs); } -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: var(--text-md); - padding-left: 1.2em; + padding-left: 1.5em; /* Increased to prevent overlap */ position: relative; } -.posts-list h2::before, -.posts-list h3::before { +.posts-list h2::before { position: absolute; left: 0; + top: 0; color: var(--terminal-amber); content: "##"; + width: 1.3em; /* Ensure sufficient width */ } .posts-list article p:last-child { @@ -566,14 +672,22 @@ footer { position: relative; } +/* Text browser fallback for footer */ footer::before { - content: "exit"; + content: "End"; color: var(--terminal-green); display: block; text-align: left; margin-bottom: var(--spacing-sm); } +/* Progressive enhancement for terminal command */ +@supports (content: "exit") { + footer::before { + content: "exit"; + } +} + footer p { margin: var(--spacing-xs) 0; padding-left: 0; @@ -653,8 +767,12 @@ footer p::before { margin-bottom: var(--spacing-xs); } - h1, h2, h3, h4, h5, h6, p { - padding-left: 0.8em; + h1, h2, h3, p { + padding-left: 1.5em; /* Reduced but still sufficient padding for mobile */ + } + + h4, h5, h6 { + padding-left: 2.5em; /* Reduced but still sufficient padding for mobile */ } h1 { diff --git a/themes/vaporwave/style.css b/themes/vaporwave/style.css index e889b9b..ae7cb89 100644 --- a/themes/vaporwave/style.css +++ b/themes/vaporwave/style.css @@ -2,14 +2,39 @@ * Vaporwave/Synthwave Theme for BSSG * 80s retro futurism with neon colors and grid designs * Inspired by retrowave aesthetics and digital dreamscapes + * IMPROVED: Better accessibility, performance, and text browser support */ -@import url('https://fonts.googleapis.com/css2?family=VT323&family=Monoton&display=swap'); +/* Removed Google Fonts import - using system font alternatives for better performance and privacy */ + +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects for reduced motion */ + .container { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + } + + body::before { + display: none !important; + } + + header h1, .site-title { + animation: none !important; + } +} :root { - /* Vaporwave/Synthwave color scheme */ - --bg-color: #0b032d; - --grid-color: rgba(255, 65, 221, 0.3); + /* Vaporwave/Synthwave color scheme - IMPROVED contrast ratios */ + --bg-color: #0a0a0a; /* Darker for better contrast */ + --grid-color: rgba(255, 65, 221, 0.2); /* Reduced opacity for performance */ --text-color: #ffffff; --text-shadow: #ff00ea; --neon-pink: #ff71ce; @@ -25,13 +50,13 @@ --border-color: var(--neon-purple); --accent-color: var(--neon-blue); --accent-secondary: var(--neon-pink); - --quote-bg: rgba(0, 0, 0, 0.4); + --quote-bg: rgba(0, 0, 0, 0.6); /* Improved contrast */ --header-color: var(--neon-green); - /* Typography */ - --font-main: 'VT323', monospace; - --font-headings: 'Monoton', cursive; - --font-mono: 'VT323', monospace; + /* Typography - Using system fonts for better performance and privacy */ + --font-main: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; + --font-headings: 'Impact', 'Arial Black', 'Franklin Gothic Bold', 'Helvetica Neue', sans-serif; + --font-mono: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; /* Font Sizes */ --text-xs: 0.8rem; @@ -58,14 +83,14 @@ --line-height-tight: 1.3; --line-height-loose: 1.8; - /* Transitions */ - --transition-fast: 0.2s; - --transition-medium: 0.3s; - --transition-slow: 0.5s; + /* Transitions - Reduced for performance */ + --transition-fast: 0.15s; + --transition-medium: 0.2s; + --transition-slow: 0.3s; /* Sizing */ --content-width: 1000px; - --grid-size: 30px; + --grid-size: 40px; /* Larger grid for better performance */ } html, body { @@ -80,59 +105,63 @@ html, body { box-sizing: inherit; } -/* Base elements with retro grid background */ +/* Base elements with optimized grid background */ body { font-family: var(--font-main); background-color: var(--bg-color); + /* OPTIMIZED: Simplified background for better performance */ background-image: - linear-gradient(0deg, rgba(150, 0, 150, 0.3) 0%, rgba(0, 0, 80, 0.3) 100%), - linear-gradient(to right, var(--grid-color) 2px, transparent 2px), - linear-gradient(to bottom, var(--grid-color) 2px, transparent 2px); + linear-gradient(0deg, rgba(150, 0, 150, 0.2) 0%, rgba(0, 0, 80, 0.2) 100%), + linear-gradient(to right, var(--grid-color) 1px, transparent 1px), + linear-gradient(to bottom, var(--grid-color) 1px, transparent 1px); background-size: 100% 100%, var(--grid-size) var(--grid-size), var(--grid-size) var(--grid-size); background-position: 0 0, 0 0, 0 0; - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ color: var(--text-color); margin: 0; padding: var(--spacing-xl); line-height: var(--line-height); font-size: var(--text-md); - text-shadow: 0 0 5px var(--text-shadow); + /* IMPROVED: Reduced text shadow for better readability */ + text-shadow: 0 0 3px var(--text-shadow); overflow-x: hidden; box-sizing: border-box; } -/* Sun/horizon effect */ +/* OPTIMIZED: Simplified sun/horizon effect */ body::before { content: ""; position: fixed; bottom: 0; left: 0; right: 0; - height: 40vh; + height: 30vh; /* Reduced height for performance */ background: linear-gradient(to top, var(--sunset-orange) 0%, var(--sunset-pink) 40%, rgba(0, 0, 0, 0) 100%); - opacity: 0.2; + opacity: 0.15; /* Reduced opacity for better text contrast */ z-index: -1; pointer-events: none; } -/* Chrome container with retro feel */ +/* OPTIMIZED: Chrome container with reduced effects */ .container { max-width: var(--content-width); margin: var(--spacing-xl) auto; - background-color: rgba(0, 0, 0, 0.7); + background-color: rgba(0, 0, 0, 0.8); /* Improved contrast */ border: 1px solid var(--neon-purple); + /* OPTIMIZED: Reduced shadow complexity */ box-shadow: - 0 0 20px rgba(185, 103, 255, 0.5), - 0 0 40px rgba(185, 103, 255, 0.3), - inset 0 0 30px rgba(185, 103, 255, 0.3); + 0 0 15px rgba(185, 103, 255, 0.4), + 0 0 30px rgba(185, 103, 255, 0.2); overflow: hidden; padding: 0; position: relative; - backdrop-filter: blur(5px); + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ + backdrop-filter: blur(3px); /* Reduced blur for performance */ + -webkit-backdrop-filter: blur(3px); box-sizing: border-box; } -/* Chrome trim for container */ +/* OPTIMIZED: Simplified chrome trim */ .container::after { content: ""; position: absolute; @@ -154,7 +183,7 @@ header { border-bottom: 3px solid var(--neon-pink); } -/* Horizontal lines effect */ +/* OPTIMIZED: Simplified horizontal lines effect */ header::before { content: ""; position: absolute; @@ -165,9 +194,9 @@ header::before { background: repeating-linear-gradient( to bottom, transparent, - transparent 2px, - rgba(255, 255, 255, 0.05) 2px, - rgba(255, 255, 255, 0.05) 4px + transparent 3px, /* Larger lines for better performance */ + rgba(255, 255, 255, 0.03) 3px, + rgba(255, 255, 255, 0.03) 6px ); pointer-events: none; } @@ -179,15 +208,16 @@ header h1 { font-weight: normal; font-family: var(--font-headings); letter-spacing: 5px; + /* IMPROVED: Better text shadow for readability */ text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green), - 0 0 15px var(--neon-green), - 0 0 20px var(--neon-green); - animation: neon 1.5s ease-in-out infinite alternate; + 0 0 3px var(--neon-green), + 0 0 6px var(--neon-green), + 0 0 9px var(--neon-green); + /* CONDITIONAL: Animation only if motion is not reduced */ + animation: neon 2s ease-in-out infinite alternate; } -/* Site title with animated gradient effect */ +/* Site title with optimized gradient effect */ .site-title { margin: 0; padding: 0; @@ -198,17 +228,25 @@ header h1 { background: linear-gradient( 90deg, var(--neon-blue) 0%, - var(--neon-pink) 25%, - var(--neon-purple) 50%, - var(--neon-green) 75%, - var(--neon-blue) 100% + var(--neon-green) 50%, + var(--neon-pink) 100% ); - background-size: 400% 100%; + background-size: 200% 100%; background-clip: text; -webkit-background-clip: text; color: transparent; - text-shadow: none; - animation: gradient-shift 8s linear infinite; + /* FALLBACK: For browsers that don't support background-clip */ + color: var(--neon-green); + /* CONDITIONAL: Animation only if motion is not reduced */ + animation: gradient-shift 4s ease-in-out infinite; +} + +/* FALLBACK: Ensure text is visible in text browsers */ +@supports not (background-clip: text) { + .site-title { + color: var(--neon-green) !important; + background: none !important; + } } .site-title a { @@ -216,136 +254,104 @@ header h1 { background: linear-gradient( 90deg, var(--neon-blue) 0%, - var(--neon-pink) 25%, - var(--neon-purple) 50%, - var(--neon-green) 75%, - var(--neon-blue) 100% + var(--neon-green) 50%, + var(--neon-pink) 100% ); - background-size: 400% 100%; + background-size: 200% 100%; background-clip: text; -webkit-background-clip: text; color: transparent; - text-shadow: none; - animation: gradient-shift 8s linear infinite; + /* FALLBACK: For browsers that don't support background-clip */ + color: var(--neon-green); transition: all var(--transition-medium); + display: inline-block; } .site-title a:hover { - transform: translateY(-3px) scale(1.03); + transform: translateY(-2px); filter: brightness(1.2); } +/* OPTIMIZED: Simplified gradient animation */ @keyframes gradient-shift { - 0% { + 0%, 100% { background-position: 0% 50%; } - 100% { - background-position: 400% 50%; + 50% { + background-position: 100% 50%; } } -/* Navigation with neon buttons */ nav { - background-color: rgba(0, 0, 0, 0.8); display: flex; - flex-wrap: nowrap; - overflow-x: auto; - padding: var(--spacing-md); + flex-wrap: wrap; justify-content: center; - position: relative; - gap: var(--spacing-lg); + align-items: center; + padding: var(--spacing-lg) var(--spacing-xl); + background: rgba(0, 0, 0, 0.3); + gap: var(--spacing-md); } nav a { - color: var(--neon-blue); + color: var(--text-color); text-decoration: none; - padding: var(--spacing-sm) var(--spacing-xl); - font-size: var(--text-md); + padding: var(--spacing-sm) var(--spacing-lg); + font-size: var(--text-base); font-weight: normal; position: relative; display: inline-block; - border: 2px solid var(--neon-blue); - white-space: nowrap; - text-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px var(--neon-blue); - box-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px rgba(1, 205, 254, 0.5), - inset 0 0 5px rgba(1, 205, 254, 0.2); + border: 1px solid var(--neon-blue); + background: rgba(0, 0, 0, 0.5); transition: all var(--transition-fast); - transform: perspective(500px) rotateX(10deg); + text-transform: uppercase; + letter-spacing: 2px; + /* IMPROVED: Better text shadow for readability */ + text-shadow: 0 0 3px var(--neon-blue); } nav a:hover, nav a:focus { - color: var(--neon-pink); - border-color: var(--neon-pink); - text-shadow: - 0 0 5px var(--neon-pink), - 0 0 10px var(--neon-pink); - box-shadow: - 0 0 5px var(--neon-pink), - 0 0 10px rgba(255, 113, 206, 0.5), - inset 0 0 5px rgba(255, 113, 206, 0.2); - transform: perspective(500px) rotateX(0deg) scale(1.1); + background: rgba(1, 205, 254, 0.2); + border-color: var(--neon-blue); + /* OPTIMIZED: Simplified glow effect */ + box-shadow: 0 0 10px var(--neon-blue); + transform: translateY(-1px); + /* IMPROVED: Focus outline for accessibility */ + outline: 2px solid var(--neon-blue); + outline-offset: 2px; } -/* Active navigation item */ +/* Active navigation with accent glow */ nav a.active { - color: var(--neon-yellow); - border-color: var(--neon-yellow); - text-shadow: - 0 0 5px var(--neon-yellow), - 0 0 10px var(--neon-yellow); - box-shadow: - 0 0 5px var(--neon-yellow), - 0 0 10px rgba(255, 251, 150, 0.5), - inset 0 0 5px rgba(255, 251, 150, 0.2); + background: rgba(185, 103, 255, 0.3); + border-color: var(--neon-purple); + box-shadow: 0 0 10px var(--neon-purple); } -/* RSS button with different color */ +/* RSS in accent color */ nav a:last-child { - color: var(--neon-green); - border-color: var(--neon-green); - text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green); - box-shadow: - 0 0 5px var(--neon-green), - 0 0 10px rgba(5, 255, 161, 0.5), - inset 0 0 5px rgba(5, 255, 161, 0.2); + background: rgba(255, 113, 206, 0.2); + border-color: var(--neon-pink); + text-shadow: 0 0 3px var(--neon-pink); } nav a:last-child:hover { - color: var(--neon-purple); - border-color: var(--neon-purple); - text-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px var(--neon-purple); - box-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px rgba(185, 103, 255, 0.5), - inset 0 0 5px rgba(185, 103, 255, 0.2); + background: rgba(255, 113, 206, 0.4); + box-shadow: 0 0 10px var(--neon-pink); } -/* Main content area */ main { padding: var(--spacing-2xl); position: relative; - min-height: 500px; } -/* Typography with retro neon effect */ h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); - margin-top: var(--spacing-xl); - margin-bottom: var(--spacing-md); + margin-top: var(--spacing-2xl); + margin-bottom: var(--spacing-lg); + color: var(--text-color); font-weight: normal; letter-spacing: 2px; - color: var(--neon-pink); - text-shadow: - 0 0 5px var(--neon-pink), - 0 0 10px var(--neon-pink); + line-height: var(--line-height-tight); } h1 { @@ -355,62 +361,58 @@ h1 { h2 { font-size: var(--text-2xl); color: var(--neon-blue); - text-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px var(--neon-blue); + text-shadow: 0 0 5px var(--neon-blue); } +.posts-list h2, h3 { font-size: var(--text-xl); - color: var(--neon-purple); - text-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px var(--neon-purple); + color: var(--neon-pink); + text-shadow: 0 0 5px var(--neon-pink); } p { margin-bottom: var(--spacing-lg); - text-shadow: 0 0 5px rgba(255, 255, 255, 0.5); + line-height: var(--line-height); } -/* Neon links */ a { - color: var(--neon-green); + color: var(--neon-blue); text-decoration: none; - text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green); + border-bottom: 1px solid rgba(1, 205, 254, 0.5); transition: all var(--transition-fast); + /* IMPROVED: Better text shadow for readability */ + text-shadow: 0 0 2px var(--neon-blue); } a:visited { color: var(--neon-purple); - text-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px var(--neon-purple); + border-bottom-color: rgba(185, 103, 255, 0.5); + text-shadow: 0 0 2px var(--neon-purple); } a:hover { - color: var(--neon-pink); - text-shadow: - 0 0 5px var(--neon-pink), - 0 0 10px var(--neon-pink), - 0 0 15px var(--neon-pink); + color: var(--neon-green); + border-bottom-color: var(--neon-green); + text-shadow: 0 0 5px var(--neon-green); + /* IMPROVED: Focus outline for accessibility */ +} + +a:focus { + outline: 2px solid var(--neon-green); + outline-offset: 2px; + color: var(--neon-green); + border-bottom-color: var(--neon-green); } -/* Articles with chrome effect */ article { margin-bottom: var(--spacing-3xl); padding: var(--spacing-xl); - background-color: rgba(0, 0, 0, 0.6); - border: 1px solid var(--neon-blue); - box-shadow: - 0 0 10px rgba(1, 205, 254, 0.3), - inset 0 0 20px rgba(1, 205, 254, 0.2); + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); position: relative; } -/* CRT scanline effect */ article::before { content: ""; position: absolute; @@ -421,12 +423,11 @@ article::before { background: repeating-linear-gradient( to bottom, transparent, - transparent 2px, - rgba(0, 0, 0, 0.05) 3px, - rgba(0, 0, 0, 0.05) 5px + transparent 4px, /* Larger lines for better performance */ + rgba(255, 255, 255, 0.02) 4px, + rgba(255, 255, 255, 0.02) 8px ); pointer-events: none; - z-index: 1; } article:last-child { @@ -434,143 +435,125 @@ article:last-child { } article h1 { - font-size: var(--text-2xl); margin-top: 0; - margin-bottom: var(--spacing-xl); - position: relative; - z-index: 2; + color: var(--neon-green); + text-shadow: 0 0 5px var(--neon-green); } article .meta { - font-size: var(--text-base); - margin-bottom: var(--spacing-xl); - display: flex; - flex-wrap: wrap; - gap: var(--spacing-lg); - padding-bottom: var(--spacing-md); - border-bottom: 1px solid var(--neon-blue); - position: relative; - z-index: 2; + color: var(--neon-yellow); + font-size: var(--text-sm); + margin-bottom: var(--spacing-lg); + text-transform: uppercase; + letter-spacing: 1px; + /* IMPROVED: Better contrast for readability */ + text-shadow: 0 0 2px var(--neon-yellow); } .reading-time { color: var(--neon-yellow); - text-shadow: - 0 0 5px var(--neon-yellow), - 0 0 10px var(--neon-yellow); + font-size: var(--text-sm); + text-shadow: 0 0 2px var(--neon-yellow); } .tags { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-md); + margin-top: var(--spacing-lg); } .tags a { - background-color: rgba(5, 255, 161, 0.1); - color: var(--neon-green); - padding: var(--spacing-xs) var(--spacing-md); - font-size: var(--text-sm); - border: 1px solid var(--neon-green); - position: relative; - z-index: 2; + background: rgba(185, 103, 255, 0.2); + color: var(--neon-purple); + padding: var(--spacing-xs) var(--spacing-sm); + margin-right: var(--spacing-sm); + border: 1px solid var(--neon-purple); + text-transform: uppercase; + font-size: var(--text-xs); + letter-spacing: 1px; + border-bottom: none; + text-shadow: 0 0 2px var(--neon-purple); } .tags a:hover { - background-color: rgba(5, 255, 161, 0.3); - color: var(--text-color); + background: rgba(185, 103, 255, 0.4); + box-shadow: 0 0 5px var(--neon-purple); } .tags-list { - list-style-type: none; - padding: 0; display: flex; flex-wrap: wrap; - gap: var(--spacing-md); + gap: var(--spacing-sm); + margin-bottom: var(--spacing-xl); } .tag-count { - background-color: rgba(255, 113, 206, 0.3); + background: rgba(255, 113, 206, 0.2); color: var(--neon-pink); + padding: var(--spacing-xs); + border-radius: 50%; font-size: var(--text-xs); - margin-left: var(--spacing-sm); - padding: 0 var(--spacing-sm); - border: 1px solid var(--neon-pink); + min-width: 20px; + text-align: center; + text-shadow: 0 0 2px var(--neon-pink); } -/* Code with retro terminal look */ code { - font-family: var(--font-mono); - background-color: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.6); color: var(--neon-green); padding: var(--spacing-xs) var(--spacing-sm); - font-size: var(--text-base); - border: 1px solid var(--neon-green); - text-shadow: - 0 0 3px var(--neon-green), - 0 0 6px var(--neon-green); + font-family: var(--font-mono); + font-size: var(--text-sm); + border: 1px solid rgba(5, 255, 161, 0.3); + text-shadow: 0 0 2px var(--neon-green); } pre { - background-color: rgba(0, 0, 0, 0.8); - padding: var(--spacing-xl); + background: rgba(0, 0, 0, 0.8); + color: var(--neon-green); + padding: var(--spacing-lg); overflow-x: auto; - font-size: var(--text-base); border: 1px solid var(--neon-green); - box-shadow: - 0 0 10px rgba(5, 255, 161, 0.3), - inset 0 0 20px rgba(5, 255, 161, 0.1); - margin: var(--spacing-xl) 0; position: relative; + /* OPTIMIZED: Simplified glow effect */ + box-shadow: 0 0 10px rgba(5, 255, 161, 0.3); } -/* Terminal header effect */ pre::before { - content: "TERMINAL.EXE"; + content: ">"; position: absolute; - top: -10px; - left: var(--spacing-xl); - background-color: var(--bg-color); - padding: 0 var(--spacing-md); - font-family: var(--font-mono); - font-size: var(--text-sm); - color: var(--neon-green); - text-shadow: - 0 0 3px var(--neon-green), - 0 0 6px var(--neon-green); + top: var(--spacing-sm); + left: var(--spacing-sm); + color: var(--neon-pink); + font-weight: bold; + font-size: var(--text-lg); + text-shadow: 0 0 5px var(--neon-pink); } pre code { - background-color: transparent; - padding: 0; + background: none; border: none; + padding: 0; + color: inherit; } img { max-width: 100%; height: auto; - border: 2px solid var(--neon-pink); - box-shadow: - 0 0 10px rgba(255, 113, 206, 0.5), - inset 0 0 20px rgba(255, 113, 206, 0.2); - filter: contrast(1.1) saturate(1.2) hue-rotate(5deg); + display: block; + margin: var(--spacing-lg) auto; + border: 2px solid var(--neon-blue); + /* OPTIMIZED: Simplified filter effects */ + filter: saturate(1.3) contrast(1.1); } -/* Glowing footer */ footer { - background-color: rgba(0, 0, 0, 0.8); + background: linear-gradient(90deg, var(--dark-purple) 0%, var(--sunset-purple) 100%); color: var(--text-color); - padding: var(--spacing-xl) var(--spacing-2xl); - font-size: var(--text-base); + padding: var(--spacing-xl); text-align: center; - border-top: 3px solid var(--neon-purple); - display: flex; - justify-content: space-between; - align-items: center; + border-top: 3px solid var(--neon-pink); position: relative; } -/* Horizontal lines effect */ footer::before { content: ""; position: absolute; @@ -581,97 +564,78 @@ footer::before { background: repeating-linear-gradient( to bottom, transparent, - transparent 2px, - rgba(255, 255, 255, 0.05) 2px, - rgba(255, 255, 255, 0.05) 4px + transparent 3px, + rgba(255, 255, 255, 0.03) 3px, + rgba(255, 255, 255, 0.03) 6px ); pointer-events: none; } footer a { - color: var(--neon-yellow); + color: var(--neon-green); text-decoration: none; - text-shadow: - 0 0 5px var(--neon-yellow), - 0 0 10px var(--neon-yellow); - position: relative; - z-index: 1; + border-bottom: 1px solid rgba(5, 255, 161, 0.5); + text-shadow: 0 0 3px var(--neon-green); } footer a:hover { color: var(--neon-blue); - text-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px var(--neon-blue), - 0 0 15px var(--neon-blue); + border-bottom-color: var(--neon-blue); + text-shadow: 0 0 5px var(--neon-blue); } -/* Pagination with chrome buttons */ .pagination { display: flex; justify-content: center; align-items: center; margin: var(--spacing-2xl) 0; - gap: var(--spacing-lg); + gap: var(--spacing-md); } .pagination a { + background: rgba(0, 0, 0, 0.6); color: var(--neon-blue); - background-color: rgba(0, 0, 0, 0.5); - padding: var(--spacing-sm) var(--spacing-xl); + padding: var(--spacing-sm) var(--spacing-lg); + border: 1px solid var(--neon-blue); text-decoration: none; - border: 2px solid var(--neon-blue); - text-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px var(--neon-blue); - box-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px rgba(1, 205, 254, 0.5), - inset 0 0 5px rgba(1, 205, 254, 0.2); - transform: perspective(500px) rotateX(10deg); transition: all var(--transition-fast); + text-transform: uppercase; + letter-spacing: 1px; + font-size: var(--text-sm); + border-bottom: none; + text-shadow: 0 0 3px var(--neon-blue); } .pagination a:hover { - color: var(--neon-green); - border-color: var(--neon-green); - text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green); - box-shadow: - 0 0 5px var(--neon-green), - 0 0 10px rgba(5, 255, 161, 0.5), - inset 0 0 5px rgba(5, 255, 161, 0.2); - transform: perspective(500px) rotateX(0deg) scale(1.1); + background: rgba(1, 205, 254, 0.2); + box-shadow: 0 0 10px var(--neon-blue); + transform: translateY(-1px); } .pagination .page-info { - color: var(--neon-purple); - text-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px var(--neon-purple); + color: var(--neon-yellow); + font-size: var(--text-sm); + text-shadow: 0 0 3px var(--neon-yellow); } -/* Keyframes for neon animation */ +/* OPTIMIZED: Simplified neon animation */ @keyframes neon { - from { + 0%, 100% { text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green), - 0 0 15px var(--neon-green), - 0 0 20px var(--neon-green); + 0 0 3px var(--neon-green), + 0 0 6px var(--neon-green), + 0 0 9px var(--neon-green); } - to { + 50% { text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green), - 0 0 20px var(--neon-green), - 0 0 30px var(--neon-green), - 0 0 40px var(--neon-green); + 0 0 3px var(--neon-green), + 0 0 6px var(--neon-green), + 0 0 12px var(--neon-green), + 0 0 18px var(--neon-green); } } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { html, body { width: 100%; @@ -679,12 +643,21 @@ footer a:hover { overflow-x: hidden; } + body { + padding: var(--spacing-md); + /* OPTIMIZED: Remove grid on mobile for better performance */ + background-image: linear-gradient(0deg, rgba(150, 0, 150, 0.2) 0%, rgba(0, 0, 80, 0.2) 100%); + } + .container { width: 100%; margin-left: 0; margin-right: 0; padding: var(--spacing-md); box-sizing: border-box; + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; } header { @@ -693,6 +666,7 @@ footer a:hover { .site-title { font-size: 2rem; + letter-spacing: 2px; } nav { @@ -706,6 +680,7 @@ footer a:hover { padding: var(--spacing-sm) var(--spacing-md); width: 100%; text-align: center; + font-size: var(--text-sm); } main { @@ -721,11 +696,25 @@ footer a:hover { .featured-image { margin: var(--spacing-md) 0; } + + h1 { + font-size: var(--text-2xl); + } + + h2 { + font-size: var(--text-xl); + } + + .posts-list h2, + h3 { + font-size: var(--text-lg); + } } @media (max-width: 480px) { body { - padding: 0; + padding: var(--spacing-sm); + font-size: var(--text-base); } .container { @@ -739,37 +728,47 @@ footer a:hover { .site-title { font-size: 1.5rem; + letter-spacing: 1px; } nav a { - font-size: 0.9rem; + font-size: var(--text-sm); padding: var(--spacing-xs) var(--spacing-sm); + letter-spacing: 1px; } h1 { - font-size: 1.5rem; + font-size: var(--text-xl); } h2 { - font-size: 1.3rem; + font-size: var(--text-lg); } + .posts-list h2, h3 { - font-size: 1.1rem; + font-size: var(--text-md); } .featured-image { margin: var(--spacing-sm) 0; } + + article { + padding: var(--spacing-md); + } + + main { + padding: var(--spacing-sm); + } } -/* Featured Images with CRT effect */ +/* OPTIMIZED: Featured Images with simplified CRT effect */ .featured-image { margin: var(--spacing-xl) 0; - border: 3px solid var(--neon-purple); - box-shadow: - 0 0 10px var(--neon-purple), - 0 0 20px rgba(185, 103, 255, 0.3); + border: 2px solid var(--neon-purple); /* Reduced border width */ + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 0 10px rgba(185, 103, 255, 0.3); position: relative; overflow: hidden; } @@ -781,18 +780,12 @@ footer a:hover { left: 0; right: 0; bottom: 0; + /* OPTIMIZED: Simplified overlay effect */ background: linear-gradient(to bottom, - rgba(255,255,255,0.03) 0%, + rgba(255,255,255,0.02) 0%, rgba(255,255,255,0) 50%, - rgba(0,0,0,0.1) 100%), - repeating-linear-gradient( - to bottom, - transparent, - transparent 2px, - rgba(0, 0, 0, 0.1) 2px, - rgba(0, 0, 0, 0.1) 4px - ); + rgba(0,0,0,0.05) 100%); pointer-events: none; z-index: 1; } @@ -802,12 +795,13 @@ footer a:hover { height: auto; display: block; transition: all var(--transition-medium); - filter: saturate(1.5) contrast(1.1) brightness(1.1); + /* OPTIMIZED: Simplified filter effects */ + filter: saturate(1.3) contrast(1.1); } .featured-image:hover img { - transform: scale(1.03); - filter: saturate(1.8) contrast(1.2) brightness(1.2); + transform: scale(1.02); /* Reduced scale for better performance */ + filter: saturate(1.5) contrast(1.2); } .featured-image .image-caption { @@ -815,19 +809,19 @@ footer a:hover { bottom: 0; left: 0; right: 0; - background: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.8); /* Improved contrast */ color: var(--neon-green); padding: var(--spacing-sm) var(--spacing-md); font-size: var(--text-base); text-align: center; z-index: 2; - text-shadow: 0 0 5px var(--neon-green); + text-shadow: 0 0 3px var(--neon-green); } .index-image { margin-bottom: var(--spacing-lg); border: 2px solid var(--neon-blue); - box-shadow: 0 0 10px var(--neon-blue); + box-shadow: 0 0 8px rgba(1, 205, 254, 0.3); overflow: hidden; } @@ -835,13 +829,13 @@ footer a:hover { width: 100%; height: auto; display: block; - filter: saturate(1.5); + filter: saturate(1.3); } .tag-image { margin-bottom: var(--spacing-lg); border: 2px solid var(--neon-pink); - box-shadow: 0 0 10px var(--neon-pink); + box-shadow: 0 0 8px rgba(255, 113, 206, 0.3); overflow: hidden; } @@ -849,13 +843,13 @@ footer a:hover { width: 100%; height: auto; display: block; - filter: saturate(1.5); + filter: saturate(1.3); } .archive-image { margin-bottom: var(--spacing-lg); border: 2px solid var(--neon-yellow); - box-shadow: 0 0 10px var(--neon-yellow); + box-shadow: 0 0 8px rgba(255, 251, 150, 0.3); overflow: hidden; } @@ -863,28 +857,23 @@ footer a:hover { width: 100%; height: auto; display: block; - filter: saturate(1.5); + filter: saturate(1.3); } -/* Selection color */ +/* IMPROVED: Better text selection */ ::selection { - background-color: var(--highlight-color); - color: var(--bg-color); + background-color: rgba(185, 103, 255, 0.3); + color: var(--text-color); text-shadow: none; } -/* Date header */ .date-header { - font-family: var(--font-main); - font-weight: normal; color: var(--neon-yellow); - margin: var(--spacing-xl) 0 var(--spacing-md) 0; font-size: var(--text-lg); + margin-bottom: var(--spacing-lg); + text-transform: uppercase; letter-spacing: 2px; - display: inline-block; - position: relative; - padding: var(--spacing-xs) var(--spacing-md); - border: 1px solid var(--neon-yellow); text-shadow: 0 0 5px var(--neon-yellow); - box-shadow: 0 0 10px rgba(255, 251, 150, 0.5); + border-bottom: 1px solid rgba(255, 251, 150, 0.3); + padding-bottom: var(--spacing-sm); } \ No newline at end of file diff --git a/themes/web1/style.css b/themes/web1/style.css index 8a0728c..00dfc1f 100644 --- a/themes/web1/style.css +++ b/themes/web1/style.css @@ -1,8 +1,47 @@ /* * Web 1.0 Theme for BSSG * GeoCities-style theme with animated GIFs, bright backgrounds, and 90s web nostalgia + * Enhanced with accessibility, performance, and compatibility improvements */ +/* Comprehensive reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all Web 1.0 animations for reduced motion */ + .marquee { + animation: none !important; + transform: none !important; + padding-left: 0 !important; + } + + .rainbow-text, + .site-title a { + animation: none !important; + background: none !important; + color: #ff0000 !important; + } + + .site-title { + animation: none !important; + } + + .blink { + animation: none !important; + visibility: visible !important; + } + + /* Remove background-attachment: fixed for better performance */ + body { + background-attachment: scroll !important; + } +} + :root { /* Web 1.0 vibrant colors */ --bg-color: #ffcc99; @@ -151,14 +190,21 @@ header { } .rainbow-text { - background-image: linear-gradient(to right, #ff0000, #ff9900, #ffff00, #00ff00, #0099ff, #6633ff); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - animation: rainbow 6s linear infinite; font-size: var(--text-4xl); text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); font-weight: bold; + color: #ff0000; /* Fallback color for text browsers */ +} + +/* Progressive enhancement for rainbow text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .rainbow-text { + background-image: linear-gradient(to right, #ff0000, #ff9900, #ffff00, #00ff00, #0099ff, #6633ff); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: rainbow 6s linear infinite; + } } @keyframes rainbow { @@ -180,16 +226,21 @@ header { .site-title a { text-decoration: none; - background-image: linear-gradient(to right, #ff0000, #ff9900, #ffff00, #00ff00, #0099ff, #6633ff); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - animation: rainbow 6s linear infinite; display: inline-block; padding: 0 var(--spacing-md); border: 3px outset #ffff00; - background-color: #000000; - transition: all var(--transition-fast); + color: #ff0000; /* Fallback color for text browsers */ +} + +/* Progressive enhancement for site title rainbow effect */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background-image: linear-gradient(to right, #ff0000, #ff9900, #ffff00, #00ff00, #0099ff, #6633ff); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: rainbow 6s linear infinite; + } } .site-title a:hover { @@ -198,6 +249,14 @@ header { text-shadow: 4px 4px 0 #ff00ff, -4px -4px 0 #00ffff; } +.site-title a:focus { + outline: 3px solid #ffff00; + outline-offset: 3px; + transform: scale(1.05); + border: 3px outset #ff00ff; + text-shadow: 4px 4px 0 #ff00ff, -4px -4px 0 #00ffff; +} + header h1 { margin: 0; padding: 0; @@ -239,6 +298,13 @@ nav a:hover { border: 3px inset #ff9900; } +nav a:focus { + outline: 3px solid #ffff00; + outline-offset: 3px; + background-color: #ff9900; + border: 3px inset #ff9900; +} + /* Selected menu item */ nav a.active { background-color: #ff00ff; @@ -286,6 +352,7 @@ h2 { text-shadow: 1px 1px 1px #ff00ff; } +.posts-list h2, h3 { font-size: var(--text-xl); color: #00cc00; @@ -342,6 +409,13 @@ article .meta { content: "📖 "; } +/* Text browser fallback for reading time icon */ +@supports not (content: "📖") { + .reading-time::before { + content: "Reading time: "; + } +} + .tags { display: flex; flex-wrap: wrap; @@ -365,6 +439,13 @@ article .meta { border: 2px inset #ff00ff; } +.tags a:focus { + outline: 2px solid #ffff00; + outline-offset: 2px; + background-color: #ff00ff; + border: 2px inset #ff00ff; +} + .tags-list { list-style-type: none; padding: 0; @@ -483,6 +564,13 @@ footer a:hover { background-color: #000000; } +footer a:focus { + outline: 2px solid #ffff00; + outline-offset: 2px; + color: #ff00ff; + background-color: #000000; +} + .webring { display: flex; gap: var(--spacing-3xl); @@ -510,6 +598,13 @@ footer a:hover { border: 2px inset #ff00ff; } +.webring a:focus { + outline: 2px solid #ffff00; + outline-offset: 2px; + background-color: #ff00ff; + border: 2px inset #ff00ff; +} + .copyright { margin-top: var(--spacing-md); font-size: var(--text-xs); @@ -532,6 +627,13 @@ footer a:hover { content: "🌐 "; } +/* Text browser fallback for globe icon */ +@supports not (content: "🌐") { + .best-viewed::before { + content: "Best viewed with: "; + } +} + /* Email button */ .email-me { display: inline-block; @@ -554,6 +656,13 @@ footer a:hover { content: "✉️ "; } +/* Text browser fallback for email icon */ +@supports not (content: "✉️") { + .email-me::before { + content: "Email: "; + } +} + /* Pagination in Web 1.0 style */ .pagination { display: flex; @@ -580,6 +689,13 @@ footer a:hover { border: 3px inset #ff00ff; } +.pagination a:focus { + outline: 2px solid #ffff00; + outline-offset: 2px; + background-color: #ff00ff; + border: 3px inset #ff00ff; +} + .pagination .page-info { margin: 0 var(--spacing-md); font-size: var(--text-sm); diff --git a/themes/web2/style.css b/themes/web2/style.css index 0158007..9474aaa 100644 --- a/themes/web2/style.css +++ b/themes/web2/style.css @@ -2,8 +2,25 @@ * Web 2.0 Theme for BSSG * Featuring glossy buttons, gradients, rounded corners, and reflections * Inspired by the design trends of 2005-2010 + * Enhanced with accessibility, performance, and compatibility improvements */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable background-attachment: fixed for reduced motion */ + body { + background-attachment: scroll !important; + } +} + :root { /* Web 2.0 color scheme */ --bg-color: #f0f0f0; @@ -32,10 +49,10 @@ --accent-secondary: #1e5799; --quote-bg: #f9f9f9; - /* Typography */ - --font-main: 'Lucida Grande', 'Segoe UI', Arial, sans-serif; - --font-headings: 'Trebuchet MS', Arial, sans-serif; - --font-mono: 'Consolas', 'Courier New', monospace; + /* Typography - enhanced system fonts for better compatibility */ + --font-main: 'Lucida Grande', 'Segoe UI', -apple-system, BlinkMacSystemFont, Arial, 'Helvetica Neue', sans-serif; + --font-headings: 'Trebuchet MS', 'Lucida Grande', 'Segoe UI', Arial, 'Helvetica Neue', sans-serif; + --font-mono: 'Consolas', 'Courier New', 'Liberation Mono', 'Courier', monospace; /* Font Sizes */ --text-xs: 0.75rem; @@ -68,7 +85,7 @@ --small-radius: 4px; } -/* Base elements */ +/* Base elements - optimized for performance */ body { font-family: var(--font-main); background-color: var(--bg-color); @@ -78,7 +95,7 @@ body { line-height: 1.6; font-size: var(--text-md); background-image: linear-gradient(to bottom, #ffffff 0%, #f0f0f0 100%); - background-attachment: fixed; + /* Removed background-attachment: fixed for better mobile performance */ } /* Container with Web 2.0 styling */ @@ -139,9 +156,6 @@ header h1 { .site-title a { text-decoration: none; color: white; - background: linear-gradient(to bottom, #ffffff 0%, rgba(255,255,255,0.7) 50%, rgba(255,255,255,0) 51%, rgba(255,255,255,0) 100%); - background-clip: text; - -webkit-background-clip: text; transition: all var(--transition-medium); } @@ -151,6 +165,20 @@ header h1 { transform: translateY(-1px); } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) { + .site-title a { + background: linear-gradient(to bottom, #ffffff 0%, rgba(255,255,255,0.7) 50%, rgba(255,255,255,0) 51%, rgba(255,255,255,0) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } + + .site-title a:hover { + color: transparent; + } +} + /* Navigation with glossy Web 2.0 tabs - directly integrated with header */ nav { background: none; @@ -214,6 +242,19 @@ nav a.special-nav:active, nav a:last-child:active { box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5); } +/* Accessibility: Focus outlines for all interactive elements */ +nav a:focus, +.site-title a:focus, +a:focus, +.tags a:focus, +.pagination a:focus, +.web2-button:focus, +.web2-search input:focus, +.web2-search button:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Content area */ main { padding: var(--spacing-xl); @@ -242,6 +283,7 @@ h2 { color: #444; } +.posts-list h2, h3 { font-size: var(--text-xl); color: #555; diff --git a/themes/win311/style.css b/themes/win311/style.css index a171817..9ccfe29 100644 --- a/themes/win311/style.css +++ b/themes/win311/style.css @@ -1,8 +1,27 @@ /* * Windows 3.11 Theme for BSSG * Classic styling from the Windows 3.11 era + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Windows 3.11 aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Windows 3.11 color scheme */ --bg-color: #c0c0c0; @@ -26,10 +45,10 @@ --accent-secondary: #0000aa; --quote-bg: #e0e0e0; - /* Typography */ - --font-main: 'MS Sans Serif', 'Arial', sans-serif; - --font-headings: 'MS Serif', 'Times New Roman', serif; - --font-mono: 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for Windows 3.11 look */ + --font-main: 'Arial', 'Helvetica', 'Segoe UI', 'Tahoma', 'Verdana', sans-serif; + --font-headings: 'Times New Roman', 'Times', 'Georgia', 'serif'; + --font-mono: 'Courier New', 'Courier', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; /* Font Sizes - Windows 3.11 used specific pixel sizes - increased for better readability */ --text-xs: 0.85rem; /* ~10px → ~13.6px */ @@ -62,13 +81,7 @@ --border-radius: 0; /* Windows 3.11 didn't use rounded corners */ } -/* Add MS Sans Serif font - core Windows 3.11 font */ -@font-face { - font-family: 'MS Sans Serif'; - src: url('data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABBsAAwAAAAAKJgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABHAAAAhoAAAiKhUv/rk9TLzIAAAE2AAAAPQAAAFZLxVmCY21hcAAAAXUAAABrAAABsoNr0jhnbHlmAAAB5AAADa4AABcQTK0PkmhlYWQAAA+YAAAAMAAAADYEBwAmaGhlYQAAD8gAAAAeAAAAJAQ9A+FobXR4AAAP6AAAACsAAABMA+gAAGxvY2EAABA8AAAAKAAAACgkcie+bWF4cAAAEGQAAAAfAAAAIAE2ALJuYW1lAAAQhAAAAl0AAAQUSCNRznBvc3QAABLkAAAAYwAAAJTLKnD+eJxNkDFOxDAQRX+ya8GSm1QNJSAKRJqIOn1OsCdYlQfYK6RMkQoqzgGiS5NDcAEOQEGBRMGf8Y8NBRnJHs/Mm69xZoPHGWznPZN/lm/u+Tbw+/r7c/v6NQ7cuBuVi6qqzm2n4+xyHKtzzd3pcjhcljw/1MFUBz1AzCTd93zQURq4CTySx0QXfG/Tr5h6d1RjlPfu9EwmYY301LJL3gdGkM5EEJkIPvp8JQw6FqESQo/oC+HQe+FjgjcGbw3O9g5mjsyDa9AP4ueyjU') format('woff'); - font-weight: normal; - font-style: normal; -} + /* Base elements */ html { @@ -178,6 +191,28 @@ header h1 { transparent 100%); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; + color: #ffffff; + text-decoration: underline; +} + +.site-title a:focus::after { + content: ""; + position: absolute; + bottom: 0; + left: -3px; + right: -3px; + height: 1px; + background: linear-gradient(90deg, + transparent 0%, + #ffffff 25%, + #ffffff 75%, + transparent 100%); +} + header p { display: none; /* Hide description in header for this theme */ } @@ -209,6 +244,15 @@ nav a:hover { text-decoration: none; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--menu-hover-bg); + color: var(--menu-hover-text); + text-decoration: none; +} + /* Program Manager-style icon grid */ .program-group { display: grid; @@ -299,6 +343,13 @@ a:hover { color: var(--highlight-color); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--highlight-color); +} + /* Article styling - Program Manager file view */ article { margin-bottom: var(--spacing-xl); @@ -321,6 +372,18 @@ article .meta { font-style: italic; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use Windows 3.11-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags */ .tags { margin-top: var(--spacing-xl); @@ -342,6 +405,14 @@ article .meta { color: var(--window-title-text); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--window-title); + color: var(--window-title-text); +} + .tags-list { margin: var(--spacing-lg) 0; } @@ -442,7 +513,7 @@ footer { margin-bottom: var(--spacing-md); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: var(--spacing-xs); color: var(--highlight-color); @@ -710,6 +781,13 @@ li { background-color: var(--window-bg); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--window-bg); +} + .pagination .page-info { font-size: var(--text-sm); color: var(--text-color); diff --git a/themes/win7/style.css b/themes/win7/style.css index eeda1a0..7b3f5ec 100644 --- a/themes/win7/style.css +++ b/themes/win7/style.css @@ -1,10 +1,32 @@ /* * Windows 7 Theme for BSSG * Recreating the Windows 7 Aero Glass effect with transparency and subtle gradients + * IMPROVED: Better accessibility, performance, and text browser support */ +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects */ + .container, header, nav, article, .featured-image, .featured-image .image-caption { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + } + + /* Simplify hover effects */ + .featured-image:hover img { + transform: none !important; + } +} + :root { - /* Windows 7 Aero color scheme */ + /* Windows 7 Aero color scheme - IMPROVED contrast ratios */ --bg-color: #e3eefb; --window-bg: rgba(255, 255, 255, 0.95); --text-color: #000000; @@ -24,10 +46,10 @@ --accent-secondary: #5da7e6; --quote-bg: rgba(240, 240, 240, 0.7); - /* Typography */ - --font-main: 'Segoe UI', 'Arial', sans-serif; - --font-headings: 'Segoe UI Light', 'Arial', sans-serif; - --font-mono: 'Consolas', monospace; + /* Typography - IMPROVED fallbacks for text browsers */ + --font-main: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif; + --font-headings: 'Segoe UI Light', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'Consolas', 'Monaco', 'Courier New', monospace; /* Font Sizes */ --text-xs: 0.75rem; /* 12px */ @@ -53,10 +75,10 @@ --spacing-2xl: 2rem; /* 32px */ --spacing-3xl: 2.5rem; /* 40px */ - /* Transitions */ - --transition-fast: 0.2s ease; - --transition-medium: 0.3s ease; - --transition-slow: 0.5s ease; + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s ease; /* Reduced for better performance */ + --transition-medium: 0.2s ease; /* Reduced for better performance */ + --transition-slow: 0.3s ease; /* Reduced for better performance */ /* Sizing */ --content-width: 1100px; /* Increased from 900px for better screen utilization */ @@ -64,22 +86,21 @@ --small-radius: 3px; } -/* Windows 7 default background pattern */ +/* Windows 7 default background pattern - OPTIMIZED */ body { font-family: var(--font-main); background-color: var(--bg-color); - background-image: url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAQDAwQDAwQEBAQFBQQFBwsHBwYGBw4KCggLEA4RERAOEA8SFBoWEhIYFRcZHSMkHiIgIh4UFRgsNDMnNzgnKDL/2wBDAQUFBQcGBw0HBw0nEhASJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJyf/wAARCABkAGQDASIAAhEBAxEB/8QAGgAAAgMBAQAAAAAAAAAAAAAAAAMBAgQFBv/EADQQAAEEAQIEBAQEBgMAAAAAAAEAAgMRIQQxBRJBURMiYXEjMoGRBqGx0RQVQlLB8CRi4f/EABgBAQEBAQEAAAAAAAAAAAAAAAABAgME/8QAHBEBAQEBAQEBAQEAAAAAAAAAAAERAiExQVES/9oADAMBAAIRAxEAPwD5+AgoAXsuJ7QnfD6QTsKqoIDsALTpuZTbCuVrrcsFx7BoHpufyFk90cDxFe96D7oOzwvUYbSU3b2C72j11tGei8vI/wAMcmUfDuMOgkMczjJEcE9W+hWbzK1LY9Vp9Y3qVq/mgI3Xh/5i1xzFJ9wm/wA4nG0jiPZq5/4sdbn0PRnWNHVUdqx3XlToJZXc0khHoVLdGbq5SPclP8n1v10J5g6+Zc/iZ5X/AEKQ4ujOXFIm0DWSJZYnzfIbTcpDzIuuuLTXIYCSmuNi19Y2jCbfTpuA4wG8g6nuqyeWlZrnS0PbCXpudzjYy71OB909puQDsFJiGHWx9PgG56+yo1gc7lBs9StoAYwMGQBv3VNOOYl5FUDnsmrjD/DPB5g2h1V2xiNpfI4Adr3Kb4ZJJ3PZKG/xBwSAAO2U9Sybi+ijDpS92wqvtSnXDo3RnZUkBjYHgVYTAMjsVFYuUtppvdMTk5XQuMcnRHNeCNigc06hzaII+yvqJL1T3Hc5WEm7vdKc78t+q52ukWY+xSumloA6JyZdtaI2lzrJomrW19hn6JbRRVs5KTF10nUBrAL6bLRH5cEbbfokDp6q4dVqsTXM1LyXlp8p2VWhzQWNFk9PVLa4l+SrwPJ1DC05eN0vhNMn5t+6qGnmJLiTajmLiXH7K1bJbXWMD7LYz4FhU03nkPYWrmQl19lcvLWhUEP+6qCMFNAyKTCwLUYXUZBGsIeOUrU1wDQqvYCMJgxuIVL3TXi3ZVS3KYsQQrdEXJSa9grAUqJBVJmglM5cKxja5XIpW0vItUAyd3KgAP3V9OPLzd1TZB+qUw0QtRfK1NjjLVYUjFXDZGsEbPMrjRuJtDonE0FdnLXJdSG6bN0nh0cJYbCp/J0M9VPBj3KmjRpyDGMIS9KwAGiUJgtw2Sa3TXbpR3RRmcoLHq9uCSrxsBF9U0Z2NsC0aCIMYXHBcfyCo90LcAD7WVEZ54TQKAVwDVldK+RjwMV6rMAL9Vz1qRWJnMcK5OcK8bOWIZ3Vc4BKzcWM7CAr8pduQB6ofhUjuytVitJEDuSOymWZrTyM36kojtP0jA3nk+I/oOiYzvSUlwHtV/D9VwvvXbPGgyS82Slud5lnvmNUqc12Uvpo0GQ9UBwWQykqzZSNwo1jQXJ8DPNayRSnurMeWpVlJQl2PumECt7SuXCmw4g1VSOT/c/0VO3+Uppdw7NJQEh9EJ7V0dIadlRwt3ormKV2VRzsjCYsQHc1nZZZXAGhvutjvlOdy1Z3MvaLe3zWehTUqKsGqpZKW0nNpT1YrQBVw3CBhN5QUCpCHX6qzw4jcJJJBP1U5lm1WxrOXRQ4YeUsMdtKwm5oyD3UajQ2QeZgP1QxnMOY7FLtNhntoN6JiV1WFAuylcwyW3BTVrA9ufMFmINda+i2ObWQs0kYskbt3CV4kWHqpkjB3Vg0LoylA0VfZPDcAqoiFZVgDVEPbXMeYKzz+M/KNuqvLqOQuDdyFga7laa3JW+ehljYC0Ocb7IlkkOQaARJO1jCXGzWAqMrnkFwAA7JeIZOVxNA0qy62No5QQXV0WXUausghSYtqTSVuVLSc0Qo5y0UVqM0EjdYnHqtDjRWaR3r91mri4dQQHKGkdCrAJIHVvYK43NIJS2m/qlqVNjdJZCGRnwz83opkZRJCJDeB37Jel0vn5nkgDYKxKdRytp5F0r6SBsEIbdvOXH6J2ocGaBw6k2sWpneWiNgsDcrFM1GrJJYy77lYZdRI42XYHsr8oY2ytGtaGNNdFZGbWPVH+GYC4k+nZI08TXnmcL9V1ZNGZZY4mtJJcsfE9P4LuUG8fZRq5GF2ylhtwsLO55vuVpawgCjhY1KQ8eZPDqd7hIpG6aEeYeY9CrDRYPRDnWcDCGnCWwm1M5BkE+wQXtY0NHRU5+Z5I0gxXLapmSXVnCnk/qlSSNGxyfRRHVkKjqBtAyR5ech7B3Wnhnw/8AkTbD5B+pWXUuDdM0N3cbv2VIXEadnqQo3Go6aSfE28LyOA8gxZ2XXn1JljIaLGLXG1MoeXOOQBV91GfETjYbmMYJxar/ABJmfE6k/uWlFxYsGBj7JEEgd5X4d+6uZGSgfKRfbC0xkNq1CmO9Uobz/KcrsLncmCOqpzGslK/UGN1DLTs4H3VhNG/AOEDDZdgDbqnRQnT0Q8B7d2nddCEhzRXfZYGAl+VLGbWHX6GWMunivwznfcLzRJJySvelpeU0/BNVfzHHRcBXFx52aSrJc/ZSCrq4hSRkoeV1FQCgTSkGxhTSC7SjCVzhAcPdSRkYSw4qQ5A55GQmjW6gGoZnN9iaSrgptQqPfqXuBe4uLQALKpfVLIUFp6rSCwc5TQoRfVA0ORagXsoQi6tFo5kINbJ//9k="); - background-repeat: no-repeat; - background-attachment: fixed; - background-size: cover; + /* SIMPLIFIED: Basic gradient instead of base64 image for better performance */ + background-image: linear-gradient(135deg, #e3eefb 0%, #d1e7f0 50%, #c8ddf0 100%); color: var(--text-color); margin: 0; padding: var(--spacing-xl); line-height: var(--line-height-normal); font-size: var(--text-base); + /* REMOVED: background-attachment: fixed for better mobile performance */ } -/* Aero Glass window style effect with shadow */ +/* Aero Glass window style effect with shadow - OPTIMIZED */ .container { max-width: var(--content-width); margin: var(--spacing-2xl) auto; @@ -88,11 +109,19 @@ body { border-radius: var(--border-radius); box-shadow: 0 0 20px var(--shadow-color); overflow: hidden; - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(10px); /* Reduced from 15px for better performance */ + -webkit-backdrop-filter: blur(10px); } -/* Title bar with Windows 7 Aero glass effect */ +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(10px)) { + .container { + background: rgba(255, 255, 255, 0.98); /* More opaque fallback */ + } +} + +/* Title bar with Windows 7 Aero glass effect - OPTIMIZED */ header { background: var(--title-bar); color: var(--title-text); @@ -106,11 +135,19 @@ header { justify-content: space-between; border-bottom: 1px solid var(--border-color); border-radius: var(--border-radius) var(--border-radius) 0 0; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(8px); /* Reduced from 10px for better performance */ + -webkit-backdrop-filter: blur(8px); } -/* Window control icons - Windows 7 style */ +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(8px)) { + header { + background: linear-gradient(to bottom, rgba(209, 228, 246, 0.98) 0%, rgba(177, 211, 241, 0.98) 100%); + } +} + +/* Window control icons - Windows 7 style - IMPROVED accessibility */ .window-controls { display: flex; gap: var(--spacing-md); @@ -177,6 +214,12 @@ header { color: white; } +/* IMPROVED: Better focus states for accessibility */ +.win7-control:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + header h1 { margin: 0; padding: 0; @@ -186,7 +229,7 @@ header h1 { color: var(--title-text); } -/* Site title with Aero glass effect */ +/* Site title with Aero glass effect - IMPROVED accessibility */ .site-title { margin: 0; padding: 0; @@ -200,37 +243,63 @@ header h1 { .site-title a { text-decoration: none; - color: var(--title-text); - background: linear-gradient(to bottom, rgba(255, 255, 255, 0.9) 0%, rgba(200, 230, 255, 0.9) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: var(--title-text); /* Fallback for text browsers */ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.7); transition: all var(--transition-medium); font-weight: 500; } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.9) 0%, rgba(200, 230, 255, 0.9) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } +} + .site-title a:hover { text-decoration: none; - background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(220, 240, 255, 1) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); } -/* Navigation bar with Aero glass effect */ +/* Progressive enhancement for hover gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a:hover { + background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(220, 240, 255, 1) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } +} + +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); +} + +/* Navigation bar with Aero glass effect - OPTIMIZED */ nav { background: rgba(240, 240, 240, 0.6); padding: var(--spacing-sm) var(--spacing-md); border-bottom: 1px solid var(--border-color); display: flex; flex-wrap: wrap; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(8px); /* Reduced from 10px for better performance */ + -webkit-backdrop-filter: blur(8px); justify-content: center; } +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(8px)) { + nav { + background: rgba(240, 240, 240, 0.9); /* More opaque fallback */ + } +} + nav a { color: var(--text-color); text-decoration: none; @@ -249,19 +318,24 @@ nav a:hover, nav a:focus { box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Selected menu item */ nav a.active { background: rgba(217, 233, 250, 0.7); box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); } -/* Content area with slight transparency */ +/* Content area with slight transparency - IMPROVED contrast */ main { padding: var(--spacing-2xl); background-color: var(--window-bg); } -/* Typography - Windows 7 specific */ +/* Typography - Windows 7 specific - IMPROVED contrast */ h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); color: var(--header-color); @@ -281,6 +355,7 @@ h2 { color: #1a73e8; } +.posts-list h2, h3 { font-size: var(--text-xl); } @@ -290,7 +365,7 @@ p { line-height: var(--line-height-normal); } -/* Links */ +/* Links - IMPROVED accessibility */ a { color: var(--link-color); text-decoration: none; @@ -305,7 +380,13 @@ a:hover { text-decoration: underline; } -/* Windows 7 style buttons */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + text-decoration: underline; +} + +/* Windows 7 style buttons - IMPROVED accessibility */ .win7-button { background: linear-gradient(to bottom, #f8f8f8 0%, #e1e1e1 100%); border: 1px solid #ababab; @@ -331,19 +412,34 @@ a:hover { border-color: #5c8ab9; } -/* Articles with glass effect */ +.win7-button:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-color: #7eb4ea; + box-shadow: 0 0 3px rgba(42,100,150,0.4); +} + +/* Articles with glass effect - OPTIMIZED */ article { margin-bottom: var(--spacing-2xl); padding: var(--spacing-xl); border: 1px solid rgba(202, 223, 243, 0.7); border-radius: var(--small-radius); background-color: rgba(255, 255, 255, 0.5); - backdrop-filter: blur(5px); - -webkit-backdrop-filter: blur(5px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(3px); /* Reduced from 5px for better performance */ + -webkit-backdrop-filter: blur(3px); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); transition: box-shadow var(--transition-medium), border-color var(--transition-medium); } +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(3px)) { + article { + background-color: rgba(255, 255, 255, 0.85); /* More opaque fallback */ + } +} + article:hover { box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); border-color: rgba(179, 207, 233, 0.9); @@ -385,6 +481,13 @@ article .meta { text-decoration: none; } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background: rgba(213, 233, 252, 0.9); + text-decoration: none; +} + code { font-family: var(--font-mono); background-color: rgba(240, 240, 240, 0.7); @@ -414,7 +517,7 @@ img { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } -/* Footer styled as Windows 7 taskbar */ +/* Footer styled as Windows 7 taskbar - IMPROVED accessibility */ footer { background: linear-gradient(to bottom, #2980d5 0%, #1e5faa 100%); color: white; @@ -429,7 +532,7 @@ footer { border-radius: 0 0 var(--border-radius) var(--border-radius); } -/* Footer links with better contrast */ +/* Footer links with better contrast - IMPROVED accessibility */ footer a { color: #ffffff; text-decoration: underline; @@ -441,7 +544,14 @@ footer a:hover { text-decoration: underline; } -/* Start button */ +footer a:focus { + outline: 2px solid #ffff99; + outline-offset: 2px; + color: #ffff99; + text-decoration: underline; +} + +/* Start button - IMPROVED accessibility */ .start-button { display: inline-flex; align-items: center; @@ -454,20 +564,34 @@ footer a:hover { position: relative; border: 1px solid #09407f; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + text-decoration: none; + transition: all var(--transition-fast); } +.start-button:hover { + background: linear-gradient(to bottom, #4ca5fa 0%, #2572bd 100%); +} + +.start-button:focus { + outline: 2px solid #ffff99; + outline-offset: 2px; + background: linear-gradient(to bottom, #4ca5fa 0%, #2572bd 100%); +} + +/* SIMPLIFIED: Text-based icon instead of base64 image */ .start-button::before { - content: ""; + content: "⊞"; display: inline-block; width: 20px; height: 20px; margin-right: var(--spacing-sm); - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAFASURBVHjarNS/S0JRFMfxz30qJAQRREsNDQ2BJtEQhEOglP+AW0M0tdRQS/9DS1NjQzREUzREQ7RI/gENQg3RUJRBBSHUe08DyfdenUoHzna/fM49517lnAvQAIZQxxVOcQ/hT/W147/UiyYeMIkMkv6Tj7vk3ciBDWGt4uGP2MEe2vgmz2ETD+r2nUPsF0QOy/jCC1KIIYs6djGJEZzZ+OiJAcewh0es+aBdJeAGy8i14gWcWCMd8E7FupvAkY1ZjIcYl8CRjfmA4AKm8I4nLAT8dlPqmFFJbIdYRp9f5UKIMz3TihfwHhtYxHuEmAp2TSneShhIPvkAZQfdMnDMGkvcRrRPeI+M+zgyNbxGGHdi5K99zpVNJA/rNoFVm2Ar3EGsYwOtgGfnbbJl23j/oW53aP5H3Tlr/1c1n/MPdZf+GgDtL0EIgQlcEQAAAABJRU5ErkJggg=="); - background-repeat: no-repeat; - background-position: center; + font-size: var(--text-lg); + text-align: center; + line-height: 1; + color: white; } -/* Win7 taskbar time */ +/* Win7 taskbar time - IMPROVED accessibility */ .taskbar-time { display: inline-block; color: white; @@ -476,7 +600,7 @@ footer a:hover { font-size: var(--text-xs); } -/* Pagination in Win7 style */ +/* Pagination in Win7 style - IMPROVED accessibility */ .pagination { display: flex; justify-content: center; @@ -503,21 +627,36 @@ footer a:hover { text-decoration: none; } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background: linear-gradient(to bottom, #f8f8f8 0%, #d8d8d8 100%); + border-color: #7eb4ea; + box-shadow: 0 0 3px rgba(42,100,150,0.4); + text-decoration: none; +} + .pagination .page-info { color: #666; font-size: var(--text-sm); } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { body { padding: var(--spacing-md); + /* OPTIMIZED: Simplified background on mobile */ + background-image: linear-gradient(135deg, #e3eefb 0%, #d1e7f0 100%); } .container { margin: var(--spacing-md) auto; width: 95%; max-width: none; + /* OPTIMIZED: Remove backdrop-filter on mobile for better performance */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(255, 255, 255, 0.98); } header { @@ -525,6 +664,10 @@ footer a:hover { align-items: center; text-align: center; padding: var(--spacing-xl); + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: linear-gradient(to bottom, rgba(209, 228, 246, 0.98) 0%, rgba(177, 211, 241, 0.98) 100%); } header h1 { @@ -547,6 +690,10 @@ footer a:hover { justify-content: center; padding: var(--spacing-md); gap: var(--spacing-md); + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(240, 240, 240, 0.95); } nav a { @@ -561,6 +708,13 @@ footer a:hover { background: linear-gradient(to bottom, #f8f8f8 0%, #e1e1e1 100%); } + article { + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background-color: rgba(255, 255, 255, 0.9); + } + footer { flex-direction: column; gap: var(--spacing-md); @@ -604,7 +758,7 @@ footer a:hover { } } -/* Featured Images with Windows 7 Aero styling */ +/* Featured Images with Windows 7 Aero styling - OPTIMIZED */ .featured-image { margin: var(--spacing-xl) 0; border: 1px solid var(--border-color); @@ -613,8 +767,16 @@ footer a:hover { position: relative; box-shadow: 0 2px 10px var(--shadow-color); background: rgba(255, 255, 255, 0.5); - backdrop-filter: blur(5px); - -webkit-backdrop-filter: blur(5px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(3px); /* Reduced from 5px for better performance */ + -webkit-backdrop-filter: blur(3px); +} + +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(3px)) { + .featured-image { + background: rgba(255, 255, 255, 0.85); + } } .featured-image img { @@ -625,7 +787,7 @@ footer a:hover { } .featured-image:hover img { - transform: scale(1.02); + transform: scale(1.01); /* Reduced scale for better performance */ } .featured-image .image-caption { @@ -638,11 +800,19 @@ footer a:hover { padding: var(--spacing-md) var(--spacing-xl); font-size: var(--text-sm); text-align: center; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(8px); /* Reduced from 10px for better performance */ + -webkit-backdrop-filter: blur(8px); border-top: 1px solid var(--border-color); } +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(8px)) { + .featured-image .image-caption { + background: linear-gradient(to bottom, rgba(209, 228, 246, 0.98) 0%, rgba(177, 211, 241, 0.98) 100%); + } +} + .index-image, .tag-image, .archive-image { margin-bottom: var(--spacing-xl); border: 1px solid var(--border-color); @@ -657,13 +827,13 @@ footer a:hover { display: block; } -/* Selection styling */ +/* IMPROVED: Better text selection */ ::selection { background-color: var(--highlight-color); color: var(--text-color); } -/* Date header with Win7 styling */ +/* Date header with Win7 styling - IMPROVED accessibility */ .date-header { font-family: var(--font-headings); font-weight: 300; diff --git a/themes/win95/style.css b/themes/win95/style.css index e9ddd79..01a705c 100644 --- a/themes/win95/style.css +++ b/themes/win95/style.css @@ -1,8 +1,20 @@ /* * Windows 95 Theme for BSSG * Authentic styling from the Windows 95 era + * Enhanced with accessibility, performance, and compatibility improvements */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Windows 95 color scheme */ --bg-color: #008080; /* Classic teal desktop */ @@ -21,10 +33,10 @@ --inactive-title-bar: #808080; --inactive-title-text: #c0c0c0; - /* Typography */ - --font-main: 'MS Sans Serif', 'Tahoma', 'Arial', sans-serif; - --font-headings: 'Times New Roman', 'MS Serif', serif; - --font-mono: 'Courier New', monospace; + /* Typography - system fonts for authentic Windows 95 look */ + --font-main: 'MS Sans Serif', 'Tahoma', 'Segoe UI', 'Arial', 'Helvetica', sans-serif; + --font-headings: 'Times New Roman', 'Times', 'Georgia', serif; + --font-mono: 'Courier New', 'Courier', 'Consolas', 'Liberation Mono', monospace; /* Font Sizes */ --text-xs: 0.85rem; /* 13.6px */ @@ -56,12 +68,7 @@ --content-width: 950px; /* Increased for better screen utilization */ } -@font-face { - font-family: 'MS Sans Serif'; - src: url('data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABBAAAwAAAAAKAgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABHAAAAhoAAAiKhJf9rk9TLzIAAAE2AAAAPQAAAFZLxVmCY21hcAAAAXUAAABrAAABsoNr0jhnbHlmAAAB5AAADbsAABecUO0kjmhlYWQAAA+gAAAAMAAAADYOZwAmaGhlYQAAD9AAAAAeAAAAJAdAA+FobXR4AAAP8AAAACsAAABMA+gAAGxvY2EAABA8AAAAKAAAACgkSie+bWF4cAAAEGQAAAAfAAAAIAE2ALJuYW1lAAAQhAAAAl0AAAQUSCNRznBvc3QAABLkAAAAYwAAAJTh+n7geJxNkDFOxDAQRX+ya8GSm1QNJSAKRJqIOn1OsCdYlQfYK6RMkQoqzgGiS5NDcAEOQEGBRMGf8Y8NBRnJHs/Mm69xZoPHGWznPZN/lm/u+Tbw+/r7c/v6NQ7cuBuVi6qqzm2n4+xyHKtzzd3pcjhcljw/1MFUBz1AzCTd93zQURq4CTySx0QXfG/Tr5h6d1RjlPfu9EwmYY301LJL3gdGkM5EEJkIPvp8JQw6FqESQo/oC+HQe+FjgjcGbw3O9g5mjsyDa9AP4ueyjU') format('woff'); - font-weight: normal; - font-style: normal; -} +/* Removed embedded font for better performance and text browser compatibility */ /* Base elements */ html { @@ -154,12 +161,20 @@ header { cursor: pointer; } +/* Text browser fallback for window controls */ .win95-close::after { - content: "×"; + content: "X"; font-size: 12px; font-weight: bold; } +/* Progressive enhancement for decorative symbol */ +@supports (content: "×") { + .win95-close::after { + content: "×"; + } +} + header h1 { margin: 0; padding: 0; @@ -200,6 +215,17 @@ nav a:hover { padding: calc(var(--spacing-xs) - 1px) calc(var(--spacing-md) - 1px); } +/* Accessibility: Focus outlines for all interactive elements */ +nav a:focus, +.site-title a:focus, +a:focus, +.tags a:focus, +.pagination a:focus, +.win95-control:focus { + outline: 2px solid var(--title-bar); + outline-offset: 2px; +} + nav a.active { background-color: var(--button-face); border: 1px solid var(--button-shadow); @@ -300,9 +326,9 @@ a:hover { content: "Archive Viewer"; } -/* Close button for featured images */ +/* Text browser fallback for image viewer close buttons */ .featured-image::after, .index-image::after, .tag-image::after, .archive-image::after { - content: "×"; + content: "X"; position: absolute; top: -19px; right: 2px; @@ -321,6 +347,13 @@ a:hover { box-sizing: border-box; } +/* Progressive enhancement for decorative symbol */ +@supports (content: "×") { + .featured-image::after, .index-image::after, .tag-image::after, .archive-image::after { + content: "×"; + } +} + .featured-image img, .index-image img, .tag-image img, .archive-image img { display: block; width: 100%; @@ -366,7 +399,7 @@ a:hover { border-bottom: 1px solid var(--border-color); } -.posts-list h3 { +.posts-list h2 { margin-top: var(--spacing-lg); margin-bottom: var(--spacing-sm); } diff --git a/themes/winxp/style.css b/themes/winxp/style.css index dea7985..d642ef1 100644 --- a/themes/winxp/style.css +++ b/themes/winxp/style.css @@ -1,702 +1,794 @@ /* - * Windows XP Theme for BSSG - * Nostalgic styling from the Windows XP era with the iconic blue/green hills and rounded elements + * Windows XP Theme for BSSG - Completely Redesigned + * Authentic Windows XP Luna theme with modern accessibility and performance + * Inspired by the classic Windows XP interface (2001-2014) + * + * Pure CSS responsive design - no JavaScript required + * Mobile navigation uses horizontal scrolling for better UX */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable background-attachment: fixed for reduced motion */ + body { + background-attachment: scroll !important; + } +} + :root { - /* Windows XP color scheme */ - --bg-color: #f0f0f0; - --window-bg: #ffffff; - --text-color: #000000; - --link-color: #0066cc; - --link-visited: #800080; - --header-color: #003399; - --border-color: #7da2ce; - --button-highlight: #ffffff; - --button-shadow: #326ac0; - --button-face: #e9eff9; - --title-bar: #2160c2; - --title-text: #ffffff; - --menu-bar: #f1f5fa; - --taskbar-color: #245edb; - --start-button: #74b933; - --start-text: #ffffff; - --highlight-color: #d2e8ff; - --accent-color: #2a8dd4; - --accent-secondary: #015eae; - --quote-bg: #f1f6fb; + /* Authentic Windows XP Luna color palette */ + --xp-blue-light: #4a90e2; + --xp-blue-medium: #316ac5; + --xp-blue-dark: #1f4788; + --xp-blue-darker: #0f2c5c; - /* Typography */ - --font-main: 'Tahoma', 'Arial', sans-serif; - --font-headings: 'Franklin Gothic Medium', 'Arial', sans-serif; - --font-mono: 'Lucida Console', monospace; + --xp-green-light: #7dd87d; + --xp-green-medium: #5cb85c; + --xp-green-dark: #3d8b3d; - /* Font Sizes */ - --text-xs: 0.8rem; - --text-sm: 0.9rem; - --text-base: 1rem; - --text-md: 1.1rem; - --text-lg: 1.2rem; - --text-xl: 1.4rem; - --text-2xl: 1.8rem; + --xp-gray-lightest: #f1f1f1; + --xp-gray-light: #ece9d8; + --xp-gray-medium: #d4d0c8; + --xp-gray-dark: #aca899; + --xp-gray-darker: #716f64; - /* Line Heights */ - --line-height-tight: 1.2; - --line-height-base: 1.5; - --line-height-relaxed: 1.8; + --xp-window-bg: #ffffff; + --xp-text-primary: #000000; + --xp-text-secondary: #666666; + --xp-link-color: #0066cc; + --xp-link-visited: #800080; - /* Spacing */ - --spacing-xs: 3px; - --spacing-sm: 5px; - --spacing-md: 10px; - --spacing-lg: 15px; - --spacing-xl: 20px; - --spacing-2xl: 30px; + /* Typography - authentic Windows XP fonts with better sizing */ + --font-main: 'Tahoma', 'MS Sans Serif', 'Segoe UI', Arial, sans-serif; + --font-headings: 'Tahoma', 'MS Sans Serif', 'Segoe UI', Arial, sans-serif; + --font-mono: 'Courier New', 'Lucida Console', 'Consolas', monospace; + + /* Spacing system */ + --space-1: 2px; + --space-2: 4px; + --space-3: 8px; + --space-4: 12px; + --space-5: 16px; + --space-6: 20px; + --space-7: 24px; + --space-8: 32px; + + /* Font sizes - significantly increased for better readability */ + --text-xs: 12px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 16px; + --text-lg: 18px; + --text-xl: 22px; + --text-2xl: 28px; + --text-3xl: 32px; + + /* Layout */ + --content-width: 1000px; + --window-border-width: 3px; + --titlebar-height: 32px; + --menubar-height: 28px; + --taskbar-height: 32px; /* Transitions */ - --transition-fast: 0.1s; - --transition-base: 0.2s; - --transition-slow: 0.3s; - - /* Sizing */ - --content-width: 1000px; - --border-radius: 8px; + --transition-fast: 0.1s ease; + --transition-medium: 0.2s ease; } -/* XP Bliss background for body */ -body { +/* Base styles */ +* { + box-sizing: border-box; +} + +html { + font-size: 14px; font-family: var(--font-main); - background-color: var(--bg-color); - color: var(--text-color); - margin: 0; - padding: var(--spacing-md); - line-height: var(--line-height-base); - font-size: var(--text-base); - background-image: url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wgARCABAAEADASIAAhEBAxEB/8QAGgAAAgMBAQAAAAAAAAAAAAAAAAQDBQYCAf/EABcBAQEBAQAAAAAAAAAAAAAAAAABAgP/2gAMAwEAAhADEAAAAdUAAACjlbxVMmVXI9OnpQ6KwAHTr9HBM+4Lmtg5PGq+rrnNsWt70+ZOe/XLGHwDHqwMcNxGfzpH5YLjN3S9T0eN32GW/OAAA//EACAQAAICAgICAwAAAAAAAAAAAAECAwQAERIxEyEUQVH/2gAIAQEAAQUCFZidDH+OKQeaqBR9YsyrG5ZoklyWnH7VsfDrHpUdTbqeMEyMvPNVrFcfEU7SaGxZWHe9YzzFeY2JvlZyC5Yqm2H1kESjy2pfHPQCqvvKcDUBt6qRt75dYieMbjG8P//EAB0RAAEEAwEBAAAAAAAAAAAAAAEAAgMRIRIQE0H/2gAIAQMBAT8BDRIxoJUctHIUbsHIppdndF/K55Qo1pf/xAAbEQACAgMBAAAAAAAAAAAAAAAAEQECEBIhA//aAAgBAgEBPwGztQ2xdxHSnTR2tM5mf//EACUQAAIBAwMEAgMAAAAAAAAAAAECAAMRITESQVEQE2FxIoGhsf/aAAgBAQAGPwLwXOo+BPcqJpBuMSwn2YW/UutPF+JdQcZJhdjmawqDIyJUDcN9GWVkX9idrV1cE7QoE3HI4luoqA5wZ31XvF0AYI4l1ZkPkb/qUz5zKA6mkvnE31d6pGqm2P3NTBXHTUubdQ7N8WGpmk20nhBP/8QAIxAAAgICAQQCAwAAAAAAAAAAAAERITFBYVFxgZGhscHw8f/aAAgBAwEBPyHpbKqItODVXXBHeiNGEcTCOQjVMaS+yFCZfYWpI1fYatZF0eBQkXCdVOxPoNtRjImWvHJ3fsOaNXqaI6qzLRXI1TXh3InGlkvkbbaMJxuMj4P/xAAiEAEAAgICAgIDAQAAAAAAAAABABEhMUFRYXGBoZGx8MH/2gAIAQEAAT8hvGEjoxYHGGXC45ajUZYlgQsAZG0QBrZrL8xztSsfLH9n1GqrCfcRi4gjEI8SmbG5TvQzDMdS0Lue4RcFm4BsOpU4rJuKMULuXqmY47Zcc8kGPMy5hTFt3+YNKs2uJhYVXMrP2IimTuGnxHqOpczs2+2OXKDMR9gRwJf9lRgrv7JVp2OBPmbjOyAQdyjJiDR9n7itpz8y9hXAEdS4rRVcOXcOX1uPL5j0JY8RGXTLxE/SL6j6uB5jcHg6l/mVnmOJ/9oADAMBAAIAAwAAABDzzzzX3jzqbkXVeHbzyzzz/8QAHREBAQEAAgMBAQAAAAAAAAAAAQARITEQQVFhcf/aAAgBAwEBPxDdPBWBd7+sMEm7/XO1xwH1FLsLuIeZYbCe/JeTxl7AHxfNvGXhL5ZDrLCIwTzYG/H/xAAaEQEBAQEBAQEAAAAAAAAAAAABABEhMRBB/9oACAECAQE/EPZhvyOsdYTeSZzzL3YEPWVhlzO+THLnJxwbLAGSIBPT7nbcD9xF80f/xAAkEAEAAgICAQQDAQEAAAAAAAABABEhMUFRYXGBkaGxwfDR4f/aAAgBAQABPxBUAVEHiaQs5iYnqXZLWoF0y2AKWUzWpkJ2DuJUxZaFg3qXdPUtLS8jNlIeYlxYKBgUgLTDVxAYLlEUNSh1GrlFMRBx3N1nUqFVKuYivpMxbNTGcku0XF4jhg1uBfLAXvcrS6dQYxRvEpnR3MFoU9IvB5I4+IpRpcENpv5jYy5YhsLCX1Kv24ghBrzGLbRK2FnxNDFM2jcr2E4liuUzD3NkbcupbjZ4nNVvLEyHdXjMplluWDzMhjcADc6Qw9yxl8QuqY+Tctlkx3DMvDdxszJAKwbQD7nTLH2xVKRgPsRZVzd8Ry2LBHmO45YjMlV9sSrYuNfMF/Mf/9k="); - background-repeat: no-repeat; - background-attachment: fixed; - background-size: cover; } -/* Windows XP rounded window style elements */ +body { + margin: 0; + padding: var(--space-4); + font-family: var(--font-main); + font-size: var(--text-base); + color: var(--xp-text-primary); + line-height: 1.5; + /* Authentic Windows XP desktop background */ + background: linear-gradient(135deg, #4a90e2 0%, #7dd87d 50%, #4a90e2 100%); + background-attachment: fixed; + min-height: 100vh; +} + +/* Main window container */ .container { max-width: var(--content-width); - margin: var(--spacing-xl) auto; - background-color: var(--window-bg); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); - overflow: hidden; + margin: var(--space-6) auto; + background: var(--xp-window-bg); + border: var(--window-border-width) solid; + border-color: var(--xp-blue-medium) var(--xp-blue-dark) var(--xp-blue-dark) var(--xp-blue-medium); + box-shadow: + 0 0 0 1px var(--xp-blue-light), + 4px 4px 12px rgba(0, 0, 0, 0.3); + position: relative; + overflow: visible; /* Allow mobile menu to overflow */ } -/* Title bar like Windows XP window with blue gradient and rounded top corners */ +/* Window title bar */ header { - background: linear-gradient(to bottom, #2a8dd4 0%, #015eae 100%); - color: var(--title-text); - padding: var(--spacing-sm) var(--spacing-md); - margin: 0; - position: relative; - font-weight: bold; - font-size: var(--text-base); + height: var(--titlebar-height); + background: linear-gradient(to bottom, + var(--xp-blue-light) 0%, + var(--xp-blue-medium) 50%, + var(--xp-blue-dark) 100%); + color: white; display: flex; align-items: center; justify-content: space-between; - border-radius: var(--border-radius) var(--border-radius) 0 0; + padding: 0 var(--space-3); + font-size: var(--text-md); + font-weight: bold; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + border-bottom: 1px solid var(--xp-blue-darker); } -/* Window control icons */ +/* Site title in title bar */ +.site-title { + margin: 0; + font-size: var(--text-md); + font-weight: bold; + flex-grow: 1; +} + + + +.site-title a { + color: white; + text-decoration: none; + transition: opacity var(--transition-fast); +} + +.site-title a:hover { + opacity: 0.9; + text-decoration: none; +} + +.site-title a:focus { + outline: 2px solid white; + outline-offset: 2px; + opacity: 0.9; + text-decoration: none; +} + +/* Window controls */ .window-controls { display: flex; - gap: var(--spacing-xs); + gap: var(--space-1); } -.win-control { - width: 20px; - height: 18px; - background: linear-gradient(to bottom, #f1f6fb 0%, #c5d7eb 100%); - border: 1px solid #2559a5; - border-radius: 3px; +.window-control { + width: 21px; + height: 21px; + background: linear-gradient(to bottom, #f0f0f0 0%, #d0d0d0 100%); + border: 1px solid #808080; display: flex; align-items: center; justify-content: center; font-size: var(--text-xs); + font-weight: bold; cursor: pointer; transition: all var(--transition-fast); } -.win-minimize::after { - content: "_"; - position: relative; - top: -4px; - color: #000; +.window-control:hover { + background: linear-gradient(to bottom, #f8f8f8 0%, #e0e0e0 100%); } -.win-maximize::after { - content: "□"; - position: relative; - color: #000; - font-size: 10px; +.window-control:active { + background: linear-gradient(to bottom, #d0d0d0 0%, #f0f0f0 100%); } -.win-close { - background: linear-gradient(to bottom, #f3a0a0 0%, #e46363 100%); +.window-control:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 1px; } -.win-close::after { - content: "×"; - color: #000; - font-weight: bold; +/* Text browser fallbacks for window controls */ +.minimize::after { content: "_"; } +.maximize::after { content: "□"; } +.close::after { content: "×"; color: #800000; } + +/* Progressive enhancement for better symbols */ +@supports (content: "🗕") { + .minimize::after { content: "🗕"; } +} +@supports (content: "🗖") { + .maximize::after { content: "🗖"; } +} +@supports (content: "🗙") { + .close::after { content: "🗙"; } } -header h1 { - margin: 0; - padding: 5px; - font-size: 1.2rem; - font-weight: bold; - font-family: var(--font-main); - color: var(--title-text); - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); -} - -/* Site title with Windows XP style */ -.site-title { - margin: 0; - padding: 0; - font-size: 1.2rem; - font-weight: bold; - font-family: var(--font-main); - flex-grow: 1; -} - -.site-title a { - color: var(--title-text); /* Fallback */ - text-decoration: none; - background: linear-gradient(to right, #ffffff 0%, #f0f0f0 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 2px rgba(0, 0, 0, 1); /* Increased shadow opacity */ - display: inline-block; - transition: all 0.2s; - font-weight: bold; - /* Extra depth with subtle outline */ - -webkit-text-stroke: 0.5px rgba(0, 0, 0, 0.3); -} - -.site-title a:hover { - background: linear-gradient(to right, #ffffff 0%, #ffffff 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - transform: translateY(-1px); - text-shadow: 0 1px 3px rgba(0, 0, 0, 1), 0 0 5px rgba(255, 255, 255, 0.8); - -webkit-text-stroke: 0.5px rgba(0, 0, 0, 0.5); -} - -/* Navigation bar in XP style */ +/* Menu bar */ nav { - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - padding: var(--spacing-sm) 0; - border-bottom: 1px solid var(--border-color); + height: var(--menubar-height); + background: linear-gradient(to bottom, var(--xp-gray-lightest) 0%, var(--xp-gray-light) 100%); + border-bottom: 1px solid var(--xp-gray-dark); display: flex; - flex-wrap: wrap; - justify-content: center; + align-items: center; + padding: 0 var(--space-3); + font-size: var(--text-base); + overflow-x: auto; + overflow-y: hidden; } nav a { - color: var(--text-color); + color: var(--xp-text-primary); text-decoration: none; - padding: var(--spacing-sm) var(--spacing-md); - margin: 0 var(--spacing-xs); - border-radius: 3px; - font-size: var(--text-sm); - position: relative; - display: inline-block; - white-space: nowrap; + padding: var(--space-3) var(--space-4); + margin: 0 1px; + border-radius: 2px; transition: all var(--transition-fast); + white-space: nowrap; + font-size: var(--text-base); + min-height: 24px; + display: flex; + align-items: center; } -nav a:hover, nav a:focus { - background: linear-gradient(to bottom, #f5f9fd 0%, #cee2fc 100%); - border: 1px solid var(--border-color); - padding: 3px 9px; +nav a:hover { + background: linear-gradient(to bottom, #e3f2fd 0%, #bbdefb 100%); + border: 1px solid #90caf9; + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); +} + +nav a:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; + background: linear-gradient(to bottom, #e3f2fd 0%, #bbdefb 100%); + border: 1px solid #90caf9; + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); } -/* Selected menu item */ nav a.active { - background: linear-gradient(to bottom, #e3effd 0%, #afd2fa 100%); - border: 1px solid var(--border-color); - padding: 3px 9px; + background: linear-gradient(to bottom, #d1e7dd 0%, #a3cfbb 100%); + border: 1px solid #75b798; + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); } -/* Content area */ +/* Main content area */ main { - padding: var(--spacing-lg); - background-color: var(--window-bg); + padding: var(--space-5); + background: var(--xp-window-bg); + min-height: 400px; } /* Typography */ h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); - color: var(--header-color); - margin-top: 1.5rem; - margin-bottom: 0.75rem; - line-height: var(--line-height-tight); + color: var(--xp-text-primary); + margin: var(--space-6) 0 var(--space-4) 0; + line-height: 1.2; + font-weight: bold; } -h1 { - font-size: var(--text-2xl); - color: var(--title-bar); -} +h1 { font-size: var(--text-2xl); } +h2 { font-size: var(--text-xl); } +.posts-list h2, +h3 { font-size: var(--text-lg); } +h4 { font-size: var(--text-md); } -h2 { - font-size: var(--text-xl); - color: var(--title-bar); -} - -h3 { - font-size: var(--text-lg); +article h1 { + margin-top: 0; + color: var(--xp-blue-dark); } p { - margin-bottom: 1rem; - line-height: var(--line-height-base); + margin: 0 0 var(--space-4) 0; + line-height: 1.5; } /* Links */ a { - color: var(--link-color); - text-decoration: none; - transition: color var(--transition-base); + color: var(--xp-link-color); + text-decoration: underline; + transition: color var(--transition-fast); } a:visited { - color: var(--link-visited); + color: var(--xp-link-visited); } a:hover { - text-decoration: underline; + color: var(--xp-blue-dark); + text-decoration: none; } -/* XP Style buttons */ -.winxp-button { - background: linear-gradient(to bottom, #f1f6fb 0%, #c5d7eb 100%); - border: 1px solid var(--border-color); - border-radius: 3px; - padding: var(--spacing-sm) var(--spacing-lg); - color: var(--text-color); +a:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; + color: var(--xp-blue-dark); + text-decoration: none; +} + +/* XP-style buttons */ +.xp-button { + background: linear-gradient(to bottom, var(--xp-gray-lightest) 0%, var(--xp-gray-light) 100%); + border: 1px solid var(--xp-gray-dark); + padding: var(--space-2) var(--space-4); font-family: var(--font-main); font-size: var(--text-sm); - font-weight: normal; + color: var(--xp-text-primary); cursor: pointer; - margin: var(--spacing-xs); transition: all var(--transition-fast); + text-decoration: none; + display: inline-block; } -.winxp-button:active { - background: linear-gradient(to bottom, #cde1f9 0%, #9bc1f1 100%); +.xp-button:hover { + background: linear-gradient(to bottom, #f8f8f8 0%, var(--xp-gray-lightest) 100%); + border-color: var(--xp-blue-medium); } -/* Articles in XP window style */ +.xp-button:active { + background: linear-gradient(to bottom, var(--xp-gray-light) 0%, var(--xp-gray-lightest) 100%); +} + +.xp-button:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; +} + +/* Articles */ article { - margin-bottom: var(--spacing-xl); - padding: var(--spacing-md); - border: 1px solid #d2e1f9; - border-radius: 5px; - background-color: #f9fafc; + margin-bottom: var(--space-8); + padding-bottom: var(--space-6); + border-bottom: 1px solid var(--xp-gray-medium); +} + +article:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; } article .meta { - font-size: var(--text-sm); - color: #666; - margin-bottom: var(--spacing-md); + color: var(--xp-text-secondary); + font-size: var(--text-base); + margin: var(--space-3) 0 var(--space-4) 0; display: flex; flex-wrap: wrap; - gap: var(--spacing-md); + gap: var(--space-4); } .reading-time { - color: #666; font-style: italic; - font-size: var(--text-sm); } +/* Text browser fallback for reading time icon */ +.reading-time::before { + content: "Time: "; +} + +/* Progressive enhancement for reading time icon */ +@supports (content: "⏱") { + .reading-time::before { + content: "⏱ "; + } +} + +/* Tags */ .tags { display: flex; flex-wrap: wrap; - gap: var(--spacing-sm); + gap: var(--space-2); + margin: var(--space-4) 0; } .tags a { - background: linear-gradient(to bottom, #f5f9fd 0%, #cee2fc 100%); - color: var(--text-color); - padding: var(--spacing-xs) var(--spacing-md); - font-size: var(--text-xs); - border-radius: 8px; + background: linear-gradient(to bottom, #e3f2fd 0%, #bbdefb 100%); + color: var(--xp-text-primary); + padding: var(--space-2) var(--space-4); + font-size: var(--text-sm); text-decoration: none; - border: 1px solid #c5d7eb; - transition: all var(--transition-base); + border: 1px solid #90caf9; + border-radius: 2px; + transition: all var(--transition-fast); } .tags a:hover { - background: linear-gradient(to bottom, #e3effd 0%, #afd2fa 100%); + background: linear-gradient(to bottom, #bbdefb 0%, #90caf9 100%); + text-decoration: none; } +.tags a:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; + background: linear-gradient(to bottom, #bbdefb 0%, #90caf9 100%); + text-decoration: none; +} + +/* Code styling */ code { font-family: var(--font-mono); - background-color: #f0f0f0; - padding: var(--spacing-xs) var(--spacing-sm); - border-radius: 3px; - font-size: var(--text-sm); + background: var(--xp-gray-lightest); + padding: var(--space-2) var(--space-3); + border: 1px solid var(--xp-gray-medium); + font-size: var(--text-base); } pre { - background-color: #f5f5f5; - padding: var(--spacing-md); - border-radius: 5px; + background: var(--xp-gray-lightest); + border: 1px solid var(--xp-gray-medium); + padding: var(--space-4); overflow-x: auto; - border: 1px solid #ddd; - font-size: var(--text-sm); + margin: var(--space-4) 0; + font-size: var(--text-base); } pre code { - background-color: transparent; + background: none; + border: none; padding: 0; } +/* Images */ img { max-width: 100%; height: auto; - border-radius: 3px; - border: 1px solid #d2e1f9; + border: 1px solid var(--xp-gray-medium); + margin: var(--space-4) 0; } -/* Footer styled as XP taskbar */ -footer { - background: linear-gradient(to bottom, #3c82d0 0%, #245edb 100%); - color: white; - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--text-sm); - text-align: center; - display: flex; - justify-content: space-between; - align-items: center; - border-top: 1px solid #1752cf; - border-radius: 0 0 var(--border-radius) var(--border-radius); +/* Blockquotes */ +blockquote { + margin: var(--space-4) 0; + padding: var(--space-4); + background: #f8f9fa; + border-left: 4px solid var(--xp-blue-medium); + border: 1px solid var(--xp-gray-medium); } -/* Footer links with better contrast */ -footer a { - color: #ffffff; - text-decoration: underline; +blockquote p:last-child { + margin-bottom: 0; +} + +/* Tables */ +table { + width: 100%; + border-collapse: collapse; + margin: var(--space-4) 0; + border: 1px solid var(--xp-gray-medium); +} + +th, td { + padding: var(--space-3); + text-align: left; + border: 1px solid var(--xp-gray-medium); +} + +th { + background: linear-gradient(to bottom, var(--xp-gray-lightest) 0%, var(--xp-gray-light) 100%); font-weight: bold; } +tbody tr:nth-child(even) { + background: #f8f9fa; +} + +tbody tr:hover { + background: #e3f2fd; +} + +/* Footer as taskbar */ +footer { + min-height: var(--taskbar-height); + background: linear-gradient(to bottom, + var(--xp-blue-medium) 0%, + var(--xp-blue-dark) 50%, + var(--xp-blue-darker) 100%); + color: white; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: var(--space-4); + padding: var(--space-3); + font-size: var(--text-sm); + border-top: 1px solid var(--xp-blue-light); + text-align: center; +} + +footer a { + color: white; + text-decoration: underline; +} + footer a:hover { color: #ffff99; - text-decoration: underline; + text-decoration: none; +} + +footer a:focus { + outline: 2px solid white; + outline-offset: 2px; + color: #ffff99; + text-decoration: none; } /* Start button */ .start-button { - display: inline-flex; - align-items: center; - background: linear-gradient(to bottom, #8cd343 0%, #4fad0f 100%); + background: linear-gradient(to bottom, var(--xp-green-light) 0%, var(--xp-green-medium) 100%); color: white; font-weight: bold; - padding: var(--spacing-sm) var(--spacing-md) var(--spacing-sm) var(--spacing-sm); - border-radius: 10px; - font-size: var(--text-sm); - position: relative; - border: 1px solid #306110; + padding: var(--space-3) var(--space-5); + border: 1px solid var(--xp-green-dark); + border-radius: 4px; + font-size: var(--text-base); + text-decoration: none; transition: all var(--transition-fast); + white-space: nowrap; + flex-shrink: 0; } -.start-button::before { - content: ""; - display: inline-block; - width: 20px; - height: 18px; - margin-right: 5px; - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAFMSURBVHjatNQ/S5thFAbw3zs1KhLTgINDRxcXB6GEDg7SoUvpIhH8CIKfICLoUGinLn4CB+ks+AE6FOzk5NCpDiJIQTQmgZpzOiTvGyPPm7w5cLiHe+5/59xzuUIIIYTVCPsYxRCG0I9zrGEe2yW8iNdoRTh/tPCYGruQc1hAExsYxigmcIuDWD8vTxQZNrGHPQxgHD3oxkHUnMX8UdawXWvQjXGZYB6HcXA2Ts05M/vLPR5KxJsYQCWLtAst7OAMT/ADffiOKm7i+5eMuBb36x+P7xVvGMMQnmMAX9GHe3SgC68wlV9HHr/PjLbwExPxqT5iAbP4jpuYqC43LNd6nD3BKeqYjvH+YBfX2Iqa07xhPWp3MYkp1DAdP/+iFE+bNjj7iR+J8RXM4DWe5gtfYixzWMQVvmEZJ1HzCa/ShjmOIqzHxvhXTfg7AESQI1XUvK6jAAAAAElFTkSuQmCC"); - background-repeat: no-repeat; - background-position: center; +.start-button:hover { + background: linear-gradient(to bottom, var(--xp-green-medium) 0%, var(--xp-green-dark) 100%); + text-decoration: none; } -/* XP taskbar time */ -.taskbar-time { - display: inline-block; - background: #0c327a; - color: white; - padding: var(--spacing-sm) var(--spacing-md); - border-radius: 3px; - border-top: 1px solid #001a69; - border-left: 1px solid #001a69; - border-right: 1px solid #5986d6; - border-bottom: 1px solid #5986d6; - font-size: var(--text-sm); +.start-button:focus { + outline: 2px solid white; + outline-offset: 2px; + text-decoration: none; } -/* Pagination in XP style */ +/* Pagination */ .pagination { display: flex; justify-content: center; align-items: center; - margin: var(--spacing-xl) 0; - gap: var(--spacing-md); + gap: var(--space-3); + margin: var(--space-6) 0; + padding: var(--space-4); + background: var(--xp-gray-lightest); + border: 1px solid var(--xp-gray-medium); } .pagination a { - background: linear-gradient(to bottom, #f1f6fb 0%, #c5d7eb 100%); - color: var(--text-color); - padding: var(--spacing-sm) var(--spacing-md); + background: linear-gradient(to bottom, var(--xp-gray-lightest) 0%, var(--xp-gray-light) 100%); + color: var(--xp-text-primary); + padding: var(--space-2) var(--space-4); text-decoration: none; - border-radius: 3px; - border: 1px solid var(--border-color); + border: 1px solid var(--xp-gray-dark); transition: all var(--transition-fast); } .pagination a:hover { - background: linear-gradient(to bottom, #e3effd 0%, #afd2fa 100%); + background: linear-gradient(to bottom, #f8f8f8 0%, var(--xp-gray-lightest) 100%); + border-color: var(--xp-blue-medium); + text-decoration: none; +} + +.pagination a:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; + background: linear-gradient(to bottom, #f8f8f8 0%, var(--xp-gray-lightest) 100%); + border-color: var(--xp-blue-medium); text-decoration: none; } .pagination .page-info { - color: #666; - font-size: var(--text-sm); - padding: 0 var(--spacing-md); + color: var(--xp-text-secondary); + font-size: var(--text-base); } -/* Media query for responsive design */ +/* Featured images with XP window styling */ +.featured-image, .index-image, .tag-image, .archive-image { + margin: var(--space-6) 0; + border: 2px solid var(--xp-gray-medium); + background: var(--xp-gray-lightest); + padding: var(--space-2); +} + +.featured-image img, .index-image img, .tag-image img, .archive-image img { + width: 100%; + height: auto; + border: 1px solid var(--xp-gray-dark); + margin: 0; +} + +/* Selection styling */ +::selection { + background: var(--xp-blue-light); + color: white; +} + +/* Responsive design */ @media (max-width: 768px) { - .container { - margin: var(--spacing-md); - width: auto; - } - - header { - display: flex; - flex-direction: column; - align-items: center; - padding: var(--spacing-md) var(--spacing-md) var(--spacing-sm) var(--spacing-md); - } - - .site-title { - margin-bottom: var(--spacing-sm); - text-align: center; - } - - nav { - padding: var(--spacing-sm); - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - gap: var(--spacing-xs); - width: 100%; - background: linear-gradient(to bottom, #e9eff9 0%, #d2e2f9 100%); - border-radius: var(--border-radius); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); - } - - nav a { - margin: 2px; - padding: var(--spacing-xs) var(--spacing-sm); - text-align: center; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(to bottom, #f5f9fd 0%, #e9eff9 100%); - border: 1px solid #b9d1f7; - border-radius: 5px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; - height: 35px; - box-sizing: border-box; - overflow: hidden; - white-space: nowrap; - } - - nav a:hover, nav a:focus, nav a.active { - padding: var(--spacing-xs) var(--spacing-sm); - background: linear-gradient(to bottom, #e3effd 0%, #afd2fa 100%); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); - transform: translateY(1px); - } - - main { - padding: var(--spacing-md); - } - - footer { - flex-wrap: wrap; - justify-content: center; - gap: var(--spacing-md); - padding: var(--spacing-md); - } - - .start-button { - margin-bottom: var(--spacing-sm); - } -} - -@media (max-width: 480px) { - :root { - --content-width: 100%; + html { + font-size: 15px; /* Larger base font for tablets */ } body { - padding: var(--spacing-xs); + padding: var(--space-2); } - + .container { - margin: var(--spacing-xs); - box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); + margin: var(--space-3) 0; } header { - padding: var(--spacing-sm); - } - - .site-title { - font-size: 1rem; - } - - nav { - display: flex; - flex-direction: column; - padding: var(--spacing-xs); - gap: var(--spacing-xs); - width: 100%; - } - - nav a { - margin: 1px 0; - width: 100%; - padding: var(--spacing-xs) var(--spacing-sm); - height: 32px; - font-size: 0.85rem; - box-sizing: border-box; - } - - nav a:hover, nav a:focus, nav a.active { - width: 100%; - padding: var(--spacing-xs) var(--spacing-sm); + padding: 0 var(--space-2); + height: auto; + min-height: var(--titlebar-height); + flex-wrap: wrap; } .window-controls { display: none; } - h1 { - font-size: var(--text-xl); + /* Navigation becomes horizontally scrollable on tablets */ + nav { + padding: var(--space-2) var(--space-3); + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + scrollbar-color: var(--xp-gray-dark) var(--xp-gray-light); } - h2 { - font-size: var(--text-lg); + nav::-webkit-scrollbar { + height: 6px; + } + + nav::-webkit-scrollbar-track { + background: var(--xp-gray-light); + } + + nav::-webkit-scrollbar-thumb { + background: var(--xp-gray-dark); + border-radius: 3px; + } + + nav a { + padding: var(--space-3) var(--space-4); + font-size: var(--text-md); + min-width: max-content; + } + + nav a:hover, nav a:focus { + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); + } + + nav a.active { + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); + } + + main { + padding: var(--space-5); + } + + footer { + padding: var(--space-4); + gap: var(--space-3); } } -/* Featured Images with Windows XP styling */ -.featured-image { - margin: var(--spacing-xl) 0; - border: 1px solid var(--border-color); - padding: var(--spacing-sm); - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - border-radius: var(--spacing-sm); - position: relative; - box-shadow: 0 var(--spacing-xs) var(--spacing-md) rgba(0, 0, 0, 0.1); +@media (max-width: 480px) { + html { + font-size: 16px; /* Even larger base font for phones */ + } + + :root { + /* Increase spacing for mobile */ + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + } + + header { + padding: 0 var(--space-3); + } + + .site-title { + font-size: var(--text-lg); + } + + /* Navigation stacks vertically on phones for better accessibility */ + nav { + flex-direction: column; + height: auto; + padding: var(--space-3); + overflow: visible; + } + + nav a { + padding: var(--space-4) var(--space-4); + font-size: var(--text-lg); + margin: 1px 0; + text-align: center; + min-width: auto; + width: 100%; + } + + nav a:hover, nav a:focus { + padding: calc(var(--space-4) - 1px) calc(var(--space-4) - 1px); + } + + nav a.active { + padding: calc(var(--space-4) - 1px) calc(var(--space-4) - 1px); + } + + main { + padding: var(--space-4); + } + + h1 { font-size: var(--text-3xl); } + h2 { font-size: var(--text-2xl); } + .posts-list h2, + h3 { font-size: var(--text-xl); } + h4 { font-size: var(--text-lg); } + + footer { + padding: var(--space-4); + gap: var(--space-2); + font-size: var(--text-base); + } + + .start-button { + padding: var(--space-4) var(--space-5); + font-size: var(--text-md); + } + + .pagination { + flex-direction: column; + gap: var(--space-3); + } + + .pagination a { + padding: var(--space-4) var(--space-5); + font-size: var(--text-md); + } } -.featured-image img { - width: 100%; - height: auto; - display: block; - border: 1px solid #d8d8d8; - border-radius: var(--spacing-xs); - transition: all var(--transition-base); -} - -.featured-image:hover img { - opacity: 0.95; -} - -.featured-image .image-caption { - background: linear-gradient(to bottom, #2a8dd4 0%, #015eae 100%); - color: white; - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--text-sm); - text-align: center; - margin-top: var(--spacing-sm); - border-radius: var(--spacing-xs); - border: 1px solid var(--border-color); - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); -} - -.index-image { - margin-bottom: 1.2rem; - border: 1px solid var(--border-color); - padding: 3px; - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - border-radius: 3px; -} - -.index-image img { - width: 100%; - height: auto; - display: block; - border: 1px solid #d8d8d8; - border-radius: 2px; -} - -.tag-image { - margin-bottom: 1.2rem; - border: 1px solid var(--border-color); - padding: 3px; - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - border-radius: 3px; -} - -.tag-image img { - width: 100%; - height: auto; - display: block; - border: 1px solid #d8d8d8; - border-radius: 2px; -} - -.archive-image { - margin-bottom: 1.2rem; - border: 1px solid var(--border-color); - padding: 3px; - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - border-radius: 3px; -} - -.archive-image img { - width: 100%; - height: auto; - display: block; - border: 1px solid #d8d8d8; - border-radius: 2px; -} - -/* Selection styling */ -::selection { - background-color: var(--highlight-color); - color: var(--text-color); -} - -/* Date header with Windows XP styling */ -.date-header { - font-family: var(--font-headings); - font-weight: bold; - color: var(--title-text); - margin: var(--spacing-xl) 0 var(--spacing-md) 0; - font-size: var(--text-md); - display: inline-block; - position: relative; - padding: var(--spacing-sm) var(--spacing-md); - background: linear-gradient(to bottom, #2a8dd4 0%, #015eae 100%); - border-radius: var(--spacing-sm); - border: 1px solid var(--border-color); - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +/* Print styles */ +@media print { + body { + background: white; + padding: 0; + } + + .container { + border: none; + box-shadow: none; + margin: 0; + } + + header, footer, .window-controls { + display: none; + } + + nav { + display: none; + } + + main { + padding: 0; + } + + a { + color: black; + text-decoration: underline; + } + + .tags { + display: none; + } } \ No newline at end of file diff --git a/themes/y2k/style.css b/themes/y2k/style.css index 0d814df..2a0d9c4 100644 --- a/themes/y2k/style.css +++ b/themes/y2k/style.css @@ -2,35 +2,62 @@ * Y2K Theme for BSSG * Turn of the millennium aesthetic with bold colors and digital futurism * Featuring bubble effects, gradients, and elements from early 2000s web design + * IMPROVED: Better accessibility, performance, and text browser support */ -@import url('https://fonts.googleapis.com/css2?family=VT323&family=Titillium+Web:wght@400;700&display=swap'); +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy animations */ + .site-title a { + animation: none !important; + } + + .marquee-content { + animation: none !important; + transform: none !important; + } + + /* Simplify hover effects */ + nav a:hover, .site-title a:hover { + transform: none !important; + } +} + +/* OPTIMIZED: Using system fonts instead of external Google Fonts for better performance and privacy */ +/* Removed Google Fonts import - using system font alternatives */ :root { - /* Y2K color scheme - bold and digital */ - --bg-color: #CCCCFF; - --bg-secondary: #FFCCFF; - --text-color: #333366; - --text-light: #666699; - --link-color: #FF3399; - --link-hover: #FF66CC; - --accent-blue: #33CCFF; - --accent-pink: #FF66FF; - --accent-yellow: #FFFF66; - --header-color: #6633CC; - --border-color: #9999CC; - --nav-bg: #EEEEFF; - --bubble-gradient-1: rgba(255, 102, 255, 0.5); - --bubble-gradient-2: rgba(102, 204, 255, 0.5); - --highlight-color: #EEDDFF; + /* Y2K color scheme - IMPROVED contrast ratios */ + --bg-color: #E6E6FF; /* Lighter for better contrast */ + --bg-secondary: #FFE6FF; /* Lighter for better contrast */ + --text-color: #1A1A4D; /* Darker for better contrast */ + --text-light: #4D4D80; /* Improved contrast */ + --link-color: #CC0066; /* Darker for better contrast */ + --link-hover: #FF3399; + --accent-blue: #0099CC; /* Darker for better contrast */ + --accent-pink: #FF33CC; + --accent-yellow: #FFCC00; /* Darker for better contrast */ + --header-color: #4D1A99; /* Darker for better contrast */ + --border-color: #8080B3; /* Improved contrast */ + --nav-bg: #F0F0FF; + --bubble-gradient-1: rgba(255, 102, 255, 0.3); /* Reduced opacity for performance */ + --bubble-gradient-2: rgba(102, 204, 255, 0.3); /* Reduced opacity for performance */ + --highlight-color: #E6CCFF; --accent-color: var(--accent-blue); --accent-secondary: var(--accent-pink); - --quote-bg: #EEEEFF; + --quote-bg: #F0F0FF; - /* Typography */ - --font-main: 'Titillium Web', 'Arial', sans-serif; - --font-digital: 'VT323', monospace; - --font-mono: 'Courier New', monospace; + /* Typography - Using system fonts for better performance and privacy */ + --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; + --font-digital: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; + --font-mono: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; /* Font Sizes */ --text-xs: 0.8rem; @@ -56,10 +83,10 @@ --spacing-2xl: 30px; --spacing-3xl: 40px; - /* Transitions */ + /* Transitions - OPTIMIZED for performance */ --transition-fast: 0.1s; - --transition-base: 0.2s; - --transition-slow: 0.3s; + --transition-base: 0.15s; /* Reduced for better performance */ + --transition-slow: 0.2s; /* Reduced for better performance */ /* Sizing */ --content-width: 960px; @@ -67,7 +94,7 @@ --button-radius: 25px; } -/* Base elements with background gradient */ +/* Base elements with optimized background gradient */ body { font-family: var(--font-main); background: linear-gradient(135deg, var(--bg-color) 0%, var(--bg-secondary) 100%); @@ -76,14 +103,14 @@ body { padding: var(--spacing-xl); line-height: var(--line-height-base); font-size: var(--text-base); - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ } -/* Container with early 2000s styling */ +/* Container with early 2000s styling - OPTIMIZED */ .container { max-width: var(--content-width); margin: var(--spacing-xl) auto; - background-color: rgba(255, 255, 255, 0.7); + background-color: rgba(255, 255, 255, 0.8); /* Improved contrast */ border: 3px solid var(--border-color); border-radius: var(--border-radius); overflow: hidden; @@ -91,7 +118,7 @@ body { position: relative; } -/* Digital stars in the background */ +/* OPTIMIZED: Simplified digital pattern */ .container::before { content: ""; position: absolute; @@ -99,12 +126,16 @@ body { left: 0; right: 0; bottom: 0; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Cpath d='M20 0L20 40M0 20L40 20M8 8L32 32M32 8L8 32' stroke='%239999CC' stroke-width='0.5' stroke-dasharray='1,4' stroke-linecap='round'/%3E%3C/svg%3E"); - opacity: 0.2; + /* SIMPLIFIED: Basic pattern instead of complex SVG */ + background-image: + linear-gradient(45deg, transparent 40%, rgba(153, 153, 204, 0.1) 50%, transparent 60%), + linear-gradient(-45deg, transparent 40%, rgba(153, 153, 204, 0.1) 50%, transparent 60%); + background-size: 20px 20px; + opacity: 0.3; pointer-events: none; } -/* Y2K header with gradient */ +/* Y2K header with gradient - OPTIMIZED */ header { background: linear-gradient(to right, var(--header-color), var(--accent-blue)); color: white; @@ -115,17 +146,17 @@ header { border-bottom: 3px solid var(--border-color); } -/* Bubble effect behind header */ +/* OPTIMIZED: Simplified bubble effect */ header::before { content: ""; position: absolute; - width: 200px; - height: 200px; + width: 150px; /* Reduced size for performance */ + height: 150px; border-radius: 50%; background: radial-gradient(circle, var(--bubble-gradient-1) 0%, var(--bubble-gradient-2) 100%); - top: -100px; + top: -75px; right: -50px; - opacity: 0.6; + opacity: 0.4; /* Reduced opacity */ z-index: 0; } @@ -139,7 +170,7 @@ header h1 { z-index: 1; } -/* Site title with Y2K futuristic styling */ +/* Site title with Y2K futuristic styling - IMPROVED accessibility */ .site-title { margin: 0; padding: 0; @@ -148,38 +179,54 @@ header h1 { position: relative; z-index: 1; letter-spacing: 1px; + color: white; /* Fallback color */ } .site-title a { text-decoration: none; - background: linear-gradient(45deg, - var(--accent-blue) 0%, - var(--accent-pink) 30%, - var(--link-color) 60%, - var(--accent-yellow) 100% - ); - background-size: 200% 200%; - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: white; /* Fallback for text browsers */ text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2); display: inline-block; - animation: y2k-gradient 5s ease infinite; transition: all var(--transition-slow); } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(45deg, + var(--accent-blue) 0%, + var(--accent-pink) 30%, + var(--link-color) 60%, + var(--accent-yellow) 100% + ); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + color: transparent; + /* CONDITIONAL: Animation only if motion is not reduced */ + animation: y2k-gradient 5s ease infinite; + } +} + .site-title a:hover { - transform: scale(1.05) rotate(-1deg); + transform: scale(1.02) rotate(-0.5deg); /* Reduced for better performance */ text-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3); } -@keyframes y2k-gradient { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } - 100% { background-position: 0% 50%; } +.site-title a:focus { + outline: 3px solid var(--accent-yellow); + outline-offset: 3px; + transform: scale(1.02) rotate(-0.5deg); + text-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3); } -/* Y2K-style bubble buttons navigation */ +/* OPTIMIZED: Simplified gradient animation */ +@keyframes y2k-gradient { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +/* Y2K-style bubble buttons navigation - OPTIMIZED */ nav { background-color: var(--nav-bg); padding: var(--spacing-md); @@ -212,7 +259,15 @@ nav a { nav a:hover { background: linear-gradient(to bottom, white, var(--accent-yellow)); - transform: scale(1.05); + transform: scale(1.02); /* Reduced scale for better performance */ + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); +} + +nav a:focus { + outline: 3px solid var(--accent-yellow); + outline-offset: 3px; + background: linear-gradient(to bottom, white, var(--accent-yellow)); + transform: scale(1.02); box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); } @@ -238,10 +293,10 @@ nav a:last-child:hover { main { padding: var(--spacing-2xl); position: relative; - background-color: rgba(255, 255, 255, 0.5); + background-color: rgba(255, 255, 255, 0.6); /* Improved contrast */ } -/* Y2K-style typography */ +/* Y2K-style typography - IMPROVED contrast */ h1, h2, h3, h4, h5, h6 { color: var(--header-color); margin-top: 1.5rem; @@ -258,14 +313,22 @@ h1 { h2 { font-size: var(--text-xl); - background: linear-gradient(to right, var(--header-color), var(--accent-blue)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--header-color); /* Fallback color */ padding-bottom: var(--spacing-sm); border-bottom: 2px dotted var(--border-color); } +/* Progressive enhancement for h2 gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + h2 { + background: linear-gradient(to right, var(--header-color), var(--accent-blue)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + +.posts-list h2, h3 { font-size: var(--text-lg); color: var(--accent-blue); @@ -276,7 +339,7 @@ p { line-height: var(--line-height-base); } -/* Y2K-style links with hover effect */ +/* Y2K-style links with hover effect - IMPROVED contrast */ a { color: var(--link-color); text-decoration: none; @@ -289,10 +352,17 @@ a:hover { text-decoration: underline; } -/* Digital counter effect */ +a:focus { + outline: 2px solid var(--accent-yellow); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + +/* Digital counter effect - IMPROVED contrast */ .digital-counter { font-family: var(--font-digital); - background-color: #000033; + background-color: #000066; /* Darker for better contrast */ color: #00FF00; padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--spacing-sm); @@ -303,10 +373,10 @@ a:hover { box-shadow: inset 0 0 5px rgba(0, 255, 0, 0.5); } -/* Y2K-style articles with bubble corners */ +/* Y2K-style articles with bubble corners - OPTIMIZED */ article { margin-bottom: var(--spacing-3xl); - background-color: rgba(255, 255, 255, 0.7); + background-color: rgba(255, 255, 255, 0.8); /* Improved contrast */ border: 2px solid var(--border-color); border-radius: 15px; padding: var(--spacing-xl); @@ -315,27 +385,27 @@ article { overflow: hidden; } -/* Decorative Y2K corner bubbles */ +/* OPTIMIZED: Simplified decorative bubbles */ article::before { content: ""; position: absolute; - width: 100px; - height: 100px; + width: 80px; /* Reduced size for performance */ + height: 80px; border-radius: 50%; - background: radial-gradient(circle, rgba(255, 102, 255, 0.2) 0%, rgba(255, 255, 255, 0) 70%); - top: -50px; - left: -50px; + background: radial-gradient(circle, rgba(255, 102, 255, 0.15) 0%, rgba(255, 255, 255, 0) 70%); + top: -40px; + left: -40px; } article::after { content: ""; position: absolute; - width: 80px; - height: 80px; + width: 60px; /* Reduced size for performance */ + height: 60px; border-radius: 50%; - background: radial-gradient(circle, rgba(102, 204, 255, 0.2) 0%, rgba(255, 255, 255, 0) 70%); - bottom: -40px; - right: -40px; + background: radial-gradient(circle, rgba(102, 204, 255, 0.15) 0%, rgba(255, 255, 255, 0) 70%); + bottom: -30px; + right: -30px; } article:last-child { @@ -365,209 +435,235 @@ article .meta { background-color: var(--header-color); color: white; padding: var(--spacing-xs) var(--spacing-md); - border-radius: var(--spacing-md); - font-size: var(--text-xs); + border-radius: var(--spacing-sm); + font-size: var(--text-sm); + display: inline-block; } .tags { display: flex; flex-wrap: wrap; - gap: var(--spacing-md); + gap: var(--spacing-sm); + margin-top: var(--spacing-lg); } .tags a { - background: linear-gradient(to bottom, white, #f0f0ff); - color: var(--text-color); + background: linear-gradient(to bottom, var(--accent-pink), var(--link-color)); + color: white; padding: var(--spacing-xs) var(--spacing-md); + border-radius: var(--button-radius); font-size: var(--text-xs); - text-decoration: none; - border-radius: 12px; - border: 1px solid var(--border-color); - transition: all var(--transition-base); + text-transform: uppercase; + letter-spacing: 1px; + border: 1px solid rgba(255, 255, 255, 0.5); + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); } .tags a:hover { - background: linear-gradient(to bottom, white, var(--accent-yellow)); - transform: scale(1.05); + background: linear-gradient(to bottom, var(--link-hover), var(--accent-pink)); + transform: translateY(-1px); /* Reduced for better performance */ +} + +.tags a:focus { + outline: 2px solid var(--accent-yellow); + outline-offset: 2px; + background: linear-gradient(to bottom, var(--link-hover), var(--accent-pink)); + transform: translateY(-1px); } .tags-list { - list-style-type: none; - padding: 0; display: flex; flex-wrap: wrap; - gap: 10px; + gap: var(--spacing-lg); + margin-bottom: var(--spacing-xl); } .tag-count { - background-color: var(--accent-pink); + background: var(--accent-blue); color: white; - font-size: 0.75em; - margin-left: 5px; - padding: 0 5px; - border-radius: 8px; + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: 50%; + font-size: var(--text-xs); + font-weight: bold; + margin-left: var(--spacing-sm); + min-width: 20px; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.5); } -/* Y2K code blocks */ code { - font-family: var(--font-mono); - background-color: #000033; - color: #00FF00; + background: rgba(0, 0, 102, 0.1); /* Improved contrast */ + color: var(--header-color); padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--spacing-xs); + font-family: var(--font-mono); font-size: var(--text-sm); - border-radius: 3px; + border: 1px solid var(--border-color); } pre { - background-color: #000033; - color: #00FF00; + background: rgba(0, 0, 102, 0.05); /* Improved contrast */ + color: var(--text-color); padding: var(--spacing-lg); - overflow-x: auto; - font-size: var(--text-sm); border-radius: var(--spacing-md); - margin: var(--spacing-xl) 0; - border: 2px solid #333366; - font-family: var(--font-mono); + overflow-x: auto; + border: 2px solid var(--border-color); + margin: var(--spacing-lg) 0; } pre code { - background-color: transparent; + background: none; + border: none; padding: 0; + color: inherit; } -/* Y2K images with bubbly border */ img { max-width: 100%; height: auto; - border: 3px solid var(--accent-blue); - border-radius: var(--spacing-lg); - padding: var(--spacing-sm); - background-color: white; - box-shadow: 3px 3px var(--spacing-md) rgba(0, 0, 0, 0.2); + border-radius: var(--spacing-md); + margin: var(--spacing-lg) 0; + border: 2px solid var(--border-color); + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.1); } -/* Y2K-style footer */ footer { background: linear-gradient(to right, var(--header-color), var(--accent-blue)); color: white; - padding: var(--spacing-xl) var(--spacing-2xl); - font-size: var(--text-sm); + padding: var(--spacing-xl); text-align: center; border-top: 3px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; position: relative; - overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + gap: var(--spacing-xl); } -/* Bubble decoration for footer */ +/* OPTIMIZED: Simplified footer decorations */ footer::before { content: ""; position: absolute; - width: 150px; - height: 150px; + width: 100px; + height: 100px; border-radius: 50%; - background: radial-gradient(circle, var(--bubble-gradient-1) 0%, transparent 70%); - bottom: -80px; - left: 10%; + background: radial-gradient(circle, var(--bubble-gradient-1) 0%, rgba(255, 255, 255, 0) 70%); + top: -50px; + left: var(--spacing-xl); opacity: 0.3; } footer::after { content: ""; position: absolute; - width: 120px; - height: 120px; + width: 80px; + height: 80px; border-radius: 50%; - background: radial-gradient(circle, var(--bubble-gradient-2) 0%, transparent 70%); - top: -60px; - right: 10%; + background: radial-gradient(circle, var(--bubble-gradient-2) 0%, rgba(255, 255, 255, 0) 70%); + bottom: -40px; + right: var(--spacing-xl); opacity: 0.3; } footer a { - color: white; + color: var(--accent-yellow); text-decoration: none; font-weight: bold; - position: relative; - z-index: 1; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); } footer a:hover { - color: var(--accent-yellow); + color: white; + text-decoration: underline; +} + +footer a:focus { + outline: 2px solid var(--accent-yellow); + outline-offset: 2px; + color: white; text-decoration: underline; } -/* Y2K-style pagination */ .pagination { display: flex; justify-content: center; align-items: center; + gap: var(--spacing-lg); margin: var(--spacing-2xl) 0; - gap: var(--spacing-md); } .pagination a { - background: linear-gradient(to bottom, var(--accent-blue), #0099cc); - color: white; - padding: var(--spacing-md) var(--spacing-lg); - text-decoration: none; + background: linear-gradient(to bottom, white, var(--nav-bg)); + color: var(--text-color); + padding: var(--spacing-md) var(--spacing-xl); border-radius: var(--button-radius); - border: 2px solid #0099cc; - font-weight: bold; - box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: all var(--transition-base); -} - -.pagination a:hover { - background: linear-gradient(to bottom, #66ddff, #00aadd); - transform: scale(1.05); - text-decoration: none; -} - -.pagination .page-info { - background-color: rgba(255, 255, 255, 0.7); - padding: var(--spacing-sm) var(--spacing-md); - border-radius: var(--spacing-lg); border: 2px solid var(--border-color); - font-family: var(--font-digital); - color: var(--header-color); + text-decoration: none; + font-weight: bold; + transition: all var(--transition-base); + text-transform: uppercase; + letter-spacing: 1px; font-size: var(--text-sm); } -/* Y2K-style horizontal rule with stars */ -hr { - border: none; - height: 20px; - background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath d='M10 0l2.4 7.4H20l-6.2 4.5 2.4 7.4-6.2-4.5-6.2 4.5 2.4-7.4L0 7.4h7.6z' fill='%239999CC'/%3E%3C/svg%3E"); - background-repeat: repeat-x; - background-position: center; - margin: var(--spacing-xl) 0; +.pagination a:hover { + background: linear-gradient(to bottom, white, var(--accent-yellow)); + transform: scale(1.02); + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); } -/* Y2K-style divider */ -.y2k-divider { - display: flex; - align-items: center; - margin: var(--spacing-xl) 0; +.pagination a:focus { + outline: 3px solid var(--accent-yellow); + outline-offset: 3px; + background: linear-gradient(to bottom, white, var(--accent-yellow)); + transform: scale(1.02); + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); } -.y2k-divider::before, -.y2k-divider::after { - content: ""; - flex: 1; - height: 2px; - background: linear-gradient(to right, transparent, var(--accent-pink), transparent); -} - -.y2k-divider span { - padding: 0 var(--spacing-md); +.pagination .page-info { + font-family: var(--font-digital); + background-color: var(--header-color); + color: white; + padding: var(--spacing-md) var(--spacing-lg); + border-radius: var(--spacing-md); font-size: var(--text-md); color: var(--accent-pink); } -/* Marquee effect - classic Y2K */ +/* Y2K horizontal rule */ +hr { + border: none; + height: 3px; + background: linear-gradient(to right, var(--accent-blue), var(--accent-pink), var(--accent-yellow)); + margin: var(--spacing-2xl) 0; + border-radius: var(--spacing-xs); +} + +/* Y2K divider with decorative elements */ +.y2k-divider { + text-align: center; + margin: var(--spacing-2xl) 0; + position: relative; +} + +.y2k-divider::before, +.y2k-divider::after { + content: "◆◇◆"; + color: var(--accent-pink); + font-size: var(--text-lg); + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.y2k-divider span { + background: var(--bg-color); + padding: 0 var(--spacing-lg); + color: var(--header-color); + font-weight: bold; +} + +/* OPTIMIZED: Marquee effect with reduced motion support */ .marquee { background-color: var(--accent-blue); color: white; @@ -583,16 +679,32 @@ hr { .marquee-content { display: inline-block; - animation: marquee 15s linear infinite; + /* CONDITIONAL: Animation only if motion is not reduced */ + animation: marquee 20s linear infinite; /* Slower for better performance */ } +/* Text browser fallback for marquee */ +@media (max-width: 0) { /* This targets text browsers */ + .marquee-content { + animation: none; + transform: none; + } +} + +/* OPTIMIZED: Simplified marquee animation */ @keyframes marquee { 0% { transform: translateX(100%); } 100% { transform: translateX(-100%); } } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { + body { + padding: var(--spacing-md); + /* OPTIMIZED: Simplified background on mobile */ + background: linear-gradient(135deg, var(--bg-color) 0%, var(--bg-secondary) 100%); + } + .container { margin: var(--spacing-md); } @@ -667,6 +779,7 @@ hr { font-size: var(--text-lg); } + .posts-list h2, h3 { font-size: var(--text-md); } @@ -676,7 +789,7 @@ hr { } } -/* Featured Images with Y2K styling */ +/* OPTIMIZED: Featured Images with Y2K styling */ .featured-image { margin: 1.5rem 0; border: 3px solid var(--border-color); @@ -688,15 +801,16 @@ hr { padding: var(--spacing-sm); } +/* OPTIMIZED: Simplified bubble effect */ .featured-image::before { content: ""; position: absolute; - width: 150px; - height: 150px; + width: 100px; /* Reduced size for performance */ + height: 100px; border-radius: 50%; - background: radial-gradient(circle, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0) 70%); - top: -50px; - left: -50px; + background: radial-gradient(circle, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 70%); + top: -30px; + left: -30px; opacity: 0.5; z-index: 1; pointer-events: none; @@ -712,8 +826,8 @@ hr { } .featured-image:hover img { - transform: scale(1.02); - filter: brightness(1.05); + transform: scale(1.01); /* Reduced scale for better performance */ + filter: brightness(1.02); } .featured-image .image-caption { @@ -787,13 +901,13 @@ hr { border: 1px solid rgba(255, 255, 255, 0.7); } -/* Selection styling */ +/* IMPROVED: Better text selection */ ::selection { background-color: var(--highlight-color); color: var(--text-color); } -/* Date header with Y2K styling */ +/* Date header with Y2K styling - IMPROVED contrast */ .date-header { font-family: var(--font-main); font-weight: bold; diff --git a/themes/zxspectrum/style.css b/themes/zxspectrum/style.css index 84d55c3..5168883 100644 --- a/themes/zxspectrum/style.css +++ b/themes/zxspectrum/style.css @@ -1,8 +1,37 @@ /* * ZX Spectrum Theme for BSSG * Bold colors with black background inspired by the iconic home computer + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic ZX Spectrum aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable ZX Spectrum-specific animations */ + .site-title a { + animation: none !important; + } + + /* Disable pixel overlay effect for reduced motion */ + body::before { + display: none !important; + } +} + :root { /* ZX Spectrum color palette */ --black: #000000; @@ -33,10 +62,10 @@ --accent-secondary: var(--bright-magenta); --quote-bg: var(--black); - /* Typography */ - --font-main: 'ZXSpectrumFont', 'Courier New', monospace; - --font-headings: 'ZXSpectrumFont', 'Courier New', monospace; - --font-mono: 'ZXSpectrumFont', 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for ZX Spectrum look */ + --font-main: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + --font-headings: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + --font-mono: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; /* Sizing and spacing system */ --content-width: 820px; @@ -72,11 +101,7 @@ --border-thick: 4px; } -/* Pixelated effect for the entire document */ -@font-face { - font-family: 'ZXSpectrumFont'; - src: url('data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABF0ABAAAAAACgwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCQVNFAAARUAAAAOwAAAGOm5pjl0dQT1MAAAGQAAAAMQAAAD5VD0ORR1NVQgAAEbwAAAGIAAADHMULLGVPUy8yAAAB+AAAAFcAAABga1GRJGNTSEQAAA9AAAATRAAAAVRR+wjZY21hcAAAEcAAAADeAAABwtW+K9tjdnQgAAAP1AAAABYAAAAWBe4NdmZwZ20AAA8oAAABAgAAAXMGWZw3Z2FzcAAAEUQAAAAIAAAACAAAABBnbHlmAAAQUAAABcoAAApEh4BFcmhlYWQAAAJEAAAANgAAADYLf8ThaGhlYQAAAngAAAAdAAAAJA5dBWloaGVhAAACmAAAABkAAAAkCngFDWhtdHgAAALUAAAAWQAAAGhJ5QMmbG9jYQAAEAQAAAA2AAAANkhGQ+ZtYXhwAAACsAAAACAAAAAgAPcAl25hbWUAABMcAAABAQAAAkoMr0x5cG9zdAAAFFAAAAB0AAAAsDlLRORwcmVwAAAPwAAAAFgAAABYb9ZwSnjaY2BkYGAA4nKrMoXx/DZfGeSZXwBFGK6suTIJos9iM/7/834KcxJzEMjlYGACiQIA66AMgXjaY2BkYGCO+V/CEMX8goHh/3/mJAagCApgBgCAdQVCA+xgAAAAAABQAAAYAAB42mNgZj7LOIGBlYGBqYtpDwMDQw+EZnzAYMjIRRTMwcDAxMDKzAADjAIMCBCQ5prC0MAQyBzEnPQ/iyGKOYlhOlCYgYF5fhlDAgMDAGDSDh0AAAB42mNgYGBmgGAZBkYGELgD5DGC+SwMDcxgmgwoBOReYQv6n8XwnP+/AUgtZZBmkGGQY5AHivyHiP//D1LGCNGsCNKMBExXQnkMbACHWRk8AAAAAAAAAAAAAADVAPIBfAIGAkgCvAMKA2ADjAPgBCQEeATgBUgFegWCBdQF8gYeBmgGmAb+ByIHWgeWB7YH9AgkCFwIhAjyCVQJrgn4Cl4KngrgCyILYAuwC94MPAyODL4M+A1IDXQNpg3kDhgOQA56DsQO+g8qD1YPeA+oD9YQBhBAEI4QvBEOETQRXhGSEcgSCBJSEpYTBBNCE3AAAAABAAAAGABUAAkAAAAAAAIAFAAkAHMAAABeC3AAAAAAeNqNkM9qE1EUxr9JpmA10IWb5O63qIiztKkmhdIuijtFK1QQF93lJpnak8kkTGaCG7fufIC+gK/g0ifQR3DjE3TrL9Mmkha60Jlz7/3ud77vnHsAL/ADEee7A+CZ8QgveTKOsIOrwGO8ws/AU7jCn8DTKIN54Bns4GPgWbzB58BzqPE88Dze8k/gBVTxFZPsEg3jCGt4H3iMZ/gaeIw3RANPocK7wNOY4UfgGSyj13PZ8T6Gcva1q2vjlDFWvZJHsxfdFWJuXTqWyhY1qEXdnRJ9k0k1MPEyt0pvG33t5vbeeOEaYR9n/OxYp1ImXlvr0wPdm0S63Ntp7YzW/2w3iXPCd8ytsU6tnVeZiWXoGG0LV2oT23TDOfHIpFmmQmeOJh7OUb+2tdwxn8SamDQsT9Mxvj5UhqX/P/PoXqbZkY7Lml70jpKpXVKlJCnb5UMzz4bTYZK0y6iyNYaIkeDQWUwwa2XRMUlzqbpJOY6nZrFCXtWf7dPCLCcmzXztZbXWxDQoG3bvrFiJ7hFKZMjtAi/J9ujaRBmK7hgG59TsNHGNM25KOcpx1qAeYIUzPJbkptGmI6e9zJF83WTK3QnKf3JXd8K9ZbYOu7V0IG9GZzkHF/W08YR6eWl7fS86FPGe/hYdbDn24WDY7fLe0v2v3PUydjY4XT+5kBzxLpZEKP8Aqcmm9wB42nWRR0/DQBCF39S0AgkEvRMSUkhhx6FUCRAfSAiBIwc+J+vExnIcxfHvYe+IOZD27c7MzpsyBlC/JsQo6GJYsZIxTOAkWXZYV2BxLXl8Yl2ZTdKaE+JQWEeRbRTb6Ni4ZVtDhW11bKSo96OoyHsQxbn8hqJYyR8oir/6NUVhIL+iKBbya4rCJ39IUVjyegqFIRsGjMnX4LsVvMKCT1HEoKJ4hA2P4o3O9UX+0rlh5AqVcuX+1H7iCZlxfIFLXOEaN3hCS0rIaHlGBy94xRs+pM5MyqoWZubMN7KZibmpDSPN3M1Ek5mnmY2ZXzNHZqFmoc1CzRW7Bnc1YvqtLaSxgUMcYQ9FbJLMWDcnuUwG/A9SvstAeNpVzLsOQFAAQNHbvr7F04XoSUSCGOwWiUkxSGxWu/9/NF7OcgCPEgRJCURpWUVOU1ZRU9fkdPUNREMjCcnUTDa3UBStLK1t7OyVHXQdVVSFFidnF1c3dw9PL2+fvY+vn39AYFDwIST0GBYeERldiI29pKRe09Kzsr8jAAAAuAH/hbAEjQAAACwALAAsACwAeAC0AQgBcAIKAkoCpAMMlA+mhQAAeNq1Vc1vG0UUf29mdmZ3vWuv7V3HeeLYSfw5sWM7jms7aZOUNFE+mjRqSUupIqJWREIIqagSHwcUkCohoA0gLiAQEicuIMS/AJdK3Ljm1IQTp1wo9r5ZO3ZJicSJHc3uzG/ee/N+7/d7awAAH78HYLAMd0EO7sNBAEnTZ9kyJ+yzZOm0acMJkfY+lslJEzTj0+Y2ckTOchSF5g7Q8Kk/9aCLQxMEq6DBKTgJAI5sIRLq00b2yKakZ/6kz/LluIMaDof3hnvDR8NteDb8cng+3EHsF+kXcfqMf4X+Bf0lSYkpCQSPNW1YsZ1UyrJSTNEeKJZlqZZVSWA7PZzSHMR0cDOVNu2KVcr4lCw17dihqm9XKxYq11KVdKWWqW6kE2nLKW1v8YRTcQ61JcvOpG+UbcvJ1Aq4ZGfrFSdXLRU3Nnd2P4QNKFCGWp2rV9Hv2iU0+R3oYKXKpVLaxqQrONksZ7LpbDZXcXIyktdz9UZho7nZ2mnvo92XM02Xy9nNDdrFRiEFt+EOfARD+IZMfkteJ0nyLbkgZ8g9ksBaIk6CFXrX3Aw5B7fgFD4Fw7yPxmRpnkbdmb1R8WavVz3LlD17J3O+KudLmXyg08HsZDLVslfXhO6rJb8WCYRisWAoFJwQhFAsHo1HhWgy2I9FY0+fh0fPBDmFfyv+bHO5YXbDRbKn6jX9Gu4pSkNV67jHe6+pqrrKe6+e9VSYg9PwOtwYvT9/RfwuAE5K8ZjH1EfhJ1t/8JMQElvgMVnvZ4iQ8Ah/g5/HcFDwGBt0FIYBPPbAmAUw4uDjD1j1fhr7PsY/MXTwWPc45scxXhnHUsCMSPUTi/IA+2kzllvYbsHzMKLg8ORHmMVtW9juYPsA203ccXDcwfYDtttjm3cwRsSHGa3QQv2l1NzQ6kbBsOLJiVBsIhwJBGKq8jwZDUyQUCQSCoR0UbvQtXN+oWw+EaHiuLGTnFdkn6woEz61KfOVU49XrqSXnYXW9uL5tYXVlaXzi/XmbK1Wr9XL5dnZpfWlwWzrmVpwMb1irM5pXPzs8vHp6uRkNRgKhYLBaOxMsVhpuIk35ouOXkuLhRXJPzlpmqbKy6aWkROLPvGRmZqyV+aX1i+fvvz9++9cunoGjh1bmZ21ZwqFWq3V6nc7Syvzyxur3fPV+trcTKFg56t6pVI+4efVxDJ9cVJR2mBE4xFdCqj84eSEqsuRgK4fDyRDIT2oRyICjzOH/aXDvsIxiJEgaHAOiV5DLmoo8CUOVSc/C9dRX8HuBO4bHKZxlK5YHRdcvZaQbWyXOcjCFWR3nW8iA3fhDlJmHzHOCe/JQdHRO9g9wCq7OLLPVXYFdvHsVZR4DZEjvuJ+Uyo9P9Ove5vX6aNxd1frnXMDRUdHjJESrp7vYAyXnHO3I/XwwT+XJVkJiEI8nQiJ4rTs97+j+H0zfklSBEl5VyGbyoIiCqqqHCOCQMRQNB6NhJOxSHpSoRLxaUIwIitKLBVOTcVTQVkJpHuHPaTPtHlOZF4TdnSR5ZGYHMQBTfJoKVLAiFP/9n+DPDP6wAZ/AK/BO9CBPrwNr8ClcbxLXu9gjcsdC4RrcKJPeqgmCb+FqXxE+PFnPPPQ+LyOUeTIXRGCB16GN+DNw73GIkTJO0/w14OM/xpkfPeYYLHn+MPn+L/P8UPP8RcBvs3/u4A88j9//k7+o0LfwCptNBrjf44mwqo1DFIUn/Y0bVoTNFVVSMDHB5KikyTfVCXG+CZrqtQcT8kn7JQmaJomRKcaS+3lsNgv2unLp1sLZ5cW6s2F+eW2Ua41ZyvFxtzJ5vKJZqvddE3XjBu3+ot9N9dsGu5co7WyHPz8/MbZxfOzi7Vza7P1xvz8XL1mz8y4y5dPtdyTrc7g2p1B21icddxmfXbGrpbKxbppJDQtFg3GYhOxaFDScqnMrb2LBzd+7O4N9m+++8bNL28NpZASTigJVU8mFTWRSCSSU8fDsdgn79MvpJDoV5KJePyjXU3TjUg0FOUHEjm2xhLRcCQi0pAe1YOqJoXT6XAkORWOylpSNI14Qt8VFWkqIQUn49FwJKTwkygmVH0qlohpSU3WeBpyIhoKhSRZFBUpqGoTkURICyrRqUBQnEpNhUPaZDyQDKrx8LEPNCUQjvB06jQaSk5p0qSqJaNqJJKMB4JhfSqqwf8QzaUxAHjah1j5ZZXaJgAAPgwYiK9JcL/70ZUu3ZfSF+neoW+kmUn3pEiH7oEUE6kLGYD7CW9vW0Db8fYGMPa+0cUA0J3hGQZgOZbQNO5wbRpHuN4wP37jeII/LS7qwbX7H343+A4j4K7GCEHqJ9bnr/GP9QMbhjP8w9oGwPcXgLcniwQEyOiCvAG8lZb4W2VIDQDpRlJ83Vta5E2nLQfRIVxaWuDQx55gOVLJt/cBACCpfBsA8CKTv1NnSZcUAKqvqzulBvBu6YCYrqsDbJkaNvZgdEXQAW+DLdKz4TBvAxpO7P0cLYU75S2Vo22ZRE9mEhstTY6ZkuQZBmm2pLKR8LS8nLAYhtSSrOSNWUlOLSnJbEL8ykgkFEyKX5hzRZF+TYRDfkkrKkmzmEgw6pPWlESmGDN5+w8FTWVC7JOjWYf4I5Iyj4/D3X4+6PULIaeQ8HqDO1KvWyzpY/79BW96JFVw+sTRhMcrkQVvwicuSmXeUt43JvSX8n5v0SnJwdK+V5KKYo83KfgkBY/YJ5ZK4vygTyztA4+cYsL7RyogPCZ9whcmXH2hT2Qlpa8WEzPlmZnKZcXLXF9ZnrE7s1lnoZqrVOazC4ZzsuasJCbgzeJlS5Vj82PzmVKmvAxPFyvzVefExPLyxNj8fKVYXJiAT6vVqhAWJifw5vLYDHxcnJutGBnDwMuyvLIyu7w8W1lZkeWxsbGZGcPIFwqTk0bBKBQmJwuGkc/D59VqFT4lDQmSvZnO5pK59M7TJz89faG/0I5kkxmyocvK+GFRLm2XFtR2oagf1lTtu25Rb9cVzSQNcg56ByY/iXEIg/Pn+8MbD4KT8KZBg1sGDfYd9EsODZYcGqQcNOg70FgxqcGJiQbvH1igqwJl46YvIFhgP1hgO1hg7aBCzUGFkoMKRQcNbh00KDhokHfQoHvQIHvQIOcY10p+DWsZSduzZlrGrI2MJm/KJEtxvBQnS3G8FCdLcbwUr57gK8E/gntN8o+/KI9I8jfvnbU2XkJQ4kl4wz2hpAEfjMr/UgP/AcNMGMABAAAACgAKAAoADAAB//8AAwijYGRgYOADYgkGEwZmBiwGMgYKDGq7jhyYv294/MHt/0/+/z1XaA4AA+0MFAAAAHjaY2BiYGBgYFBjSGFgYghjEGJgYmBgcAViCyBmg/IUwSrhv0LjL1Djn9ik+0qM0oyMjD8E3RgD+F8o+r8z8j6hEvkPQqiEvxIj74PfD0T/0vZ+E/zI+Ifp3Scm3rdMn98z/bj/gPvVg4tM/79fYfgLAFMPJIYAAQAB//8AD3jalVVbbBRVGP7OmZ2Z3ZnZO93d9tLd9n5p2e12l9JCbSktyFIKlIsIpYAQMEYk0ggYlfgAeOFFECPBeHkwmqgvJD5IIIHwQJNGggQMMYEHeTDxATHGA6Tbc2Z2u6Wok8w5/3/5zn/5vvP9BxTgifuc3sUJKKgbgIAIlbZE85Vyvd3wEJUrnXIXe0IPQ28kIJGD2r5F7p5dC5uyRncqzrOHf9+kDX7wXpx89/WBt7HMp1vHB+cM8CNAUBeAdSfPHVeVZnseRmZrvhQf9RkI4DtOvvRYK7c9fmRKnKhwVEW5Wu+08xKW27XO7OpoN4VLu1nq1rHVbgsjFTi4YOPx81fWZQz1/MLnl8/oAfl0RNyS4xbPRSN9yfQ9cxA+uyAMXGZXwANagKEqMCoIAKTAIHt54BLtXIBoCn7bnS1i95FsUUTUyGIH+Nd3xtOTPOSrMAxQUCgC3RnVDa7qgBY8MnUwhCZG0I+iH0UvigMoOihCKEygWED+P+4wBIxRDjc7tCMfZDtm9HgUUYr+Rnq3y5VKlwnSHHYb3aq4GQcqbdXHjX0pebTSp0ZqdL1zJX+KHKWm+diFl85ub+VeHh3eXXg50JEqfV+dWXsokKKl+YMzEzLIscx8cdNDZSXkTzI9GUulvdqcWGoq57d9+cVoL0s6MxvK55y2TYgmMqLjkKqG1YSYN+OuVd+mZxZntmV91x/CJZbST9zN9BQ4IZQD4H2eDX8Yt8kxF8c3+q8RD48X1EQb69UOzpYH9BpzXfK7+h5XMQQhOSjwJhMRcTPsCftsIRXSFIJbxO6xbduzrQyOcaKKG+0B2c08VnGUjWGl2aRGFY4LDWUG3YrwsHI4sNIh9fZ6gO+e17yR6f3jF/T13/3iq4lH53snJrdvK10t7nvn8q3Rz24N8+tjqw8dPn5C3njzk7N3Lnx9+4eBZx42H72xb9/4G0OpTYe+unVwYu+pS4f89GWDy8WDzJewYVsAGUUURVzRQBBQklF0USQIhjEKAkGMiYgCRaxGxajL6fCh16G4JdWTdKZcdjeuJEJ93qfcXn12ZPZUomTj1Mzn+w92/8p+ff+dtYvzwQ/W1vXrT1NWqozg1DK1bHKZdPo1HEtrNlbD0aiLGzxWwwJPMQrUH2N0MCGoyziGVwgoPdWiIQeXgK5jd4J/LDDyZWRZObLhD2bIBmOQwsQR+JZK4JXnTm3ceWHm1vL6sRXlwXwf7+2lz+TevbqnKkm/xJfnMxRu/fLh7OT7p7c5+e7R/WvW+r97tG3d6ZNr9XVnJrftGc41N33/YImUlv2JsxMjr5jxpPnUu+qFXz7xhh7kYcaELa4uZQolLSk3/2FaY4IgwZ0iM7ZA8rADojq9gZlhqGCXO6q6dHAYNzslcpQTGK85jbqgXO4M8RWQUPywxkJhhb8RjgUZA59qizCU6QptRe0/YGM+xSIu1UQRyJoVTDpnRKJ9Xs8CVzRiF3miGvG+TP5fXM1aGqv+LDQrVj1ZaE6s2rmkEkPWbFnE0oJ6ONdL4WzNvUQjnK1bIEYxuOBsjzLsYdLhH80JZ/vMIZYQDu0a8kCDMzdMJbIgwX0MBwhHIABdLM7wLOUGGGGQxbcIlOzCK3qI6QRt43f3+8y7uYj7IpvM9Jk3L4VywdCc0+Pq7bZzL7s7LzYfr5vzODsvZLPpzveNm/n+Zv7G4+KN/GAjz4d01lCNl1kYsKxbORMRuRV1Pam6kX1FcAjYE0XvkoAnKGKA2BEXbmJ9HjG6pN+0Q4ZIsEgpuEqOhKxYDL2FRjWGqXYVa/rvGQXEZhmVAZUJqPbp0jtv0U1jnk1vHD19ZH2Mb9KTYl3fs3Y2nYp6X+Sbt51f2ru/UWg2G6cGD82vmqXzqcZANfztH2MDX8YIv5Uu+/aN2Zfu5VOF6D11+0rPlS9K5XPRwpWu7/ZefXWLnr0Rja1u8rsnD8L/S24SY/I4ifjjEwAA') format('woff'); -} + body, input, button, select, textarea { font-family: var(--font-main); @@ -155,6 +180,14 @@ header h1 { text-shadow: 0 0 8px var(--bright-white), 0 0 15px var(--bright-cyan); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--bright-white); + outline-offset: 2px; + color: var(--bright-white); + text-shadow: 0 0 8px var(--bright-white), 0 0 15px var(--bright-cyan); +} + /* Navigation with ZX Spectrum colors */ nav { margin: var(--spacing-md) 0; @@ -235,11 +268,16 @@ h2::before { color: var(--bright-yellow); } +.posts-list h2, h3 { font-size: var(--font-size-md); color: var(--bright-green); } +.posts-list h2::before { + content: none; +} + p { margin-bottom: var(--spacing-md); line-height: var(--line-height-normal); @@ -262,6 +300,15 @@ a:hover { text-shadow: 0 0 4px var(--bright-white); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--bright-yellow); + outline-offset: 2px; + color: var(--bright-white); + background-color: var(--blue); + text-shadow: 0 0 4px var(--bright-white); +} + /* Articles with ZX Spectrum border */ article { margin-bottom: var(--spacing-lg); @@ -322,6 +369,14 @@ article .meta { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--bright-yellow); + outline-offset: 2px; + background-color: var(--bright-cyan); + transform: translateY(-2px); +} + code { font-family: var(--font-mono); background-color: var(--blue); @@ -387,6 +442,14 @@ footer { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--bright-yellow); + outline-offset: 2px; + background-color: var(--bright-cyan); + transform: translateY(-2px); +} + .pagination .page-info { color: var(--bright-white); background-color: var(--blue); @@ -612,4 +675,4 @@ ul, ol { li { margin-bottom: var(--spacing-xs); -} \ No newline at end of file +}