landing / app.py
Digitalus76's picture
Add Gradio dark landing page
a4b070d unverified
Raw History Blame Contribute Delete
10 kB
"""Tri-State Media Group — Gradio landing page.
A single-page marketing site rendered with Gradio Blocks, using custom
HTML/CSS for a modern dark, gradient-hero look while Gradio handles the
web server and a couple of interactive widgets.
"""
import gradio as gr
BRAND = "Tri-State Media Group"
STATS = [
("12+", "Newsrooms & brands"),
("340M", "Monthly impressions"),
("98%", "Uptime across properties"),
("24/7", "Coverage & support"),
]
FEATURES = [
(
"Local-first reporting",
"A regional news network spanning three states, built on original "
"journalism and community trust — not aggregated filler.",
),
(
"Modern media stack",
"Broadcast, print and digital teams share one publishing pipeline, "
"so a story ships everywhere the moment it is ready.",
),
(
"Creators & partners",
"A growing roster of shows, newsletters and podcasts, with tooling "
"that lets small teams move like large ones.",
),
]
# ---------------------------------------------------------------- styles ----
CSS = """
:root {
--bg: #07070c;
--bg-soft: #0d0d17;
--panel: rgba(255, 255, 255, 0.04);
--border: rgba(255, 255, 255, 0.09);
--text: #e9e9f1;
--muted: #9a9ab0;
--brand-1: #a855f7;
--brand-2: #6366f1;
--brand-3: #22d3ee;
}
.gradio-container {
background: var(--bg) !important;
max-width: 100% !important;
padding: 0 !important;
}
/* hide the default Gradio chrome so this reads as a landing page */
.gradio-container > .main,
.gradio-container .prose,
footer { background: transparent !important; }
.tmg-page {
color: var(--text);
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
Roboto, sans-serif;
position: relative;
overflow: hidden;
isolation: isolate;
}
.tmg-hero {
position: relative;
padding: 96px 24px 80px;
text-align: center;
}
.tmg-hero::before {
content: "";
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: radial-gradient(circle at 50% 30%, #000 0%, transparent 72%);
z-index: -2;
}
.tmg-glow {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.55;
z-index: -1;
animation: tmg-float 9s ease-in-out infinite;
}
.tmg-glow.a { width: 320px; height: 320px; top: -60px; left: 8%;
background: radial-gradient(circle, var(--brand-1), transparent 70%); }
.tmg-glow.b { width: 280px; height: 280px; top: 40px; right: 10%;
background: radial-gradient(circle, var(--brand-3), transparent 70%);
animation-delay: 2s; }
.tmg-glow.c { width: 360px; height: 360px; top: 220px; left: 45%;
background: radial-gradient(circle, var(--brand-2), transparent 70%);
animation-delay: 4s; }
@keyframes tmg-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-18px); }
}
.tmg-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 15px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--panel);
color: var(--muted);
font-size: 13px;
letter-spacing: 0.02em;
backdrop-filter: blur(8px);
}
.tmg-pill .dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--brand-3);
box-shadow: 0 0 12px var(--brand-3);
}
.tmg-title {
margin: 22px auto 0;
max-width: 16ch;
font-size: clamp(2.4rem, 6vw, 4.2rem);
line-height: 1.04;
font-weight: 800;
letter-spacing: -0.02em;
}
.tmg-grad {
background: linear-gradient(100deg, var(--brand-1), var(--brand-2) 45%,
var(--brand-3));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.tmg-sub {
margin: 20px auto 0;
max-width: 54ch;
color: var(--muted);
font-size: clamp(1rem, 1.6vw, 1.18rem);
line-height: 1.6;
}
.tmg-cta {
margin-top: 34px;
display: flex;
gap: 14px;
justify-content: center;
flex-wrap: wrap;
}
.tmg-btn {
display: inline-block;
padding: 13px 26px;
border-radius: 12px;
font-weight: 600;
font-size: 15px;
text-decoration: none !important;
transition: transform 0.15s ease, box-shadow 0.2s ease, opacity 0.2s ease;
}
.tmg-btn:hover { transform: translateY(-2px); }
.tmg-btn.primary {
color: #0b0b14;
background: linear-gradient(100deg, var(--brand-1), var(--brand-3));
box-shadow: 0 12px 34px -10px rgba(168, 85, 247, 0.7);
}
.tmg-btn.ghost {
color: var(--text);
border: 1px solid var(--border);
background: var(--panel);
}
/* ---- sections ---- */
.tmg-section { padding: 26px 24px; max-width: 1080px; margin: 0 auto; }
.tmg-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-top: 8px;
}
.tmg-stat {
padding: 22px 18px;
border-radius: 16px;
border: 1px solid var(--border);
background: linear-gradient(180deg, var(--panel), transparent);
text-align: center;
}
.tmg-stat .n {
font-size: clamp(1.6rem, 3vw, 2.1rem);
font-weight: 800;
letter-spacing: -0.02em;
background: linear-gradient(100deg, var(--brand-1), var(--brand-3));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.tmg-stat .l { margin-top: 6px; color: var(--muted); font-size: 13.5px; }
.tmg-h2 {
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 700;
letter-spacing: -0.01em;
margin: 34px 0 18px;
text-align: center;
}
.tmg-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.tmg-card {
position: relative;
padding: 24px 22px;
border-radius: 18px;
border: 1px solid var(--border);
background: var(--panel);
overflow: hidden;
}
.tmg-card::after {
content: "";
position: absolute;
inset: 0;
border-radius: 18px;
padding: 1px;
background: linear-gradient(120deg, var(--brand-1), var(--brand-3));
-webkit-mask: linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
transition: opacity 0.25s ease;
}
.tmg-card:hover::after { opacity: 1; }
.tmg-card h3 { margin: 0 0 10px; font-size: 1.12rem; }
.tmg-card p { margin: 0; color: var(--muted); line-height: 1.6; font-size: 14.5px; }
.tmg-panel {
margin-top: 30px;
padding: 34px 28px;
border-radius: 22px;
border: 1px solid var(--border);
background: linear-gradient(140deg, rgba(168, 85, 247, 0.12),
rgba(34, 211, 238, 0.08));
text-align: center;
}
.tmg-panel h3 { margin: 0 0 10px; font-size: 1.35rem; }
.tmg-panel p { margin: 0 auto 20px; color: var(--muted); max-width: 50ch; }
/* ---- footer ---- */
.tmg-footer {
margin-top: 48px;
padding: 26px 24px 44px;
border-top: 1px solid var(--border);
color: var(--muted);
font-size: 13.5px;
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
max-width: 1080px;
margin-left: auto;
margin-right: auto;
}
@media (max-width: 820px) {
.tmg-stats { grid-template-columns: repeat(2, 1fr); }
.tmg-cards { grid-template-columns: 1fr; }
.tmg-hero { padding: 64px 18px 56px; }
}
@media (prefers-reduced-motion: reduce) {
.tmg-glow { animation: none; }
.tmg-btn { transition: none; }
}
"""
# ------------------------------------------------------------------ view ----
def _stat_html(number, label):
return f'<div class="tmg-stat"><div class="n">{number}</div>' \
f'<div class="l">{label}</div></div>'
def _card_html(title, body):
return f'<div class="tmg-card"><h3>{title}</h3><p>{body}</p></div>'
def hero_html():
stats = "".join(_stat_html(n, l) for n, l in STATS)
cards = "".join(_card_html(t, b) for t, b in FEATURES)
return f"""
<div class="tmg-page">
<span class="tmg-glow a"></span>
<span class="tmg-glow b"></span>
<span class="tmg-glow c"></span>
<section class="tmg-hero">
<span class="tmg-pill"><span class="dot"></span>Now live on Hugging Face</span>
<h1 class="tmg-title">Stories that move a <span class="tmg-grad">region</span>.</h1>
<p class="tmg-sub">
{BRAND} brings together local newsrooms, broadcast teams and digital
creators on one modern publishing platform.
</p>
<div class="tmg-cta">
<a class="tmg-btn primary" href="#contact">Get in touch</a>
<a class="tmg-btn ghost" href="#about">Learn more</a>
</div>
</section>
<div class="tmg-section" id="about">
<div class="tmg-stats">{stats}</div>
<h2 class="tmg-h2">Built for the way people read now</h2>
<div class="tmg-cards">{cards}</div>
<div class="tmg-panel" id="contact">
<h3>Want to work with us?</h3>
<p>Partnerships, advertising, or a story tip — drop us a line below
and the right team will get back to you.</p>
</div>
</div>
<footer class="tmg-footer">
<span>&copy; {BRAND}. All rights reserved.</span>
<span>Built with Gradio &middot; hosted on Hugging Face Spaces</span>
</footer>
</div>
"""
with gr.Blocks(css=CSS, theme=gr.themes.Base(), title=BRAND) as demo:
gr.HTML(hero_html())
with gr.Group():
gr.Markdown("### Send us a message")
with gr.Row():
name = gr.Textbox(label="Name", placeholder="Jane Doe", scale=2)
email = gr.Textbox(label="Email", placeholder="jane@example.com", scale=2)
message = gr.Textbox(
label="Message",
placeholder="Tell us what you need…",
lines=4,
)
send = gr.Button("Send", variant="primary")
out = gr.Markdown()
def _submit(n, e, m):
if not (n and e and m):
return "⚠️ Please fill in your name, email and message."
return f"✅ Thanks **{n}** — we received your message and will reply to `{e}` soon."
send.click(_submit, inputs=[name, email, message], outputs=out)
if __name__ == "__main__":
demo.launch()