diff --git a/README.md b/README.md index 46f5519..211cca1 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,12 @@ silently stay blank instead of erroring. - Per-course study guide export (markdown, organized by section) **Personalization** (Settings) -- Built-in themes or a custom accent color, corner radius, and card style +- 54 built-in themes (grouped in the dropdown: Base, Editor Themes, and 5 + categories of website-inspired schemes sourced from Figma's "53 Unique + Website Color Schemes" - 42 of the 53 kept, after cutting a couple of + neon-bright ones and several that ended up near-duplicates of another + scheme once mapped into actual UI colors), or a fully custom accent + color, corner radius, and card style - Font, text size, page width, and spacing density - Default library path override + manual "Refresh Library" (bypasses the 5-minute filesystem-scan cache) @@ -188,3 +193,16 @@ folder and file names. | Documents | `.txt` `.md` `.html` `.htm` `.pdf` `.docx` `.doc` `.rtf` | | Subtitles | `.srt` `.vtt` `.ass` `.sub` `.sbv` | | Quizzes | Any doc file whose name contains `quiz`, `exam`, `test`, `assessment`, `exercise`, `assignment`, or `homework` | + +--- + +## Tools + +`tools/generate_website_scheme_themes.py` is the one-off script that +produced the 42 "website scheme" theme entries in `THEME_PALETTES` (of +53 candidates - 11 were cut after visual review, 2 for a neon-bright +accent and 9 for landing too close to another kept scheme once mapped +into actual UI colors) - not part of the running app, kept for reference +if those palettes ever need regenerating or extending. See its module +docstring for how it turned Figma's color-scheme screenshots into actual +UI palettes. diff --git a/offlineu_core.py b/offlineu_core.py index b5151b3..920bdf0 100644 --- a/offlineu_core.py +++ b/offlineu_core.py @@ -100,6 +100,28 @@ SETTINGS_CHOICES = { 'theme': { 'dark', 'light', 'dracula', 'tokyo_night', 'catppuccin_mocha', 'ayu_dark', 'github_dark', 'atom_one_dark', 'houston', 'night_owl', 'nord', 'matcha', + # 42 palettes derived from Figma's "53 Unique Website Color Schemes" + # resource page (11 of the 53 dropped: 2 for a neon-bright accent + # that read badly as a button color, the rest for being too close + # to another scheme already kept - same accent hue/lightness and + # background, just a different name) - see WEBSITE_SCHEME_CATEGORIES + # below for the dropdown grouping, and + # tools/generate_website_scheme_themes.py (a one-off content- + # generation script, not part of the running app) for how the kept + # ones were built from that page's screenshots: dominant colors + # extracted per scheme, then mapped into this app's bg/text/accent + # shape with automatic contrast adjustment against both the + # background and the white button text. + 'ink_wash', 'jade_pebble_morning', 'woodland', 'graphite', 'urban_slate', 'pearl', + 'vichy', 'sorbet', 'yacht_club', 'amber_walnut_morning', 'copper_aquamarine_dream', + 'cocoa_topaz_noonday', 'sandstone_aquamarine_serenity', 'honey_opal_sunset', + 'seashell_garnet_afternoon', 'rose_quartz_evening', 'calcite', 'fireside', 'terrazzo', + 'sapphire_nightfall_whisper', 'marina', 'emerald_lavender_lake', + 'sage_peridot_morning', 'amethyst_dawn_haze', 'turquoise_amber_autumn', + 'sapphire_ash_morning', 'royal_glimmer', 'neptune', 'tropical_heat', 'celestial', + 'festive_eve', 'freshly_squeezed', 'jelly_shoes', 'gossamer', 'clockwork', + 'lemon_granite_morning', 'arctic_reflection', 'slate', 'autumn_luxe', 'inked', + 'wraith', 'urban_nocturne', }, 'font_family': {'system', 'sans', 'serif', 'monospace', 'monaspace'}, 'font_size': {'small', 'medium', 'large', 'xlarge'}, @@ -118,6 +140,48 @@ THEME_DISPLAY_NAMES = { 'ayu_dark': 'Ayu Dark', 'github_dark': 'GitHub Dark', 'atom_one_dark': 'Atom One Dark', 'houston': 'Houston', 'night_owl': 'Night Owl', 'nord': 'Nord', 'matcha': 'Matcha', + 'ink_wash': 'Ink Wash', + 'jade_pebble_morning': 'Jade Pebble Morning', + 'woodland': 'Woodland', + 'graphite': 'Graphite', + 'urban_slate': 'Urban Slate', + 'pearl': 'Pearl', + 'vichy': 'Vichy', + 'sorbet': 'Sorbet', + 'yacht_club': 'Yacht Club', + 'amber_walnut_morning': 'Amber Walnut Morning', + 'copper_aquamarine_dream': 'Copper Aquamarine Dream', + 'cocoa_topaz_noonday': 'Cocoa Topaz Noonday', + 'sandstone_aquamarine_serenity': 'Sandstone Aquamarine Serenity', + 'honey_opal_sunset': 'Honey Opal Sunset', + 'seashell_garnet_afternoon': 'Seashell Garnet Afternoon', + 'rose_quartz_evening': 'Rose Quartz Evening', + 'calcite': 'Calcite', + 'fireside': 'Fireside', + 'terrazzo': 'Terrazzo', + 'sapphire_nightfall_whisper': 'Sapphire Nightfall Whisper', + 'marina': 'Marina', + 'emerald_lavender_lake': 'Emerald Lavender Lake', + 'sage_peridot_morning': 'Sage Peridot Morning', + 'amethyst_dawn_haze': 'Amethyst Dawn Haze', + 'turquoise_amber_autumn': 'Turquoise Amber Autumn', + 'sapphire_ash_morning': 'Sapphire Ash Morning', + 'royal_glimmer': 'Royal Glimmer', + 'neptune': 'Neptune', + 'tropical_heat': 'Tropical Heat', + 'celestial': 'Celestial', + 'festive_eve': 'Festive Eve', + 'freshly_squeezed': 'Freshly Squeezed', + 'jelly_shoes': 'Jelly Shoes', + 'gossamer': 'Gossamer', + 'clockwork': 'Clockwork', + 'lemon_granite_morning': 'Lemon Granite Morning', + 'arctic_reflection': 'Arctic Reflection', + 'slate': 'Slate', + 'autumn_luxe': 'Autumn Luxe', + 'inked': 'Inked', + 'wraith': 'Wraith', + 'urban_nocturne': 'Urban Nocturne', } # Full color palette per theme. 'accent' here is only the *default* accent @@ -196,6 +260,233 @@ THEME_PALETTES = { 'bg-tertiary-hover': '#3c4850', 'text-primary': '#d1ded3', 'text-muted': '#7c8885', 'border-color': '#707c4f', 'accent': '#a4b07e', 'accent-hover': '#8b966b', }, + # --- Minimal & Neutral (from Figma's "53 Unique Website Color Schemes") --- + 'ink_wash': { + 'bg-primary': '#202020', 'bg-secondary': '#323232', 'bg-tertiary': '#444444', + 'bg-tertiary-hover': '#4b4b4b', 'text-primary': '#e6e6e6', 'text-muted': '#9e9e9e', + 'border-color': '#494949', 'accent': '#2990d6', 'accent-hover': '#2173ab', + }, + 'jade_pebble_morning': { + 'bg-primary': '#f2f4f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#edefeb', + 'bg-tertiary-hover': '#e0e5dc', 'text-primary': '#242522', 'text-muted': '#6b7066', + 'border-color': '#d1d6cd', 'accent': '#659539', 'accent-hover': '#4c702b', + }, + 'woodland': { + 'bg-primary': '#323220', 'bg-secondary': '#48482e', 'bg-tertiary': '#5d5d3c', + 'bg-tertiary-hover': '#676742', 'text-primary': '#e7e7e4', 'text-muted': '#a4a498', + 'border-color': '#646440', 'accent': '#acc927', 'accent-hover': '#879e1f', + }, + 'graphite': { + 'bg-primary': '#f4f1f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efebeb', + 'bg-tertiary-hover': '#e5dcdc', 'text-primary': '#252222', 'text-muted': '#706666', + 'border-color': '#d6cdcd', 'accent': '#d34646', 'accent-hover': '#ba2c2c', + }, + 'urban_slate': { + 'bg-primary': '#f4f1f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efebeb', + 'bg-tertiary-hover': '#e5dcdc', 'text-primary': '#252222', 'text-muted': '#706666', + 'border-color': '#d6cdcd', 'accent': '#2174ab', 'accent-hover': '#195780', + }, + 'pearl': { + 'bg-primary': '#f2f1f4', 'bg-secondary': '#ffffff', 'bg-tertiary': '#ecebef', + 'bg-tertiary-hover': '#dfdce5', 'text-primary': '#232225', 'text-muted': '#696670', + 'border-color': '#d0cdd6', 'accent': '#bd7c3d', 'accent-hover': '#966331', + }, + 'vichy': { + 'bg-primary': '#f4f1f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efebeb', + 'bg-tertiary-hover': '#e5dcdc', 'text-primary': '#252222', 'text-muted': '#706666', + 'border-color': '#d6cdcd', 'accent': '#1d968b', 'accent-hover': '#156b63', + }, + 'sorbet': { + 'bg-primary': '#f4f1f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efebeb', + 'bg-tertiary-hover': '#e5dcdc', 'text-primary': '#252222', 'text-muted': '#706666', + 'border-color': '#d6cdcd', 'accent': '#c07b59', 'accent-hover': '#a7613f', + }, + 'yacht_club': { + 'bg-primary': '#f4f1f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efebeb', + 'bg-tertiary-hover': '#e5dcdc', 'text-primary': '#252222', 'text-muted': '#706666', + 'border-color': '#d6cdcd', 'accent': '#97562b', 'accent-hover': '#6f3f20', + }, + # --- Warm (from Figma's "53 Unique Website Color Schemes") --- + 'amber_walnut_morning': { + 'bg-primary': '#292929', 'bg-secondary': '#3b3b3b', 'bg-tertiary': '#4d4d4d', + 'bg-tertiary-hover': '#545454', 'text-primary': '#e6e6e6', 'text-muted': '#9e9e9e', + 'border-color': '#525252', 'accent': '#d09070', 'accent-hover': '#c3724a', + }, + 'copper_aquamarine_dream': { + 'bg-primary': '#f4f4f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efefeb', + 'bg-tertiary-hover': '#e5e5dc', 'text-primary': '#252522', 'text-muted': '#707066', + 'border-color': '#d6d6cd', 'accent': '#bc5224', 'accent-hover': '#913f1c', + }, + 'cocoa_topaz_noonday': { + 'bg-primary': '#322620', 'bg-secondary': '#48362e', 'bg-tertiary': '#5d473c', + 'bg-tertiary-hover': '#674e42', 'text-primary': '#e7e5e4', 'text-muted': '#a49c98', + 'border-color': '#644c40', 'accent': '#f1a04e', 'accent-hover': '#ed871f', + }, + 'sandstone_aquamarine_serenity': { + 'bg-primary': '#322420', 'bg-secondary': '#48342e', 'bg-tertiary': '#5d443c', + 'bg-tertiary-hover': '#674b42', 'text-primary': '#e7e5e4', 'text-muted': '#a49b98', + 'border-color': '#644940', 'accent': '#76c9e5', 'accent-hover': '#4bb8dd', + }, + 'honey_opal_sunset': { + 'bg-primary': '#322920', 'bg-secondary': '#483b2e', 'bg-tertiary': '#5d4d3c', + 'bg-tertiary-hover': '#675442', 'text-primary': '#e7e6e4', 'text-muted': '#a49e98', + 'border-color': '#645240', 'accent': '#d6b12a', 'accent-hover': '#ac8e21', + }, + 'seashell_garnet_afternoon': { + 'bg-primary': '#f1f2f4', 'bg-secondary': '#ffffff', 'bg-tertiary': '#ebedef', + 'bg-tertiary-hover': '#dce0e5', 'text-primary': '#222425', 'text-muted': '#666b70', + 'border-color': '#cdd1d6', 'accent': '#da3e50', 'accent-hover': '#c02537', + }, + 'rose_quartz_evening': { + 'bg-primary': '#322024', 'bg-secondary': '#482e34', 'bg-tertiary': '#5d3c44', + 'bg-tertiary-hover': '#67424b', 'text-primary': '#e7e4e5', 'text-muted': '#a4989b', + 'border-color': '#644049', 'accent': '#f36868', 'accent-hover': '#ef3939', + }, + 'calcite': { + 'bg-primary': '#292929', 'bg-secondary': '#3b3b3b', 'bg-tertiary': '#4d4d4d', + 'bg-tertiary-hover': '#545454', 'text-primary': '#e6e6e6', 'text-muted': '#9e9e9e', + 'border-color': '#525252', 'accent': '#f1854e', 'accent-hover': '#ed641f', + }, + 'fireside': { + 'bg-primary': '#f4f2f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efedeb', + 'bg-tertiary-hover': '#e5e0dc', 'text-primary': '#252422', 'text-muted': '#706b66', + 'border-color': '#d6d1cd', 'accent': '#ca7226', 'accent-hover': '#9f5a1e', + }, + 'terrazzo': { + 'bg-primary': '#203232', 'bg-secondary': '#2e4848', 'bg-tertiary': '#3c5d5d', + 'bg-tertiary-hover': '#426767', 'text-primary': '#e4e7e7', 'text-muted': '#98a4a4', + 'border-color': '#406464', 'accent': '#ecad6f', 'accent-hover': '#e69342', + }, + # --- Cool (from Figma's "53 Unique Website Color Schemes") --- + 'sapphire_nightfall_whisper': { + 'bg-primary': '#202932', 'bg-secondary': '#2e3b48', 'bg-tertiary': '#3c4c5d', + 'bg-tertiary-hover': '#425467', 'text-primary': '#e4e6e7', 'text-muted': '#989ea4', + 'border-color': '#405264', 'accent': '#297fd6', 'accent-hover': '#2166ab', + }, + 'marina': { + 'bg-primary': '#f1f3f4', 'bg-secondary': '#ffffff', 'bg-tertiary': '#ebeeef', + 'bg-tertiary-hover': '#dce2e5', 'text-primary': '#222425', 'text-muted': '#666d70', + 'border-color': '#cdd3d6', 'accent': '#3e8cda', 'accent-hover': '#2572c0', + }, + 'emerald_lavender_lake': { + 'bg-primary': '#f1f4f4', 'bg-secondary': '#ffffff', 'bg-tertiary': '#ebefef', + 'bg-tertiary-hover': '#dce5e5', 'text-primary': '#222525', 'text-muted': '#667070', + 'border-color': '#cdd6d6', 'accent': '#239f58', 'accent-hover': '#1a7541', + }, + 'sage_peridot_morning': { + 'bg-primary': '#f1f4f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#ebefeb', + 'bg-tertiary-hover': '#dce5dc', 'text-primary': '#222522', 'text-muted': '#667066', + 'border-color': '#cdd6cd', 'accent': '#1f9e83', 'accent-hover': '#177360', + }, + 'amethyst_dawn_haze': { + 'bg-primary': '#242032', 'bg-secondary': '#342e48', 'bg-tertiary': '#443c5d', + 'bg-tertiary-hover': '#4b4267', 'text-primary': '#e5e4e7', 'text-muted': '#9b98a4', + 'border-color': '#494064', 'accent': '#ccba27', 'accent-hover': '#a1931f', + }, + 'turquoise_amber_autumn': { + 'bg-primary': '#322620', 'bg-secondary': '#48362e', 'bg-tertiary': '#5d473c', + 'bg-tertiary-hover': '#674e42', 'text-primary': '#e7e5e4', 'text-muted': '#a49c98', + 'border-color': '#644c40', 'accent': '#d66329', 'accent-hover': '#ab4f21', + }, + 'sapphire_ash_morning': { + 'bg-primary': '#f1f2f4', 'bg-secondary': '#ffffff', 'bg-tertiary': '#ebecef', + 'bg-tertiary-hover': '#dcdfe5', 'text-primary': '#222325', 'text-muted': '#666970', + 'border-color': '#cdd0d6', 'accent': '#356a8d', 'accent-hover': '#274e68', + }, + 'royal_glimmer': { + 'bg-primary': '#1f312d', 'bg-secondary': '#2d4741', 'bg-tertiary': '#3b5c56', + 'bg-tertiary-hover': '#41665e', 'text-primary': '#e4e7e7', 'text-muted': '#98a4a2', + 'border-color': '#3f635b', 'accent': '#29d6b4', 'accent-hover': '#21ab90', + }, + 'neptune': { + 'bg-primary': '#f1f3f4', 'bg-secondary': '#ffffff', 'bg-tertiary': '#ebeeef', + 'bg-tertiary-hover': '#dce2e5', 'text-primary': '#222425', 'text-muted': '#666d70', + 'border-color': '#cdd3d6', 'accent': '#248fb8', 'accent-hover': '#1c6e8d', + }, + # --- Vibrant & Bold (from Figma's "53 Unique Website Color Schemes") --- + 'tropical_heat': { + 'bg-primary': '#f4f3f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efeeeb', + 'bg-tertiary-hover': '#e5e3dc', 'text-primary': '#252522', 'text-muted': '#706e66', + 'border-color': '#d6d4cd', 'accent': '#ca5226', 'accent-hover': '#9f411e', + }, + 'celestial': { + 'bg-primary': '#f4f4f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efefeb', + 'bg-tertiary-hover': '#e5e4dc', 'text-primary': '#252522', 'text-muted': '#707066', + 'border-color': '#d6d5cd', 'accent': '#918b1c', 'accent-hover': '#666214', + }, + 'festive_eve': { + 'bg-primary': '#f2f1f4', 'bg-secondary': '#ffffff', 'bg-tertiary': '#ecebef', + 'bg-tertiary-hover': '#dfdce5', 'text-primary': '#232225', 'text-muted': '#696670', + 'border-color': '#d0cdd6', 'accent': '#733eda', 'accent-hover': '#5a25c0', + }, + 'freshly_squeezed': { + 'bg-primary': '#f4f3f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efeeeb', + 'bg-tertiary-hover': '#e5e2dc', 'text-primary': '#252422', 'text-muted': '#706d66', + 'border-color': '#d6d3cd', 'accent': '#a28320', 'accent-hover': '#776118', + }, + 'jelly_shoes': { + 'bg-primary': '#f3f1f4', 'bg-secondary': '#ffffff', 'bg-tertiary': '#eeebef', + 'bg-tertiary-hover': '#e1dce5', 'text-primary': '#242225', 'text-muted': '#6c6670', + 'border-color': '#d2cdd6', 'accent': '#9d3eda', 'accent-hover': '#8325c0', + }, + # --- Modern (from Figma's "53 Unique Website Color Schemes") --- + 'gossamer': { + 'bg-primary': '#f4f1f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efebeb', + 'bg-tertiary-hover': '#e5dcdc', 'text-primary': '#252222', 'text-muted': '#706666', + 'border-color': '#d6cdcd', 'accent': '#d95c37', 'accent-hover': '#ba4623', + }, + 'clockwork': { + 'bg-primary': '#f4f1f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efebeb', + 'bg-tertiary-hover': '#e5dcdc', 'text-primary': '#252222', 'text-muted': '#706666', + 'border-color': '#d6cdcd', 'accent': '#d26c28', 'accent-hover': '#a75620', + }, + 'lemon_granite_morning': { + 'bg-primary': '#f4f1f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efebeb', + 'bg-tertiary-hover': '#e5dcdc', 'text-primary': '#252222', 'text-muted': '#706666', + 'border-color': '#d6cdcd', 'accent': '#89831b', 'accent-hover': '#5e5a13', + }, + 'arctic_reflection': { + 'bg-primary': '#202c32', 'bg-secondary': '#2e3f48', 'bg-tertiary': '#3c525d', + 'bg-tertiary-hover': '#425a67', 'text-primary': '#e4e6e7', 'text-muted': '#98a0a4', + 'border-color': '#405864', 'accent': '#4693b9', 'accent-hover': '#387694', + }, + 'slate': { + 'bg-primary': '#f4f1f1', 'bg-secondary': '#ffffff', 'bg-tertiary': '#efebeb', + 'bg-tertiary-hover': '#e5dcdc', 'text-primary': '#252222', 'text-muted': '#706666', + 'border-color': '#d6cdcd', 'accent': '#1f9e31', 'accent-hover': '#177324', + }, + 'autumn_luxe': { + 'bg-primary': '#303020', 'bg-secondary': '#45452e', 'bg-tertiary': '#5b5b3d', + 'bg-tertiary-hover': '#646443', 'text-primary': '#e7e7e4', 'text-muted': '#a4a498', + 'border-color': '#616141', 'accent': '#c08040', 'accent-hover': '#9a6633', + }, + 'inked': { + 'bg-primary': '#171717', 'bg-secondary': '#292929', 'bg-tertiary': '#3b3b3b', + 'bg-tertiary-hover': '#424242', 'text-primary': '#e6e6e6', 'text-muted': '#9e9e9e', + 'border-color': '#404040', 'accent': '#27c9c9', 'accent-hover': '#1f9e9e', + }, + 'wraith': { + 'bg-primary': '#1c1c12', 'bg-secondary': '#323220', 'bg-tertiary': '#48482e', + 'bg-tertiary-hover': '#515134', 'text-primary': '#e7e7e4', 'text-muted': '#a4a498', + 'border-color': '#4e4e32', 'accent': '#d69c29', 'accent-hover': '#ab7d21', + }, + 'urban_nocturne': { + 'bg-primary': '#171717', 'bg-secondary': '#292929', 'bg-tertiary': '#3b3b3b', + 'bg-tertiary-hover': '#424242', 'text-primary': '#e6e6e6', 'text-muted': '#9e9e9e', + 'border-color': '#404040', 'accent': '#bfc927', 'accent-hover': '#969e1f', + }, +} + +# Groups the 53 Figma-derived theme keys above for the Settings dropdown +# (rendered as s) - matches that source page's own "Minimal and +# neutral / Warm / Cool / Vibrant and bold / Modern" section headings, so +# browsing the dropdown still mirrors how the original schemes were curated. +WEBSITE_SCHEME_CATEGORIES = { + 'Minimal & Neutral': ['ink_wash', 'jade_pebble_morning', 'woodland', 'graphite', 'urban_slate', 'pearl', 'vichy', 'sorbet', 'yacht_club'], + 'Warm': ['amber_walnut_morning', 'copper_aquamarine_dream', 'cocoa_topaz_noonday', 'sandstone_aquamarine_serenity', 'honey_opal_sunset', 'seashell_garnet_afternoon', 'rose_quartz_evening', 'calcite', 'fireside', 'terrazzo'], + 'Cool': ['sapphire_nightfall_whisper', 'marina', 'emerald_lavender_lake', 'sage_peridot_morning', 'amethyst_dawn_haze', 'turquoise_amber_autumn', 'sapphire_ash_morning', 'royal_glimmer', 'neptune'], + 'Vibrant & Bold': ['tropical_heat', 'celestial', 'festive_eve', 'freshly_squeezed', 'jelly_shoes'], + 'Modern': ['gossamer', 'clockwork', 'lemon_granite_morning', 'arctic_reflection', 'slate', 'autumn_luxe', 'inked', 'wraith', 'urban_nocturne'], } # How each choice resolves to an actual CSS value. Keeping this server-side @@ -2514,15 +2805,23 @@ def bulk_rename_apply_api(): def settings_page(): """Render the display-settings page.""" current = load_settings() - named_presets = sorted( - (t for t in SETTINGS_CHOICES['theme'] if t not in ('dark', 'light')), + # Grouped for the dropdown as s: the two base themes, the + # original hand-picked editor-inspired presets, then the Figma-derived + # website-scheme themes under their own source-page categories (see + # WEBSITE_SCHEME_CATEGORIES) - a flat 65-option list would be unusable. + website_scheme_keys = {k for keys in WEBSITE_SCHEME_CATEGORIES.values() for k in keys} + editor_presets = sorted( + SETTINGS_CHOICES['theme'] - {'dark', 'light'} - website_scheme_keys, key=lambda t: THEME_DISPLAY_NAMES[t] ) + theme_groups = [('Base', ['dark', 'light']), ('Editor Themes', editor_presets)] + for category, keys in WEBSITE_SCHEME_CATEGORIES.items(): + theme_groups.append((category, sorted(keys, key=lambda t: THEME_DISPLAY_NAMES[t]))) return render_template( 'settings.html', settings=current, default_library_path=LIBRARY_PATH, - theme_choices=['dark', 'light'] + named_presets, + theme_groups=theme_groups, theme_display_names=THEME_DISPLAY_NAMES, font_family_choices=['system', 'sans', 'serif', 'monospace', 'monaspace'], font_size_choices=['small', 'medium', 'large', 'xlarge'], diff --git a/templates/settings.html b/templates/settings.html index 2b91a13..d356b8b 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -348,8 +348,12 @@ Overall light/dark palette diff --git a/tools/generate_website_scheme_themes.py b/tools/generate_website_scheme_themes.py new file mode 100644 index 0000000..c176e06 --- /dev/null +++ b/tools/generate_website_scheme_themes.py @@ -0,0 +1,235 @@ +""" +One-off tool that generated the 53 "website scheme" theme entries in +THEME_PALETTES/THEME_DISPLAY_NAMES (offlineu_core.py), sourced from Figma's +"53 Unique Website Color Schemes" resource page: +https://www.figma.com/resource-library/website-color-schemes/ + +That page has no raw hex data - each scheme is a rendered mockup +screenshot, not a swatch grid - so website_scheme_swatches.json (checked in +alongside this script) holds dominant colors already extracted from those +53 images via canvas pixel-histogram sampling in a browser, one entry per +scheme: {n, name, colors: [{hex, pct}, ...]}. This script turns that raw +material into an actual UI palette per scheme (bg/text/accent, matching +THEME_PALETTES' shape), then nudges lightness as needed so every result +clears the same contrast bars this file's checks apply: text-vs-background, +accent-vs-background, and accent-vs-white (buttons always use white text - +see .btn in course_dashboard.html - so a too-bright accent needs catching +even when it reads fine against the background alone). + +Not a live pipeline - re-running it regenerates the exact same 53 themes +from the same frozen swatch data. To add more schemes, extract their +dominant colors the same way (see the canvas-sampling approach used +in-session; not scripted here) and append to the JSON, then rerun and +splice the output into offlineu_core.py by hand. + +Usage: python3 generate_website_scheme_themes.py +Writes generated_themes.json with the full computed palette per scheme. +""" +import colorsys +import json + + +def hex_to_rgb(h): + h = h.lstrip('#') + return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) + + +def rgb_to_hex(rgb): + return '#' + ''.join(f'{max(0, min(255, round(c))):02x}' for c in rgb) + + +def rgb_to_hsl(rgb): + r, g, b = [c / 255 for c in rgb] + h, l, s = colorsys.rgb_to_hls(r, g, b) + return h, s, l + + +def hsl_to_rgb(h, s, l): + r, g, b = colorsys.hls_to_rgb(h, l, s) + return tuple(round(c * 255) for c in (r, g, b)) + + +def set_l(h, s, l): + return hsl_to_rgb(h, s, l) + + +def relative_luminance(rgb): + def chan(c): + c = c / 255 + return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 + r, g, b = rgb + return 0.2126 * chan(r) + 0.7152 * chan(g) + 0.0722 * chan(b) + + +def contrast_ratio(rgb1, rgb2): + l1, l2 = relative_luminance(rgb1), relative_luminance(rgb2) + l1, l2 = max(l1, l2), min(l1, l2) + return (l1 + 0.05) / (l2 + 0.05) + + +def clamp(x, lo, hi): + return max(lo, min(hi, x)) + + +def slugify(name): + return name.lower().replace(' ', '_').replace("'", '') + + +DEFAULT_ACCENT_HSL = rgb_to_hsl(hex_to_rgb('#007acc')) + + +def tame_accent_saturation(s, l): + """ + Cap accent saturation to something a UI element can wear all day rather + than the fully-saturated brand colors these screenshots were sampled + from. Calibrated against the existing hand-picked themes: they only + reach full saturation out at pastel lightness (houston/night_owl, + l>=0.75) - in the mid-lightness band accents actually render in here + (~0.32-0.62, both branches below), full saturation is exactly what + reads as neon, so it gets capped harder there. + """ + if 0.32 <= l <= 0.62: + return min(s, 0.68) + return min(s, 0.85) + + +def build_theme(entry): + name = entry['name'] + swatches = entry['colors'] + parsed = [] + for sw in swatches: + rgb = hex_to_rgb(sw['hex']) + h, s, l = rgb_to_hsl(rgb) + parsed.append({'hex': sw['hex'], 'rgb': rgb, 'h': h, 's': s, 'l': l, 'pct': sw['pct']}) + + # exclude near-white "page chrome" padding, keep the scheme's real content colors + content = [p for p in parsed if p['l'] < 0.90] + if not content: + content = parsed + + darkest_l = min(p['l'] for p in content) + is_dark = darkest_l < 0.28 + + accent_candidates = [p for p in content if p['s'] > 0.25] + if accent_candidates: + accent_src = max(accent_candidates, key=lambda p: (round(p['s'], 2), p['pct'])) + ah, asat, al = accent_src['h'], accent_src['s'], accent_src['l'] + else: + ah, asat, al = DEFAULT_ACCENT_HSL + + if is_dark: + base = min(content, key=lambda p: p['l']) + bh, bs = base['h'], min(base['s'], 0.22) + bg_l = clamp(base['l'], 0.09, 0.16) + bg_primary = set_l(bh, bs, bg_l) + bg_secondary = set_l(bh, bs, clamp(bg_l + 0.07, 0.14, 0.24)) + bg_tertiary = set_l(bh, bs, clamp(bg_l + 0.14, 0.20, 0.32)) + bg_tertiary_hover = set_l(bh, bs, clamp(bg_l + 0.17, 0.22, 0.35)) + text_primary = set_l(bh, min(bs * 0.3, 0.08), 0.90) + text_muted = set_l(bh, min(bs * 0.3, 0.10), 0.62) + border_color = set_l(bh, bs, clamp(bg_l + 0.16, 0.24, 0.34)) + accent_l = clamp(al, 0.50, 0.68) + accent_s = tame_accent_saturation(max(asat, 0.45), accent_l) + accent = set_l(ah, accent_s, accent_l) + else: + light_candidates = [p for p in content if p['l'] >= 0.55] + base_h = light_candidates[0]['h'] if light_candidates else ah + bg_primary = set_l(base_h, 0.12, 0.95) + bg_secondary = (255, 255, 255) + bg_tertiary = set_l(base_h, 0.12, 0.93) + bg_tertiary_hover = set_l(base_h, 0.14, 0.88) + text_primary = set_l(base_h, 0.05, 0.14) + text_muted = set_l(base_h, 0.05, 0.42) + border_color = set_l(base_h, 0.10, 0.82) + accent_l = clamp(al, 0.38, 0.55) + accent_s = tame_accent_saturation(max(asat, 0.45), accent_l) + accent = set_l(ah, accent_s, accent_l) + + palette = { + 'bg-primary': rgb_to_hex(bg_primary), + 'bg-secondary': rgb_to_hex(bg_secondary), + 'bg-tertiary': rgb_to_hex(bg_tertiary), + 'bg-tertiary-hover': rgb_to_hex(bg_tertiary_hover), + 'text-primary': rgb_to_hex(text_primary), + 'text-muted': rgb_to_hex(text_muted), + 'border-color': rgb_to_hex(border_color), + 'accent': rgb_to_hex(accent), + } + + bgp_rgb = hex_to_rgb(palette['bg-primary']) + + tp_rgb = hex_to_rgb(palette['text-primary']) + tries = 0 + while contrast_ratio(bgp_rgb, tp_rgb) < 4.5 and tries < 30: + h, s, l = rgb_to_hsl(tp_rgb) + l = clamp(l + (0.03 if is_dark else -0.03), 0, 1) + tp_rgb = hsl_to_rgb(h, s, l) + tries += 1 + palette['text-primary'] = rgb_to_hex(tp_rgb) + + acc_rgb = hex_to_rgb(palette['accent']) + tries = 0 + while contrast_ratio(bgp_rgb, acc_rgb) < 3.0 and tries < 30: + h, s, l = rgb_to_hsl(acc_rgb) + l = clamp(l + (0.03 if is_dark else -0.03), 0, 1) + acc_rgb = hsl_to_rgb(h, s, l) + tries += 1 + # .btn always uses white text on the accent background (see + # course_dashboard.html .btn), regardless of theme - calibrated to + # 1.8 rather than a stricter WCAG bar because the existing hand-picked + # themes (ayu_dark 1.91, nord 2.00, dracula 2.41, ...) already run + # fairly loose here; this only catches genuine outliers like a + # near-white/neon accent that would be nearly illegible. + white_rgb = (255, 255, 255) + tries = 0 + while contrast_ratio(acc_rgb, white_rgb) < 1.8 and tries < 30: + h, s, l = rgb_to_hsl(acc_rgb) + l = clamp(l - 0.03, 0, 1) + acc_rgb = hsl_to_rgb(h, s, l) + tries += 1 + palette['accent'] = rgb_to_hex(acc_rgb) + + # Derived from the *final* accent (after both safety loops above), not + # the pre-adjustment value - matching every existing hand-picked theme's + # own convention (accent-hover always a bit darker than accent, e.g. + # dracula #bd93f9 -> #a672f0, light theme #007acc -> #005a9e). Building + # this from the original unadjusted accent_l would let a since-darkened + # accent end up *lighter* than its own hover state. + h, s, l = rgb_to_hsl(acc_rgb) + hover_l = clamp(l - 0.10, 0.10, 0.90) + hover_rgb = hsl_to_rgb(h, s, hover_l) + palette['accent-hover'] = rgb_to_hex(hover_rgb) + + bgs_rgb = hex_to_rgb(palette['bg-secondary']) + tm_rgb = hex_to_rgb(palette['text-muted']) + tries = 0 + while contrast_ratio(bgs_rgb, tm_rgb) < 3.0 and tries < 30: + h, s, l = rgb_to_hsl(tm_rgb) + l = clamp(l + (0.03 if is_dark else -0.03), 0, 1) + tm_rgb = hsl_to_rgb(h, s, l) + tries += 1 + palette['text-muted'] = rgb_to_hex(tm_rgb) + + return { + 'key': slugify(name), + 'display_name': name, + 'mood': 'dark' if is_dark else 'light', + 'palette': palette, + 'contrast_text_bg': round(contrast_ratio(bgp_rgb, hex_to_rgb(palette['text-primary'])), 2), + 'contrast_accent_bg': round(contrast_ratio(bgp_rgb, hex_to_rgb(palette['accent'])), 2), + } + + +if __name__ == '__main__': + data = json.load(open('website_scheme_swatches.json')) + results = [build_theme(e) for e in data] + json.dump(results, open('generated_themes.json', 'w'), indent=2) + dark_count = sum(1 for r in results if r['mood'] == 'dark') + light_count = sum(1 for r in results if r['mood'] == 'light') + print(f'Generated {len(results)} themes: {dark_count} dark, {light_count} light') + low_contrast = [r for r in results if r['contrast_text_bg'] < 4.5 or r['contrast_accent_bg'] < 3.0] + print(f'Themes still under contrast targets after adjustment: {len(low_contrast)}') + for r in low_contrast: + print(' ', r['key'], r['contrast_text_bg'], r['contrast_accent_bg']) + for r in results: + print(r['key'].ljust(32), r['mood'].ljust(6), r['palette']['bg-primary'], r['palette']['accent'], f"text/bg={r['contrast_text_bg']}", f"accent/bg={r['contrast_accent_bg']}") diff --git a/tools/website_scheme_swatches.json b/tools/website_scheme_swatches.json new file mode 100644 index 0000000..96f4191 --- /dev/null +++ b/tools/website_scheme_swatches.json @@ -0,0 +1 @@ +[{"n":1,"name":"Ink wash","colors":[{"hex":"#ffffff","pct":30},{"hex":"#808080","pct":16.7},{"hex":"#d0d0d0","pct":16.3},{"hex":"#505050","pct":15.9},{"hex":"#202020","pct":14.3},{"hex":"#f0f0f0","pct":1.7},{"hex":"#303030","pct":1.4},{"hex":"#a0a0a0","pct":0.8}]},{"n":2,"name":"Neutral elegance","colors":[{"hex":"#ffffff","pct":26.4},{"hex":"#ffe0c0","pct":15.3},{"hex":"#d0c0b0","pct":15.3},{"hex":"#605030","pct":12.5},{"hex":"#a08060","pct":10.9},{"hex":"#705030","pct":2.4},{"hex":"#a08070","pct":2.4},{"hex":"#fffff0","pct":1.9}]},{"n":3,"name":"Jade pebble morning","colors":[{"hex":"#ffffff","pct":27.8},{"hex":"#405040","pct":20.5},{"hex":"#708080","pct":9.8},{"hex":"#c0d0b0","pct":7.9},{"hex":"#f0f0f0","pct":7.9},{"hex":"#809070","pct":7.1},{"hex":"#e0e0e0","pct":2.5},{"hex":"#80a070","pct":1.5}]},{"n":4,"name":"Woodland","colors":[{"hex":"#ffffff","pct":29.5},{"hex":"#505030","pct":20.5},{"hex":"#a0a0a0","pct":9.7},{"hex":"#b0d020","pct":9.5},{"hex":"#d0b0a0","pct":9.4},{"hex":"#a07060","pct":8.9},{"hex":"#f0f0f0","pct":1.8},{"hex":"#b0b0b0","pct":0.8}]},{"n":5,"name":"Driftwood pearl morning","colors":[{"hex":"#ffffff","pct":26.7},{"hex":"#d0d0d0","pct":19},{"hex":"#7090a0","pct":9.2},{"hex":"#c08070","pct":8.5},{"hex":"#603030","pct":8.5},{"hex":"#e0a0a0","pct":8.5},{"hex":"#d0d0c0","pct":2.4},{"hex":"#e0a090","pct":1.7}]},{"n":6,"name":"Graphite","colors":[{"hex":"#ffffff","pct":30},{"hex":"#f0c0c0","pct":16.6},{"hex":"#c0c0c0","pct":10.2},{"hex":"#90a0b0","pct":9.6},{"hex":"#808070","pct":9.2},{"hex":"#f0f0e0","pct":7},{"hex":"#f0f0f0","pct":3.5},{"hex":"#f0c0d0","pct":3.2}]},{"n":7,"name":"Urban slate","colors":[{"hex":"#ffffff","pct":29.7},{"hex":"#7080a0","pct":17},{"hex":"#808090","pct":8.8},{"hex":"#605050","pct":8.1},{"hex":"#f0e0f0","pct":8.1},{"hex":"#b09090","pct":6.3},{"hex":"#708090","pct":3.7},{"hex":"#b0a090","pct":3.4}]},{"n":8,"name":"Pearl","colors":[{"hex":"#ffffff","pct":30.8},{"hex":"#b0a0d0","pct":17.9},{"hex":"#e0c0a0","pct":9.3},{"hex":"#a09080","pct":8.8},{"hex":"#606060","pct":8.2},{"hex":"#f0e0e0","pct":8.1},{"hex":"#b0a0c0","pct":3.3},{"hex":"#706060","pct":2.1}]},{"n":9,"name":"Vichy","colors":[{"hex":"#ffffff","pct":45.9},{"hex":"#c0c0c0","pct":15.6},{"hex":"#00b0a0","pct":8.4},{"hex":"#808080","pct":8},{"hex":"#00b090","pct":4.1},{"hex":"#808090","pct":3.2},{"hex":"#809080","pct":2.3},{"hex":"#909090","pct":2}]},{"n":10,"name":"Sorbet","colors":[{"hex":"#ffffff","pct":41.8},{"hex":"#d0d0d0","pct":11.1},{"hex":"#f0f0f0","pct":10.5},{"hex":"#c0a090","pct":8.7},{"hex":"#e0e0d0","pct":6.6},{"hex":"#b0c090","pct":3.2},{"hex":"#c0c0a0","pct":3},{"hex":"#c0c090","pct":2.2}]},{"n":11,"name":"Frozen mist","colors":[{"hex":"#ffffff","pct":29.2},{"hex":"#e07010","pct":20},{"hex":"#808070","pct":9.5},{"hex":"#e0e0e0","pct":9},{"hex":"#ffffd0","pct":7.9},{"hex":"#b0b0b0","pct":6.8},{"hex":"#b0b0a0","pct":3.6},{"hex":"#f0f0f0","pct":1.1}]},{"n":12,"name":"Yacht club","colors":[{"hex":"#ffffff","pct":29.8},{"hex":"#f0f0f0","pct":16.6},{"hex":"#c0c0c0","pct":15.4},{"hex":"#704020","pct":14.8},{"hex":"#206070","pct":14},{"hex":"#b0c0c0","pct":0.9},{"hex":"#306070","pct":0.8},{"hex":"#306060","pct":0.8}]},{"n":13,"name":"Amber walnut morning","colors":[{"hex":"#ffffff","pct":27.4},{"hex":"#504040","pct":18.9},{"hex":"#f0f0f0","pct":10.6},{"hex":"#d0b0a0","pct":9},{"hex":"#c07040","pct":9},{"hex":"#d09070","pct":5.6},{"hex":"#c09070","pct":4.1},{"hex":"#404040","pct":1.4}]},{"n":14,"name":"Copper aquamarine dream","colors":[{"hex":"#ffffff","pct":25.8},{"hex":"#c0c0b0","pct":9.4},{"hex":"#d08050","pct":9.2},{"hex":"#508090","pct":9.2},{"hex":"#305060","pct":8.6},{"hex":"#e0b090","pct":7.1},{"hex":"#c05020","pct":5.5},{"hex":"#c05030","pct":2.6}]},{"n":15,"name":"Cocoa topaz noonday","colors":[{"hex":"#ffffff","pct":26.8},{"hex":"#604030","pct":19.8},{"hex":"#c0b0a0","pct":8.8},{"hex":"#703010","pct":8.4},{"hex":"#6080b0","pct":7.8},{"hex":"#ffa040","pct":7.7},{"hex":"#f0f0f0","pct":1.9},{"hex":"#703020","pct":1.1}]},{"n":16,"name":"Sandstone aquamarine serenity","colors":[{"hex":"#ffffff","pct":24.6},{"hex":"#b0e0e0","pct":19.9},{"hex":"#e0b0a0","pct":9.6},{"hex":"#603020","pct":9},{"hex":"#c07050","pct":8.9},{"hex":"#305050","pct":8.5},{"hex":"#f0f0f0","pct":1.5},{"hex":"#b0e0f0","pct":1.3}]},{"n":17,"name":"Honey opal sunset","colors":[{"hex":"#ffffff","pct":26.3},{"hex":"#504030","pct":19.8},{"hex":"#f0d080","pct":9.2},{"hex":"#a08010","pct":8.8},{"hex":"#d0c0a0","pct":8.1},{"hex":"#f0c010","pct":7.3},{"hex":"#ffffe0","pct":1.2},{"hex":"#f0c020","pct":1.2}]},{"n":18,"name":"Seashell garnet afternoon","colors":[{"hex":"#ffffff","pct":25.4},{"hex":"#5080a0","pct":9.5},{"hex":"#b0c0d0","pct":9.2},{"hex":"#d090a0","pct":8.5},{"hex":"#f0d090","pct":8},{"hex":"#10a0a0","pct":7},{"hex":"#ff7080","pct":6.2},{"hex":"#00a0a0","pct":2.7}]},{"n":19,"name":"Rose quartz evening","colors":[{"hex":"#ffffff","pct":26.8},{"hex":"#602030","pct":14.5},{"hex":"#e0e0e0","pct":14.4},{"hex":"#ff9090","pct":14.1},{"hex":"#b04040","pct":13.8},{"hex":"#f0f0f0","pct":1.4},{"hex":"#f09090","pct":1.3},{"hex":"#b04050","pct":1}]},{"n":20,"name":"Calcite","colors":[{"hex":"#ffffff","pct":29.8},{"hex":"#404040","pct":14.8},{"hex":"#e0e0e0","pct":14.6},{"hex":"#f0c0a0","pct":13.8},{"hex":"#ff8040","pct":13.7},{"hex":"#f08050","pct":1.6},{"hex":"#e0c0a0","pct":1},{"hex":"#f0f0f0","pct":1}]},{"n":21,"name":"Fireside","colors":[{"hex":"#ffffff","pct":29.1},{"hex":"#e08030","pct":9.7},{"hex":"#704030","pct":7.9},{"hex":"#e0d0c0","pct":7.4},{"hex":"#902010","pct":7.3},{"hex":"#c02010","pct":5.9},{"hex":"#e07010","pct":4.7},{"hex":"#b02010","pct":3.6}]},{"n":22,"name":"Terrazzo","colors":[{"hex":"#ffffff","pct":29.4},{"hex":"#b08050","pct":19.5},{"hex":"#d08020","pct":9.6},{"hex":"#e0d0c0","pct":8.8},{"hex":"#f0c090","pct":8.2},{"hex":"#305050","pct":5.1},{"hex":"#405050","pct":3.8},{"hex":"#f0c0a0","pct":1.3}]},{"n":23,"name":"Sapphire nightfall whisper","colors":[{"hex":"#ffffff","pct":25.3},{"hex":"#203040","pct":10.1},{"hex":"#b0c0f0","pct":9},{"hex":"#004080","pct":8.8},{"hex":"#304050","pct":8.4},{"hex":"#0070c0","pct":7},{"hex":"#5080b0","pct":6.7},{"hex":"#5070b0","pct":2.4}]},{"n":24,"name":"Lapis velvet evening","colors":[{"hex":"#ffffff","pct":27.5},{"hex":"#d0d0d0","pct":9.5},{"hex":"#603070","pct":9.2},{"hex":"#204080","pct":9},{"hex":"#f0e0d0","pct":8.5},{"hex":"#002050","pct":8},{"hex":"#903070","pct":7.4},{"hex":"#803070","pct":2.8}]},{"n":25,"name":"Marina","colors":[{"hex":"#ffffff","pct":30.3},{"hex":"#fff0f0","pct":15.4},{"hex":"#805030","pct":14.5},{"hex":"#306080","pct":14.4},{"hex":"#b0d0e0","pct":13},{"hex":"#b0d0f0","pct":1.6},{"hex":"#c0d0e0","pct":0.8},{"hex":"#406070","pct":0.8}]},{"n":26,"name":"Emerald lavender lake","colors":[{"hex":"#ffffff","pct":26.5},{"hex":"#209050","pct":18.9},{"hex":"#90e0e0","pct":18.3},{"hex":"#906090","pct":17.6},{"hex":"#a0e0e0","pct":2.8},{"hex":"#806090","pct":2.8},{"hex":"#f0fff0","pct":1.2},{"hex":"#f0ffff","pct":0.9}]},{"n":27,"name":"Sage peridot morning","colors":[{"hex":"#ffffff","pct":26.5},{"hex":"#306030","pct":14.4},{"hex":"#a0b050","pct":12.7},{"hex":"#90d090","pct":11.6},{"hex":"#a0f0e0","pct":8.9},{"hex":"#b0f0e0","pct":6.5},{"hex":"#a0b060","pct":2.1},{"hex":"#a0d090","pct":2}]},{"n":28,"name":"Amethyst dawn haze","colors":[{"hex":"#ffffff","pct":26.7},{"hex":"#e0d040","pct":19.8},{"hex":"#c0b0f0","pct":9.6},{"hex":"#d0a0b0","pct":8.8},{"hex":"#302070","pct":7.6},{"hex":"#403060","pct":6.6},{"hex":"#302060","pct":2.1},{"hex":"#503060","pct":2}]},{"n":29,"name":"Moon dust","colors":[{"hex":"#ffffff","pct":29.3},{"hex":"#d0d0ff","pct":15.9},{"hex":"#90c0e0","pct":15},{"hex":"#80b0ff","pct":13.9},{"hex":"#d0b0ff","pct":13.8},{"hex":"#f0ffff","pct":1.3},{"hex":"#a0c0e0","pct":1},{"hex":"#80b0f0","pct":0.9}]},{"n":30,"name":"Turquoise amber autumn","colors":[{"hex":"#ffffff","pct":25.8},{"hex":"#602010","pct":17.7},{"hex":"#305060","pct":9.5},{"hex":"#a0d0d0","pct":8.7},{"hex":"#e05010","pct":8.6},{"hex":"#208090","pct":5.9},{"hex":"#602000","pct":2.3},{"hex":"#207090","pct":2}]},{"n":31,"name":"Sapphire ash morning","colors":[{"hex":"#ffffff","pct":26.7},{"hex":"#f0f0f0","pct":10.5},{"hex":"#a0b0d0","pct":9},{"hex":"#e0b0b0","pct":8.9},{"hex":"#b06060","pct":8.9},{"hex":"#306080","pct":8.4},{"hex":"#90a0a0","pct":7},{"hex":"#90a090","pct":2.7}]},{"n":32,"name":"Frosted aura","colors":[{"hex":"#ffffff","pct":45.9},{"hex":"#a0a0a0","pct":16},{"hex":"#608090","pct":15.7},{"hex":"#d0e0e0","pct":14.8},{"hex":"#e0e0e0","pct":1.6},{"hex":"#b0b0b0","pct":0.9},{"hex":"#f0f0f0","pct":0.9},{"hex":"#909090","pct":0.6}]},{"n":33,"name":"Royal glimmer","colors":[{"hex":"#ffffff","pct":27.9},{"hex":"#803050","pct":20.2},{"hex":"#b08050","pct":8.9},{"hex":"#005040","pct":8.3},{"hex":"#d0a050","pct":7.8},{"hex":"#304080","pct":6.7},{"hex":"#204080","pct":1.5},{"hex":"#c0a050","pct":1}]},{"n":34,"name":"Neptune","colors":[{"hex":"#ffffff","pct":28.5},{"hex":"#90e0ff","pct":14.2},{"hex":"#50b0b0","pct":14.1},{"hex":"#5060ff","pct":14},{"hex":"#7090c0","pct":13.8},{"hex":"#f0ffff","pct":2.6},{"hex":"#50b0c0","pct":1.5},{"hex":"#7080d0","pct":0.8}]},{"n":35,"name":"Tropical jade sunrise","colors":[{"hex":"#ffffff","pct":26.8},{"hex":"#108080","pct":18.8},{"hex":"#ffd080","pct":9},{"hex":"#ffa080","pct":7.9},{"hex":"#20d0e0","pct":6.6},{"hex":"#a0d0b0","pct":5.1},{"hex":"#a0d0a0","pct":4.3},{"hex":"#20d0d0","pct":1.7}]},{"n":36,"name":"Amethyst mint harmony","colors":[{"hex":"#ffffff","pct":26},{"hex":"#f050c0","pct":19.3},{"hex":"#503050","pct":9.7},{"hex":"#304040","pct":9},{"hex":"#506060","pct":7.4},{"hex":"#90f090","pct":5.5},{"hex":"#90f080","pct":2.4},{"hex":"#606060","pct":2.2}]},{"n":37,"name":"Hibiscus aura","colors":[{"hex":"#ffffff","pct":27.2},{"hex":"#704070","pct":13.5},{"hex":"#f040d0","pct":12.9},{"hex":"#5050b0","pct":8.7},{"hex":"#e03020","pct":8.6},{"hex":"#e03030","pct":5},{"hex":"#6040b0","pct":2.5},{"hex":"#5040b0","pct":2.2}]},{"n":38,"name":"Ocean ruby radiance","colors":[{"hex":"#ffffff","pct":27.2},{"hex":"#0060b0","pct":19.5},{"hex":"#ff7040","pct":8.5},{"hex":"#b0e0e0","pct":8},{"hex":"#009050","pct":7.1},{"hex":"#e02070","pct":5.2},{"hex":"#d02070","pct":2.7},{"hex":"#009060","pct":1.8}]},{"n":39,"name":"Tropical heat","colors":[{"hex":"#ffffff","pct":28.6},{"hex":"#f04000","pct":14.2},{"hex":"#fff0c0","pct":13.9},{"hex":"#ffa060","pct":13.8},{"hex":"#00d0d0","pct":12.1},{"hex":"#00d0c0","pct":2.5},{"hex":"#fff0d0","pct":0.9},{"hex":"#e05010","pct":0.9}]},{"n":40,"name":"Celestial","colors":[{"hex":"#ffffff","pct":28.9},{"hex":"#fff020","pct":13.7},{"hex":"#808050","pct":13.7},{"hex":"#2020ff","pct":13.6},{"hex":"#ffc020","pct":13.1},{"hex":"#fff030","pct":1.5},{"hex":"#ffc030","pct":1.3},{"hex":"#f0c040","pct":1.2}]},{"n":41,"name":"Festive eve","colors":[{"hex":"#ffffff","pct":27.8},{"hex":"#8040ff","pct":14.5},{"hex":"#c050ff","pct":14.4},{"hex":"#20b0ff","pct":14.3},{"hex":"#2020ff","pct":12.5},{"hex":"#3020ff","pct":1.2},{"hex":"#fff0ff","pct":1},{"hex":"#a060ff","pct":0.9}]},{"n":42,"name":"Freshly squeezed","colors":[{"hex":"#ffffff","pct":26.2},{"hex":"#ffc000","pct":14.5},{"hex":"#f0d080","pct":14.2},{"hex":"#ff8000","pct":14.2},{"hex":"#ffe040","pct":13.3},{"hex":"#fffff0","pct":1.4},{"hex":"#ffe050","pct":1.3},{"hex":"#f0d070","pct":1.3}]},{"n":43,"name":"Jelly shoes","colors":[{"hex":"#ffffff","pct":29.6},{"hex":"#e0b0ff","pct":15.3},{"hex":"#c0d0ff","pct":13.4},{"hex":"#8060e0","pct":12.9},{"hex":"#e060e0","pct":10.5},{"hex":"#e070e0","pct":3.7},{"hex":"#9060e0","pct":1.6},{"hex":"#d070e0","pct":1.5}]},{"n":44,"name":"Opaline","colors":[{"hex":"#ffffff","pct":31.1},{"hex":"#f0f0f0","pct":27.1},{"hex":"#d0d0d0","pct":13.9},{"hex":"#ff6050","pct":13.3},{"hex":"#e0e0e0","pct":1.9},{"hex":"#f0f0ff","pct":1.9},{"hex":"#ff6040","pct":1.3},{"hex":"#f0f0e0","pct":1}]},{"n":45,"name":"Gossamer","colors":[{"hex":"#ffffff","pct":40.5},{"hex":"#f05020","pct":18.3},{"hex":"#d0d0d0","pct":10.7},{"hex":"#30d0d0","pct":8.9},{"hex":"#9090a0","pct":5},{"hex":"#909090","pct":4.1},{"hex":"#a0a0a0","pct":1.2},{"hex":"#e05030","pct":1.1}]},{"n":46,"name":"Clockwork","colors":[{"hex":"#ffffff","pct":38.5},{"hex":"#e06010","pct":17.9},{"hex":"#d0d0d0","pct":9.6},{"hex":"#ffa060","pct":8.1},{"hex":"#909090","pct":5.2},{"hex":"#9090a0","pct":5.1},{"hex":"#e06020","pct":1.6},{"hex":"#f0a060","pct":1.3}]},{"n":47,"name":"Lemon granite morning","colors":[{"hex":"#ffffff","pct":26.5},{"hex":"#305060","pct":15.7},{"hex":"#708090","pct":15.3},{"hex":"#c0c0c0","pct":14.2},{"hex":"#f0e010","pct":12.8},{"hex":"#f0f0f0","pct":1.5},{"hex":"#f0e000","pct":1.1},{"hex":"#909090","pct":1.1}]},{"n":48,"name":"Arctic reflection","colors":[{"hex":"#ffffff","pct":28.5},{"hex":"#708090","pct":18.7},{"hex":"#f0ffff","pct":10.1},{"hex":"#b0c0c0","pct":9.8},{"hex":"#204050","pct":9},{"hex":"#5090b0","pct":7.3},{"hex":"#608090","pct":1.2},{"hex":"#f0f0f0","pct":1.1}]},{"n":49,"name":"Slate","colors":[{"hex":"#ffffff","pct":30.9},{"hex":"#f0f0f0","pct":21.5},{"hex":"#c0c0c0","pct":9.3},{"hex":"#708070","pct":8.7},{"hex":"#80f090","pct":7.9},{"hex":"#50c050","pct":5.7},{"hex":"#50c060","pct":3.9},{"hex":"#d0d0d0","pct":1.1}]},{"n":50,"name":"Autumn luxe","colors":[{"hex":"#ffffff","pct":29.8},{"hex":"#303020","pct":15.8},{"hex":"#b0b0b0","pct":9.2},{"hex":"#c08040","pct":9.2},{"hex":"#807060","pct":8.9},{"hex":"#e0e0f0","pct":8.3},{"hex":"#303030","pct":4.7},{"hex":"#e0e0e0","pct":1.8}]},{"n":51,"name":"Inked","colors":[{"hex":"#ffffff","pct":31.3},{"hex":"#a0a0a0","pct":15.5},{"hex":"#404040","pct":10.1},{"hex":"#000000","pct":9.6},{"hex":"#e0e0e0","pct":9.4},{"hex":"#00b0b0","pct":8.5},{"hex":"#a0b0a0","pct":3.4},{"hex":"#b0b0a0","pct":1.3}]},{"n":52,"name":"Wraith","colors":[{"hex":"#ffffff","pct":29.7},{"hex":"#302000","pct":19.3},{"hex":"#e0e0e0","pct":8.5},{"hex":"#909070","pct":8.1},{"hex":"#202000","pct":7.8},{"hex":"#008060","pct":6.6},{"hex":"#008050","pct":2},{"hex":"#f0e0e0","pct":1.3}]},{"n":53,"name":"Urban nocturne","colors":[{"hex":"#ffffff","pct":28.4},{"hex":"#909090","pct":17.5},{"hex":"#101010","pct":9.6},{"hex":"#d0d0d0","pct":9.3},{"hex":"#404040","pct":8.4},{"hex":"#e0f000","pct":6.6},{"hex":"#a0a0a0","pct":3.5},{"hex":"#f0f0f0","pct":2.1}]}]