"""Brand layer: design tokens, Gradio theme, CSS and reusable HTML blocks. The look: sky blue as the main colour with autumn accents (larch gold, rust) from Graubünden hiking scenery; bold grotesque headlines, square buttons, generous white space. No official logos, names or photos. Change the tokens below to rebrand the whole app. """ import html import gradio as gr # ---------------------------------------------------------------- tokens LIGHT = { "page": "#F2EFE9", # warm stone, fills the page around the content "bar": "#FFFFFF", # top bar "bg": "#FFFFFF", "surface": "#F7F9FB", "border": "#D8DEE5", "text": "#0F1B2A", "muted": "#3E4C5C", "primary": "#1A5C96", # sky blue: buttons, selection, highlights "primary_hover": "#0E3F6E", "on_primary": "#FFFFFF", "accent": "#E2A23B", # larch gold "accent_text": "#8A3F14", # autumn rust for small accents on light backgrounds "highlight": "#E8F1FA", } DARK = { "page": "#0D1621", "bar": "#142130", "bg": "#0D1621", "surface": "#16222F", "border": "#2A3A4D", "text": "#F2F5F8", "muted": "#C3CDD8", "primary": "#2569A3", "primary_hover": "#2E78B5", "on_primary": "#FFFFFF", "accent": "#E2A23B", "accent_text": "#F2B45A", # gold for small accents on dark backgrounds "highlight": "#1A3148", } FONT = "Inter" def _css_vars(tokens): return "".join(f"--al-{k.replace('_', '-')}:{v};" for k, v in tokens.items()) # ---------------------------------------------------------------- Gradio theme theme = gr.themes.Base( primary_hue=gr.themes.colors.blue, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont(FONT), "Helvetica Neue", "Helvetica", "Arial", "sans-serif"], radius_size=gr.themes.sizes.radius_none, ).set( body_background_fill=LIGHT["page"], body_background_fill_dark=DARK["page"], body_text_color=LIGHT["text"], body_text_color_dark=DARK["text"], body_text_color_subdued=LIGHT["muted"], body_text_color_subdued_dark=DARK["muted"], background_fill_primary=LIGHT["bg"], background_fill_primary_dark=DARK["bg"], background_fill_secondary=LIGHT["surface"], background_fill_secondary_dark=DARK["surface"], block_background_fill=LIGHT["surface"], block_background_fill_dark=DARK["surface"], block_border_color=LIGHT["border"], block_border_color_dark=DARK["border"], border_color_primary=LIGHT["border"], border_color_primary_dark=DARK["border"], block_label_text_color=LIGHT["text"], block_label_text_color_dark=DARK["text"], block_label_text_size="*text_md", block_label_text_weight="600", block_title_text_size="*text_md", block_title_text_weight="600", input_text_size="*text_lg", input_placeholder_color=LIGHT["muted"], input_placeholder_color_dark=DARK["muted"], checkbox_label_text_size="*text_md", button_large_text_size="*text_lg", block_title_text_color=LIGHT["text"], block_title_text_color_dark=DARK["text"], input_background_fill=LIGHT["bg"], input_background_fill_dark=DARK["bg"], input_border_color=LIGHT["border"], input_border_color_dark=DARK["border"], input_border_color_focus=LIGHT["primary"], input_border_color_focus_dark=DARK["primary"], button_primary_background_fill=LIGHT["primary"], button_primary_background_fill_dark=DARK["primary"], button_primary_background_fill_hover=LIGHT["primary_hover"], button_primary_background_fill_hover_dark=DARK["primary_hover"], button_primary_text_color=LIGHT["on_primary"], button_primary_text_color_dark=DARK["on_primary"], button_primary_border_color="transparent", button_primary_border_color_dark="transparent", button_secondary_background_fill="transparent", button_secondary_background_fill_dark="transparent", button_secondary_border_color=LIGHT["text"], button_secondary_border_color_dark=DARK["text"], button_secondary_text_color=LIGHT["text"], button_secondary_text_color_dark=DARK["text"], button_large_text_weight="700", color_accent_soft=LIGHT["highlight"], color_accent_soft_dark=DARK["highlight"], link_text_color=LIGHT["accent_text"], link_text_color_dark=DARK["accent_text"], ) # ---------------------------------------------------------------- CSS # Header palette: sky blue with autumn (larch gold, rust) hiking scenery SKY_DEEP, SKY, SKY_LIGHT = "#0E3F6E", "#1A5C96", "#2E78B5" NAVY = "#0B2540" RUST = "#8A3F14" GOLD = "#E2A23B" AMBER_TEXT = "#F7CF8E" PANORAMA_SVG = """ """ CSS = f""" :root, body {{ {_css_vars(LIGHT)} }} .dark, body.dark {{ {_css_vars(DARK)} }} body {{ background: var(--al-page) !important; }} /* full-width page: top bar and header span the window; content columns (.al-page) are centred */ .gradio-container {{ max-width: none !important; width: 100% !important; margin: 0 !important; padding: 0 !important; font-size: 17px; line-height: 1.55; -webkit-font-smoothing: antialiased; background: transparent !important; }} .gradio-container .main, .gradio-container main, .gradio-container .wrap, .gradio-container .contain {{ padding: 0 !important; margin-top: 0 !important; max-width: none !important; }} .al-page {{ width: 80% !important; max-width: 1600px !important; margin-left: auto !important; margin-right: auto !important; padding-left: 24px !important; padding-right: 24px !important; box-sizing: border-box; }} /* top bar, header and content sit flush; spacing inside the content comes from its own blocks */ .gradio-container main.contain > .column {{ gap: 0 !important; }} #al-hero-block, #al-hero-block .html-container, #al-hero-block .prose {{ padding: 0 !important; margin: 0 !important; }} /* inner gutter of full-width bands, so their content lines up with .al-page (percent = band width) */ .al-hero, #al-toolbar {{ --al-gutter: max(24px, calc((100% - min(80%, 1600px)) / 2 + 24px)); }} @media (max-width: 900px) {{ .al-page {{ width: 100% !important; }} .al-hero, #al-toolbar {{ --al-gutter: 24px; }} }} .gradio-container input::placeholder {{ opacity: .75; }} .gradio-container [role="tab"] {{ font-size: 1.05rem !important; font-weight: 700 !important; padding: 10px 18px !important; }} .gradio-container [role="tab"][aria-selected="true"] {{ color: var(--al-accent-text) !important; border-bottom: 3px solid var(--al-primary) !important; }} .gradio-container label span, .gradio-container .label-wrap span {{ font-size: 1rem; }} .gradio-container label, .gradio-container label span, .gradio-container .label-wrap, .gradio-container .label-wrap span, .gradio-container .label-wrap .icon {{ color: var(--al-text) !important; }} footer {{ display: none !important; }} /* Gradio adds the Hugging Face Space bar when the app is opened outside huggingface.co (e.g. on clerina.ch) */ #huggingface-space-header {{ display: none !important; }} /* hero: sky with autumn hiking scenery, full width (white text on blue only) */ .al-hero {{ position: relative; overflow: hidden; color: #fff; isolation: isolate; background: radial-gradient(circle at 84% 14%, rgba(246,196,106,.50), transparent 36%), linear-gradient(180deg, {SKY_DEEP} 0%, {SKY} 55%, {SKY_LIGHT} 100%); }} .al-hero-grid {{ display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, .8fr); gap: 32px; align-items: stretch; padding: 48px var(--al-gutter) 40px; }} .al-hero-text {{ align-self: center; }} .al-hero-art {{ position: relative; min-height: 270px; }} .al-panorama {{ position: absolute; left: 0; right: 0; bottom: 0; width: 100%; height: 86%; z-index: 0; }} .al-sun {{ transform-origin: 900px 95px; animation: al-pulse 6s ease-in-out infinite; }} .al-eyebrow {{ display: inline-flex; gap: 10px; align-items: center; text-transform: uppercase; letter-spacing: .16em; font-size: .75rem; font-weight: 800; margin-bottom: 12px; color: #fff; background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.4); padding: 6px 11px; }} .al-eyebrow i {{ width: 8px; height: 8px; background: {GOLD}; box-shadow: 0 0 0 4px rgba(226,162,59,.35); font-style: normal; animation: al-blink 1.6s ease-in-out infinite; }} .al-hero h1, .al-hero h1 .al-line {{ color: #fff !important; }} .al-hero h1 {{ font-size: clamp(2.1rem, 4vw, 3.3rem) !important; font-weight: 800 !important; line-height: 1.04 !important; letter-spacing: -0.03em; margin: 0 0 12px !important; color: #fff; text-shadow: 0 4px 24px rgba(11,37,64,.45); }} .al-hero h1 .al-line {{ display: block; }} .al-hero h1 .al-line.al-dim {{ color: rgba(255,255,255,.88) !important; }} .al-rot {{ display: inline-grid; vertical-align: bottom; }} .al-rot > span {{ grid-area: 1 / 1; justify-self: start; background: #fff; color: {SKY_DEEP} !important; text-shadow: none; line-height: 1.2; padding: .02em .16em .08em; margin-top: .02em; transform: rotate(-1.5deg); opacity: 0; white-space: nowrap; }} .al-hero .al-lead {{ font-size: 1.12rem !important; line-height: 1.5; max-width: 560px; margin: 0; color: #fff !important; }} .al-hero .al-lead b {{ color: #fff; border-bottom: 2px solid {GOLD}; }} .al-cta-row {{ display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: center; }} .al-cta {{ display: inline-flex; align-items: center; gap: 12px; background: #fff; color: {SKY_DEEP} !important; font-weight: 800; font-size: 1.05rem; padding: 13px 22px; text-decoration: none !important; box-shadow: 0 12px 30px rgba(11,37,64,.35); transition: transform .15s ease, box-shadow .15s ease; }} .al-cta:hover {{ transform: translateY(-2px); box-shadow: 0 16px 36px rgba(11,37,64,.45); }} .al-cta span {{ color: {SKY_DEEP} !important; transition: transform .15s ease; }} .al-cta:hover span {{ transform: translateX(4px); }} /* numbers: own band in autumn rust */ .al-hero-band {{ display: flex; align-items: center; padding: 11px var(--al-gutter); background: {RUST}; }} .al-stats {{ display: flex; flex-wrap: wrap; row-gap: 8px; }} .al-stat {{ display: flex; align-items: baseline; gap: 8px; padding: 0 22px; border-left: 1px solid rgba(255,255,255,.4); }} .al-stat:first-child {{ padding-left: 0; border-left: 0; }} .al-stat b {{ font-size: 1.5rem; font-weight: 800; line-height: 1; letter-spacing: -0.02em; color: #fff !important; }} .al-stat span {{ font-size: .8rem; text-transform: uppercase; letter-spacing: .08em; color: #fff !important; }} .al-bubble {{ position: absolute; z-index: 1; background: #fff; color: {NAVY} !important; padding: 11px 15px 10px; box-shadow: 0 16px 36px rgba(11,37,64,.35); animation: al-float 7s ease-in-out infinite; }} .al-bubble::after {{ content: ""; position: absolute; left: 22px; bottom: -12px; border: 12px solid transparent; border-top-color: #fff; border-bottom: 0; border-left: 0; }} .al-bubble b {{ color: inherit !important; display: block; font-size: 1.25rem !important; font-weight: 800; line-height: 1.1; }} .al-bubble small {{ display: block; margin-top: 4px; font-size: .72rem; font-weight: 800; text-transform: uppercase; letter-spacing: .1em; color: {SKY} !important; }} .al-bubble.b2, .al-bubble.b4 {{ background: {NAVY}; color: #fff !important; }} .al-bubble.b2::after, .al-bubble.b4::after {{ border-top-color: {NAVY}; }} .al-bubble.b2 small, .al-bubble.b4 small {{ color: {AMBER_TEXT} !important; }} .al-bubble-main {{ left: 12%; top: 18px; padding: 14px 20px 12px; }} .al-bubble-main b {{ font-size: 1.7rem !important; }} .al-bubble-main small {{ font-size: .78rem; }} .al-bubble.b3 {{ animation-delay: -4s; }} .al-bubble.b4 {{ animation-delay: -5.5s; }} .al-ticker {{ overflow: hidden; background: {NAVY}; padding: 9px 0; }} .al-ticker-track {{ display: flex; width: max-content; animation: al-marquee 90s linear infinite; }} .al-ticker-track span {{ white-space: nowrap; padding: 0 22px; font-weight: 700; font-size: 1rem; color: #fff !important; }} .al-ticker-track span em {{ font-style: normal; font-weight: 600; color: {AMBER_TEXT} !important; margin-left: 8px; font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; }} .al-ticker-track span::before {{ content: "✦"; color: {GOLD}; margin-right: 22px; }} @keyframes al-float {{ 0%, 100% {{ transform: translateY(0); }} 50% {{ transform: translateY(-12px); }} }} @keyframes al-marquee {{ to {{ transform: translateX(-50%); }} }} @keyframes al-pulse {{ 0%, 100% {{ transform: scale(1); opacity: .9; }} 50% {{ transform: scale(1.08); opacity: 1; }} }} @keyframes al-blink {{ 50% {{ opacity: .35; }} }} @media (prefers-reduced-motion: reduce) {{ .al-rot > span, .al-bubble, .al-ticker-track, .al-sun, .al-eyebrow i {{ animation: none !important; }} .al-rot > span:first-child {{ opacity: 1; }} }} @media (max-width: 900px) {{ .al-hero-grid {{ grid-template-columns: 1fr; gap: 12px; }} .al-hero-art {{ min-height: 190px; }} }} /* top bar: full width, light/dark toggle left, languages right */ #al-toolbar {{ justify-content: space-between !important; align-items: center; flex-wrap: nowrap !important; gap: 12px; padding: 8px var(--al-gutter) !important; background: var(--al-bar); border-bottom: 1px solid var(--al-border); }} #al-brand {{ flex: 1 1 auto !important; min-width: 0 !important; width: auto !important; }} .al-brand {{ display: inline-flex; align-items: center; gap: 8px; text-transform: uppercase; letter-spacing: .14em; font-size: .78rem; font-weight: 700; color: var(--al-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }} .al-brand b {{ font-weight: 900; color: var(--al-primary); }} .dark .al-brand b {{ color: #8EC3F0; }} .al-brand i {{ width: 9px; height: 9px; background: {GOLD}; flex: 0 0 auto; }} /* narrow windows: the region names shorten with "…" instead of running into the About link */ #al-brand {{ overflow: hidden; }} .al-brand {{ display: flex; max-width: 100%; }} .al-brand-sub {{ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }} #al-back-link {{ flex: 1 1 auto !important; min-width: 0 !important; width: auto !important; }} .al-back {{ font-weight: 800 !important; }} #al-theme-toggle {{ margin-left: 4px; }} #al-theme-toggle {{ flex: 0 0 auto !important; width: auto !important; min-width: 0 !important; padding: 5px 12px !important; background: transparent !important; color: var(--al-text) !important; border: 1px solid var(--al-border) !important; font-weight: 700 !important; }} #al-theme-toggle:hover {{ border-color: var(--al-text) !important; }} /* same height as the language buttons next to it (desktop and mobile) */ #al-theme-toggle {{ height: 30px !important; min-height: 30px !important; box-sizing: border-box !important; display: inline-flex !important; align-items: center; justify-content: center; align-self: center !important; }} #al-lang .wrap {{ height: 30px; box-sizing: border-box; }} #al-lang {{ flex: 0 0 auto !important; width: auto !important; min-width: 0 !important; background: transparent !important; border: 0 !important; padding: 0 !important; }} #al-lang .wrap {{ display: flex; gap: 0; flex-wrap: nowrap; border: 1px solid var(--al-border); }} #al-lang label {{ margin: 0 !important; padding: 5px 11px !important; min-height: 0 !important; cursor: pointer; background: transparent !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; }} #al-lang label, #al-lang label span {{ color: var(--al-text) !important; font-size: .82rem !important; font-weight: 800 !important; letter-spacing: .06em; }} #al-lang label:hover {{ background: var(--al-surface) !important; }} #al-lang label.selected, #al-lang label:has(input:checked) {{ background: {SKY_DEEP} !important; }} #al-lang label.selected span, #al-lang label:has(input:checked) span {{ color: #fff !important; }} .dark #al-lang label.selected, .dark #al-lang label:has(input:checked) {{ background: {SKY_LIGHT} !important; }} #al-lang input[type="radio"] {{ position: absolute; opacity: 0; width: 1px; height: 1px; }} #al-lang label span {{ margin-left: 0 !important; }} /* Gradio's built-in page navigation is replaced by our top bar */ .gradio-container > .nav-holder {{ display: none !important; }} /* top bar link (About / back) */ #al-about-link {{ flex: 0 0 auto !important; width: auto !important; min-width: 0 !important; }} #al-about-link + #al-lang {{ margin-left: 16px; }} .al-navlink {{ color: var(--al-text) !important; font-weight: 700; font-size: .9rem; text-decoration: none; padding: 5px 4px; border-bottom: 2px solid transparent; }} .al-navlink:hover, .al-navlink:focus-visible {{ border-bottom-color: var(--al-primary); outline: none; }} /* about page */ .al-about {{ max-width: 860px; margin: 32px auto 48px; color: var(--al-text); }} .al-about h1 {{ font-size: 2.4rem !important; font-weight: 800 !important; letter-spacing: -0.02em; margin: 8px 0 12px !important; }} .al-about h2 {{ font-size: 1.5rem !important; font-weight: 800 !important; margin: 40px 0 14px !important; padding-top: 18px; border-top: 1px solid var(--al-border); }} .al-about h3 {{ font-size: 1.1rem !important; font-weight: 800 !important; margin: 0 0 8px !important; }} .al-proto {{ display: inline-block; background: var(--al-highlight); border-left: 4px solid var(--al-primary); padding: 10px 14px; margin: 0; font-size: 1rem; }} .al-about-lead {{ font-size: 1.15rem; line-height: 1.6; }} .al-how {{ padding-left: 1.3em; }} .al-how li {{ margin: 0 0 14px; }} .al-how li b {{ display: block; font-size: 1.05rem; }} .al-how li p {{ margin: 2px 0 0; color: var(--al-text); }} .al-ideas {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; }} .al-idea {{ background: var(--al-bg); border: 1px solid var(--al-border); border-top: 4px solid var(--al-accent); padding: 16px 18px; }} .al-idea ul {{ margin: 0; padding-left: 1.1em; }} .al-idea li {{ margin: 0 0 8px; }} .al-credits {{ display: grid; grid-template-columns: minmax(150px, 220px) 1fr; gap: 10px 20px; margin: 0; }} .al-credits dt {{ font-weight: 800; }} .al-credits dd {{ margin: 0; }} .al-about a {{ color: var(--al-primary) !important; }} .dark .al-about a {{ color: #8EC3F0 !important; }} .al-figure {{ margin: 0; background: var(--al-bg); border: 1px solid var(--al-border); padding: 16px; }} .al-figure figcaption {{ color: var(--al-muted); font-size: .92rem; margin-top: 8px; }} .al-diagram {{ width: 100%; height: auto; display: block; font-family: inherit; }} .al-diagram text {{ text-anchor: middle; font-size: 15px; font-weight: 700; fill: var(--al-text); }} .al-diagram .d-small {{ font-size: 12px; font-weight: 500; fill: var(--al-muted); }} .al-diagram .d-lane {{ text-anchor: start; font-size: 11px; font-weight: 800; letter-spacing: .1em; fill: var(--al-muted); }} .al-diagram .d-sep {{ stroke: var(--al-border); stroke-width: 1.5; stroke-dasharray: 6 6; }} .al-diagram .d-box rect {{ stroke-width: 2; fill: var(--al-bg); }} .al-diagram .d-src rect {{ stroke: var(--al-muted); }} .al-diagram .d-build rect {{ stroke: {GOLD}; fill: rgba(226,162,59,.12); }} .al-diagram .d-data rect {{ stroke: {RUST}; fill: rgba(138,63,20,.08); }} .al-diagram .d-user rect {{ stroke: var(--al-text); }} .al-diagram .d-app rect {{ stroke: var(--al-primary); fill: var(--al-highlight); }} .al-diagram .d-line {{ stroke: var(--al-muted); stroke-width: 2; fill: none; }} .al-diagram .d-dash {{ stroke-dasharray: 5 5; }} .al-diagram .d-arrow {{ fill: var(--al-muted); }} @media (max-width: 640px) {{ .al-credits {{ grid-template-columns: 1fr; gap: 2px 0; }} .al-credits dd {{ margin-bottom: 10px; }} .al-about h1 {{ font-size: 1.9rem !important; }} }} /* step 1: town search OR device location */ #al-locate-row {{ align-items: stretch; gap: 12px; }} #al-search {{ min-width: 110px !important; background: var(--al-bg) !important; color: var(--al-primary) !important; border: 2px solid var(--al-primary) !important; font-weight: 800 !important; }} #al-search:hover {{ background: var(--al-primary) !important; color: var(--al-on-primary) !important; }} .dark #al-search {{ color: #8EC3F0 !important; border-color: #8EC3F0 !important; }} .dark #al-search:hover {{ background: var(--al-primary) !important; color: #fff !important; border-color: var(--al-primary) !important; }} #al-or {{ flex: 0 0 auto !important; width: auto !important; min-width: 0 !important; align-self: center; }} .al-or {{ display: inline-block; padding: 0 4px; font-weight: 800; font-size: 1rem; color: var(--al-muted); text-transform: uppercase; letter-spacing: .08em; }} /* idiom map above the table */ .al-map {{ margin: 4px 0 18px; }} .al-map iframe {{ display: block; width: 100%; height: 400px; border: 1px solid var(--al-border); background: var(--al-surface); }} .al-map figcaption {{ color: var(--al-muted); font-size: .88rem; margin-top: 6px; }} /* location button: Swiss red (white text) */ #al-locate {{ background: #B50024 !important; color: #fff !important; border-color: transparent !important; }} #al-locate:hover {{ background: #93001D !important; }} .dark #al-locate {{ background: #D4173F !important; }} .dark #al-locate:hover {{ background: #B50024 !important; }} /* play buttons: native = filled, computer voice = dashed outline */ .al-play {{ display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; margin-right: 8px; vertical-align: middle; cursor: pointer; font-size: .75rem; line-height: 1; padding: 0; border: 2px solid var(--al-primary); background: transparent; color: var(--al-primary) !important; }} .dark .al-play:not(:hover):not(.is-playing) {{ color: #8EC3F0 !important; border-color: #8EC3F0; }} .al-play:hover, .al-play:focus-visible {{ background: var(--al-primary); color: var(--al-on-primary) !important; outline: none; }} .al-play.is-playing {{ background: var(--al-primary); color: var(--al-on-primary) !important; animation: al-blink 1s ease-in-out infinite; }} .al-play.al-mini {{ width: 20px; height: 20px; font-size: .55rem; margin-right: 6px; cursor: default; animation: none; }} .al-table td .al-play {{ width: 26px; height: 26px; margin-right: 6px; }} /* source markers */ .al-dagger {{ color: var(--al-accent-text); font-weight: 800; margin-left: 2px; }} .al-legend {{ color: var(--al-muted); font-size: .9rem; margin: 10px 2px 0; line-height: 1.5; }} /* meaning column: translation plus German underneath */ .al-meaning b {{ display: block; font-weight: 600; }} .al-meaning small, .al-phrase small {{ display: block; color: var(--al-muted); font-size: .95rem; }} /* section titles */ .al-step {{ font-weight: 800; font-size: 1.6rem; letter-spacing: -0.01em; margin: 28px 0 12px; color: var(--al-text); }} /* idiom name in the step 2 heading, in its map colour */ .al-idiom-tag {{ padding: 0 .3em; white-space: nowrap; -webkit-box-decoration-break: clone; box-decoration-break: clone; }} #al-start {{ margin-top: 48px; }} .al-sub {{ color: var(--al-muted); font-size: 1.05rem; margin: 0 0 10px; }} /* location result */ .al-result {{ border-left: 4px solid var(--al-primary); background: var(--al-surface); padding: 18px 20px; margin: 4px 0 16px; color: var(--al-text); }} .al-result .al-idiom {{ font-size: 1.9rem; font-weight: 800; margin: 2px 0 6px; }} .al-result .al-where {{ margin: 0 0 8px; font-size: 1.05rem; }} .al-chip {{ display: inline-block; font-size: .8rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; background: {NAVY} !important; color: {AMBER_TEXT} !important; padding: 4px 9px; }} .al-tip {{ margin: 12px 0 0; padding-top: 12px; border-top: 1px solid var(--al-border); color: var(--al-text); font-size: 1.02rem; }} /* phrase cards */ .al-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }} .al-card {{ background: var(--al-bg); border: 1px solid var(--al-border); padding: 16px 18px; display: flex; gap: 14px; align-items: flex-start; color: var(--al-text); }} .al-num {{ font-weight: 800; color: var(--al-accent-text); font-size: 1.05rem; min-width: 22px; }} .al-rm {{ font-size: 1.4rem; font-weight: 700; line-height: 1.25; margin: 0 0 4px; }} .al-en {{ margin: 0; }} .al-de {{ margin: 2px 0 0; color: var(--al-muted); font-size: 1rem; }} /* all-idioms view: table with the local column highlighted */ /* keep Gradio's flex layout from stretching to the table's width, so the table scrolls inside its box */ .gradio-container, .gradio-container .main, .gradio-container .wrap, .gradio-container main, .gradio-container .column, .gradio-container .block, .gradio-container .html-container {{ min-width: 0 !important; }} .gradio-container {{ width: 100% !important; box-sizing: border-box; }} .al-table-wrap {{ max-width: 100%; overflow-x: auto; border: 1px solid var(--al-border); background: var(--al-bg); }} .al-table {{ border-collapse: collapse; width: 100%; min-width: 760px; color: var(--al-text); font-size: 1.02rem; }} .al-table th, .al-table td {{ padding: 13px 16px; text-align: left; vertical-align: top; border: 0; border-bottom: 1px solid var(--al-border); }} .al-table thead th {{ font-size: .8rem; text-transform: uppercase; letter-spacing: .06em; color: var(--al-text); font-weight: 700; vertical-align: bottom; background: var(--al-surface); }} .al-table tbody th {{ font-weight: 400; min-width: 210px; position: sticky; left: 0; background: var(--al-bg); z-index: 1; }} .al-table tbody th b {{ display: block; }} .al-table tbody th small {{ display: block; color: var(--al-muted); }} .al-phrase {{ display: flex; gap: 10px; align-items: baseline; }} .al-table thead th:first-child {{ position: sticky; left: 0; z-index: 2; }} .al-phrase .al-num {{ flex: 0 0 auto; min-width: 18px; }} .al-table td {{ font-weight: 700; }} .al-table .is-local {{ background: var(--al-highlight); border-left: 3px solid var(--al-primary); border-right: 3px solid var(--al-primary); }} .al-table thead .is-local {{ color: var(--al-accent-text); border-top: 3px solid var(--al-primary); }} .al-table tbody tr:last-child .is-local {{ border-bottom: 3px solid var(--al-primary); }} .al-table td.is-local {{ font-size: 1.12rem; }} .al-table-wrap {{ border: 0 !important; background: transparent !important; }} .al-table thead tr:not(.al-here-row) th, .al-table tbody th, .al-table tbody td {{ background-clip: padding-box; }} .al-table thead tr:not(.al-here-row) th {{ border-top: 1px solid var(--al-border); }} .al-table tbody tr td, .al-table tbody tr th {{ background-color: var(--al-bg); }} .al-table tbody td.is-local {{ background-color: var(--al-highlight); }} /* frame only around the real table, not the transparent label row (Gradio frames itself) */ .al-table {{ border: 0 !important; border-collapse: separate !important; border-spacing: 0; }} .al-table thead tr:not(.al-here-row) th:first-child, .al-table tbody tr > :first-child {{ border-left: 1px solid var(--al-border) !important; }} .al-table thead tr:not(.al-here-row) th:last-child, .al-table tbody tr > :last-child {{ border-right: 1px solid var(--al-border) !important; }} .al-table tr.al-here-row th {{ background: transparent !important; border: 0 !important; padding: 0 16px 6px !important; position: static !important; text-transform: none; letter-spacing: 0; font-size: .9rem; font-weight: 800; color: var(--al-accent-text) !important; vertical-align: bottom; white-space: nowrap; }} .al-std {{ display: block; text-transform: none; letter-spacing: 0; font-weight: 600; font-size: .78rem; color: var(--al-muted); margin-top: 2px; }} /* idiom colours (same as the map): tinted columns, colour bar on the header */ .al-swatch {{ display: inline-block; width: 12px; height: 12px; margin-right: 7px; vertical-align: -1px; border: 1px solid rgba(0,0,0,.15); }} .al-table thead th.al-col {{ box-shadow: inset 0 5px 0 var(--c); padding-top: 16px; }} .al-table tbody td.al-col {{ background-color: color-mix(in srgb, var(--c) 8%, var(--al-bg)) !important; }} .dark .al-table tbody td.al-col {{ background-color: color-mix(in srgb, var(--c) 16%, var(--al-bg)) !important; }} .al-table tbody td.al-col.is-local {{ background-color: color-mix(in srgb, var(--c) 20%, var(--al-bg)) !important; }} .dark .al-table tbody td.al-col.is-local {{ background-color: color-mix(in srgb, var(--c) 32%, var(--al-bg)) !important; }} .al-table .al-col.is-local {{ border-left: 3px solid var(--c) !important; border-right: 3px solid var(--c) !important; }} .al-table thead .al-col.is-local {{ color: var(--al-text) !important; border-top: 0 !important; box-shadow: inset 0 7px 0 var(--c); }} .al-table tbody tr:last-child .al-col.is-local {{ border-bottom: 3px solid var(--c) !important; }} /* my-idiom table (desktop) */ .al-table.al-mine {{ min-width: 0; }} .al-mine td {{ font-weight: 400; vertical-align: middle; }} .al-mine td.al-num {{ width: 36px; }} .al-mine td.al-rm {{ font-size: 1.3rem; font-weight: 700; margin: 0; }} .al-mine td.al-de {{ font-size: 1rem; margin: 0; }} .al-mine tbody tr:hover {{ background: var(--al-highlight); }} .al-mobile {{ display: none !important; }} #al-view label {{ font-size: 1.02rem; font-weight: 600; }} .al-empty {{ color: var(--al-muted); font-size: 1.05rem; padding: 28px; border: 1px dashed var(--al-border); text-align: center; }} .al-footer {{ margin-top: 28px; padding: 18px 0 8px; border-top: 1px solid var(--al-border); color: var(--al-muted); font-size: .92rem; line-height: 1.6; }} .al-footer a {{ color: var(--al-accent-text) !important; }} @media (max-width: 640px) {{ .al-hero, #al-toolbar {{ --al-gutter: 16px; }} .al-page {{ padding-left: 16px !important; padding-right: 16px !important; }} #al-locate-row {{ flex-direction: column !important; }} #al-locate-row > * {{ width: 100% !important; }} #al-or {{ text-align: center; }} .al-or::before, .al-or::after {{ content: "—"; margin: 0 10px; font-weight: 400; }} .al-hero-grid {{ padding: 32px var(--al-gutter) 28px; }} .al-hero-art {{ display: none; }} .al-eyebrow {{ letter-spacing: .08em; font-size: .68rem; }} .al-hero .al-lead {{ font-size: 1.05rem !important; }} .al-hero-band {{ padding: 10px var(--al-gutter); }} .al-stats {{ display: grid; grid-template-columns: repeat(2, 1fr); row-gap: 10px; width: 100%; }} .al-stat:nth-child(3) {{ padding-left: 0; border-left: 0; }} .al-stat {{ flex-direction: column; gap: 2px; padding: 0 12px; }} .al-stat b {{ font-size: 1.35rem; }} .al-stat span {{ font-size: .7rem; }} .al-cta {{ width: 100%; justify-content: center; }} .al-grid {{ grid-template-columns: 1fr; }} .al-desktop {{ display: none !important; }} #al-theme-toggle {{ min-width: 34px !important; width: 34px !important; padding: 0 !important; font-size: 0 !important; }} #al-about-link {{ max-width: 30px !important; }} #al-about-link + #al-lang {{ margin-left: 6px; }} #al-theme-toggle::before {{ content: "◐"; font-size: 1rem; }} #al-lang label {{ padding: 5px 6px !important; }} #al-lang label, #al-lang label span {{ font-size: .74rem !important; letter-spacing: .02em; }} .al-brand {{ letter-spacing: .08em; }} #al-brand, #al-about-link {{ padding: 0 !important; }} #al-theme-toggle {{ margin-left: 0; }} .al-brand-sub {{ display: none; }} #al-brand {{ flex: 0 0 auto !important; }} #al-about-link {{ margin-left: auto; }} #al-about-link .al-navlink {{ font-size: 0 !important; padding: 2px 2px; }} #al-about-link .al-navlink::before {{ content: "ⓘ"; font-size: 1.25rem; line-height: 1; }} /* ⓘ centred in a 30px box, on one line with the language and theme buttons */ #al-about-link {{ align-self: center !important; height: 30px !important; min-height: 0 !important; }} #al-about-link > *, #al-about-link .html-container, #al-about-link .prose {{ height: 30px !important; margin: 0 !important; padding: 0 !important; display: flex !important; align-items: center; justify-content: center; }} #al-about-link .al-navlink {{ display: inline-flex !important; align-items: center; justify-content: center; height: 30px; width: 30px; padding: 0 !important; border-bottom: 0 !important; }} #al-toolbar {{ gap: 6px !important; }} .al-navlink {{ font-size: .8rem; }} .al-map iframe {{ height: 300px; }} .al-table tbody th {{ min-width: 140px; }} .al-table th, .al-table td {{ padding: 11px 10px; }} .al-mobile {{ display: grid !important; }} }} """ def _rotation_css(max_words=6, slot=2.5): """Rotating headline word: one keyframe set per word count, so the cycle has no empty gap and each word has fully faded out before the next one comes in (otherwise a word shows over the other's white box).""" out = [] for n in range(1, max_words + 1): total = n * slot pct = lambda sec: f"{sec / total * 100:.2f}%" # noqa: E731 out.append( f".al-rot-{n} > span {{ animation: al-rotate-{n} {total}s infinite; }}\n" f"@keyframes al-rotate-{n} {{ 0% {{ opacity: 0; transform: translateY(.35em) rotate(-1.5deg); }}\n" f" {pct(.25)}, {pct(slot - .45)} {{ opacity: 1; transform: translateY(0) rotate(-1.5deg); }}\n" f" {pct(slot - .1)}, 100% {{ opacity: 0; transform: translateY(-.35em) rotate(-1.5deg); }} }}\n") out += [f".al-rot > span:nth-child({i + 1}) {{ animation-delay: {i * slot}s; }}\n" for i in range(1, max_words)] # keep the reduced-motion rule last so it wins over the animations above out.append("@media (prefers-reduced-motion: reduce) { .al-rot > span { animation: none !important; } " ".al-rot > span:first-child { opacity: 1; } }\n") return "".join(out) CSS += _rotation_css() HEAD = ( '' '' ) # Applies the saved light/dark preference on load; light is the default (the system setting is not used). # Gradio derives many colours at :root, so the dark class must sit on as well as ; # otherwise controls keep light-mode (near-black) text in dark mode. LOAD_JS = """ () => { let pref = null; try { pref = localStorage.getItem('al-theme'); } catch (e) {} const dark = pref === 'dark'; document.documentElement.classList.toggle('dark', dark); document.body.classList.toggle('dark', dark); } """ TOGGLE_JS = """ () => { const dark = !document.documentElement.classList.contains('dark'); document.documentElement.classList.toggle('dark', dark); document.body.classList.toggle('dark', dark); try { localStorage.setItem('al-theme', dark ? 'dark' : 'light'); } catch (e) {} } """ # ---------------------------------------------------------------- HTML blocks esc = html.escape def hero(eyebrow, lines, rotating, lead_html, stats, bubble, ticker): """Campaign banner: title left, artwork right, then a numbers strip and the phrase ticker. lines: headline lines; the rotating word is appended to the last one rotating: words that cycle in the headline (up to 6) lead_html: trusted HTML for the intro paragraph stats: [(value, label)], bubble: (phrase, label) shown over the artwork, ticker: [(phrase, idiom)] """ head = "".join( f'{esc(line)}' for i, line in enumerate(lines[:-1]) ) rotating = rotating[:6] rot = "".join(f"{esc(w)}" for w in rotating) head += f'{esc(lines[-1])} {rot}' stat_html = "".join(f'
{esc(v)}{esc(k)}
' for v, k in stats) bubble_html = f'
{esc(bubble[0])}{esc(bubble[1])}
' items = "".join(f"{esc(p)}{esc(n)}" for p, n in ticker) return f"""
{f'
{esc(eyebrow)}
' if eyebrow else ""}

{head}

{lead_html}

{stat_html}
""" def step(title, subtitle="", anchor=None, title_html=None): """title_html: trusted HTML used instead of the escaped title.""" sub = f'

{esc(subtitle)}

' if subtitle else "" anchor_attr = f' id="{esc(anchor)}"' if anchor else "" return f'
{title_html or esc(title)}
{sub}' def empty(text): return f'
{esc(text)}
' def footer(html_text): return f''