feat: add Porchlight branding with logo, favicon, and redesigned CSS theme

Rebrand from FastAPI OIDC OP to Porchlight with warm amber/gold identity:
- Add doorway-with-light SVG logo and 32x32 PNG favicon
- Rewrite style.css with full design system (color tokens, spacing scale,
  typography scale, section cards, button variants, dark mode)
- Update base template with site header, logo, and favicon
- Update all page titles and FastAPI app title to Porchlight
This commit is contained in:
Johan Lundberg 2026-02-16 12:08:19 +01:00
parent e15dcc4745
commit 84e61464c7
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
8 changed files with 310 additions and 76 deletions

View file

@ -68,7 +68,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
settings = Settings() # type: ignore[call-arg]
app = FastAPI(
title="FastAPI OIDC OP",
title="Porchlight",
version="0.1.0",
docs_url="/docs" if settings.debug else None,
redoc_url=None,

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

View file

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<!-- Light / glow circle -->
<circle cx="32" cy="12" r="5" fill="#d97706"/>
<!-- Subtle rays -->
<line x1="32" y1="3" x2="32" y2="5" stroke="#d97706" stroke-width="2" stroke-linecap="round"/>
<line x1="23.5" y1="5.5" x2="25" y2="7" stroke="#d97706" stroke-width="2" stroke-linecap="round"/>
<line x1="40.5" y1="5.5" x2="39" y2="7" stroke="#d97706" stroke-width="2" stroke-linecap="round"/>
<line x1="21" y1="12" x2="23" y2="12" stroke="#d97706" stroke-width="2" stroke-linecap="round"/>
<line x1="43" y1="12" x2="41" y2="12" stroke="#d97706" stroke-width="2" stroke-linecap="round"/>
<!-- Doorway arch -->
<path d="M16 62 V32 A16 16 0 0 1 48 32 V62" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<!-- Threshold line -->
<line x1="14" y1="62" x2="50" y2="62" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 984 B

View file

@ -1,30 +1,67 @@
/* ==========================================================================
Porchlight Default Theme
========================================================================== */
/* ---------- Design tokens ---------- */
:root {
--bg: #fdfdfd;
--fg: #1a1a1a;
--accent: #2563eb;
/* Colors — light mode */
--bg: #fafaf9;
--fg: #1c1917;
--fg-muted: #78716c;
--accent: #d97706;
--accent-hover: #b45309;
--accent-fg: #fff;
--border: #d1d5db;
--surface: #f5f5f4;
--border: #d6d3d1;
--error-bg: #fef2f2;
--error-fg: #991b1b;
--error-fg: #dc2626;
--success-bg: #f0fdf4;
--success-fg: #166534;
--success-fg: #16a34a;
--radius: 0.375rem;
/* Spacing scale (4px base) */
--sp-1: 0.25rem; /* 4px */
--sp-2: 0.5rem; /* 8px */
--sp-3: 0.75rem; /* 12px */
--sp-4: 1rem; /* 16px */
--sp-5: 1.25rem; /* 20px */
--sp-6: 1.5rem; /* 24px */
--sp-8: 2rem; /* 32px */
--sp-10: 2.5rem; /* 40px */
--sp-12: 3rem; /* 48px */
/* Typography */
--font-family: system-ui, -apple-system, sans-serif;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--line-height: 1.6;
--line-height-tight: 1.25;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--fg: #e5e5e5;
--accent: #60a5fa;
--accent-fg: #111;
--border: #404040;
--error-bg: #450a0a;
--error-fg: #fca5a5;
--success-bg: #052e16;
--success-fg: #86efac;
--bg: #1c1917;
--fg: #fafaf9;
--fg-muted: #a8a29e;
--accent: #f59e0b;
--accent-hover: #fbbf24;
--accent-fg: #1c1917;
--surface: #292524;
--border: #44403c;
--error-bg: #451a1a;
--error-fg: #f87171;
--success-bg: #14532d;
--success-fg: #4ade80;
}
}
/* ---------- Reset ---------- */
*,
*::before,
*::after {
@ -32,23 +69,247 @@
}
body {
font-family: system-ui, -apple-system, sans-serif;
font-family: var(--font-family);
font-size: var(--font-size-base);
line-height: var(--line-height);
background: var(--bg);
color: var(--fg);
max-width: 40rem;
margin: 2rem auto;
padding: 0 1rem;
line-height: 1.6;
margin: 0;
padding: 0;
}
/* ---------- Layout ---------- */
.site-header {
max-width: 40rem;
margin: 0 auto;
padding: var(--sp-4) var(--sp-4) 0;
}
.site-header a {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
text-decoration: none;
color: var(--fg);
}
.site-header a:hover {
opacity: 0.8;
}
.site-logo {
width: 2rem;
height: 2rem;
flex-shrink: 0;
}
.site-title {
font-size: var(--font-size-xl);
font-weight: 600;
letter-spacing: -0.01em;
}
main {
max-width: 40rem;
margin: 0 auto;
padding: var(--sp-6) var(--sp-4) var(--sp-12);
}
/* ---------- Typography ---------- */
h1 {
font-size: var(--font-size-3xl);
font-weight: 700;
line-height: var(--line-height-tight);
margin: 0 0 var(--sp-6);
}
h2 {
font-size: var(--font-size-2xl);
font-weight: 600;
line-height: var(--line-height-tight);
margin: 0 0 var(--sp-4);
}
h3 {
font-size: var(--font-size-xl);
font-weight: 600;
line-height: var(--line-height-tight);
margin: 0 0 var(--sp-3);
}
p {
margin: 0 0 var(--sp-4);
}
small {
font-size: var(--font-size-sm);
color: var(--fg-muted);
}
/* ---------- Links ---------- */
a {
color: var(--accent);
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
a:hover {
color: var(--accent-hover);
}
/* ---------- Sections / Cards ---------- */
section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--sp-6);
margin-bottom: var(--sp-6);
}
section h2 {
margin-top: 0;
}
section h2:first-child {
margin-top: 0;
}
/* ---------- Forms ---------- */
label {
display: block;
margin-bottom: var(--sp-1);
font-weight: 500;
font-size: var(--font-size-sm);
}
input[type="text"],
input[type="password"],
input[type="email"] {
display: block;
width: 100%;
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg);
color: var(--fg);
font-family: var(--font-family);
font-size: var(--font-size-base);
line-height: var(--line-height);
margin-bottom: var(--sp-4);
transition: border-color 0.15s ease;
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus {
border-color: var(--accent);
outline: none;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 25%, transparent);
}
/* ---------- Buttons ---------- */
button,
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-4);
border: none;
border-radius: var(--radius);
background: var(--accent);
color: var(--accent-fg);
font-family: var(--font-family);
font-size: var(--font-size-base);
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s ease;
text-decoration: none;
line-height: var(--line-height-tight);
}
button:hover,
.btn:hover {
background: var(--accent-hover);
}
button:active,
.btn:active {
transform: translateY(1px);
}
/* Secondary button */
.btn-secondary {
background: transparent;
color: var(--fg);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--surface);
border-color: var(--fg-muted);
}
/* Danger button */
.btn-danger {
background: var(--error-fg);
color: #fff;
}
.btn-danger:hover {
opacity: 0.9;
}
/* ---------- Alerts ---------- */
[role="alert"] {
background: var(--error-bg);
color: var(--error-fg);
padding: var(--sp-3) var(--sp-4);
border-radius: var(--radius);
border: 1px solid color-mix(in srgb, var(--error-fg) 20%, transparent);
margin-bottom: var(--sp-4);
font-size: var(--font-size-sm);
}
[role="status"] {
background: var(--success-bg);
color: var(--success-fg);
padding: var(--sp-3) var(--sp-4);
border-radius: var(--radius);
border: 1px solid color-mix(in srgb, var(--success-fg) 20%, transparent);
margin-bottom: var(--sp-4);
font-size: var(--font-size-sm);
}
/* ---------- Lists ---------- */
ul {
padding-left: var(--sp-5);
margin: 0 0 var(--sp-4);
}
li {
margin-bottom: var(--sp-2);
}
/* ---------- Accessibility ---------- */
.skip-link {
position: absolute;
left: -9999px;
top: 0;
background: var(--accent);
color: var(--accent-fg);
padding: 0.5rem 1rem;
padding: var(--sp-2) var(--sp-4);
z-index: 100;
font-weight: 500;
}
.skip-link:focus {
@ -60,56 +321,6 @@ body {
outline-offset: 2px;
}
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
}
input[type="text"],
input[type="password"],
input[type="email"] {
display: block;
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg);
color: var(--fg);
font-size: 1rem;
margin-bottom: 1rem;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: var(--radius);
background: var(--accent);
color: var(--accent-fg);
font-size: 1rem;
cursor: pointer;
}
button:hover {
opacity: 0.9;
}
[role="alert"] {
background: var(--error-bg);
color: var(--error-fg);
padding: 0.75rem 1rem;
border-radius: var(--radius);
margin-bottom: 1rem;
}
[role="status"] {
background: var(--success-bg);
color: var(--success-fg);
padding: 0.75rem 1rem;
border-radius: var(--radius);
margin-bottom: 1rem;
}
.sr-only {
position: absolute;
width: 1px;
@ -122,6 +333,8 @@ button:hover {
border: 0;
}
/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
*,
*::before,

View file

@ -3,11 +3,18 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}FastAPI OIDC OP{% endblock %}</title>
<title>{% block title %}Porchlight{% endblock %}</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<a href="/">
<img class="site-logo" src="/static/logo.svg" alt="" width="32" height="32">
<span class="site-title">Porchlight</span>
</a>
</header>
<main id="main" tabindex="-1">
{% block content %}{% endblock %}
</main>

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Login — FastAPI OIDC OP{% endblock %}
{% block title %}Sign in — Porchlight{% endblock %}
{% block content %}
<h1>Sign in</h1>

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Credentials — FastAPI OIDC OP{% endblock %}
{% block title %}Credentials — Porchlight{% endblock %}
{% block content %}
<h1>Credentials</h1>

View file

@ -12,7 +12,7 @@ async def test_app_has_title(client: AsyncClient) -> None:
response = await client.get("/openapi.json")
assert response.status_code == 200
data = response.json()
assert data["info"]["title"] == "FastAPI OIDC OP"
assert data["info"]["title"] == "Porchlight"
async def test_app_has_repos_on_state(client: AsyncClient) -> None: