landing: add product screenshots — hero shot, feature thumbnails, lightbox

Five PNGs at app/static/images/ (renamed from screenshot dumps):
- dashboard.png         — full dashboard hero shot, sits below the hero CTAs
- news-feed.png         — feature-card thumbnail: auto-tagged news feed
- indicators-read.png   — feature-card thumbnail: per-group AI commentary
- strategic-log.png     — feature-card thumbnail: hourly strategic log text
- chat-with-log.png     — "More views" gallery: ask follow-ups against a log

Every screenshot is a <button class="shot"> with data-full + data-caption;
click opens an HTML5 <dialog>-based lightbox. <dialog> handles focus trap,
ESC-to-close, inert background; the backdrop click closes too. Images use
loading="lazy" so the lightbox-only ones don't block first paint.

CSS appended to cassandra.css: .shot, .shot-hero, .shots-grid, .shot__caption,
and .shot-modal (+ ::backdrop). All colours pull from the existing palette
vars so light and dark themes stay coherent.

Total image weight: ~950 KB across all five — acceptable for a marketing
landing page with lazy-loaded thumbnails.
This commit is contained in:
Giorgio Gilestro 2026-05-26 00:27:02 +02:00
parent 13ea63be5c
commit 4ded3632e9
7 changed files with 226 additions and 0 deletions

View file

@ -1724,3 +1724,150 @@ a.btn-secondary:hover { color: var(--accent); border-color: var(--accent); }
vertical-align: middle; vertical-align: middle;
user-select: none; user-select: none;
} }
/* -----------------------------------------------------------------------------
Landing-page screenshots: hero shot, thumbnails inside feature cards, gallery
strip, and a <dialog>-based lightbox. See app/templates/landing.html. */
/* All clickable screenshots are <button>s reset the default chrome so they
read as image cards, not form controls. */
.shot {
appearance: none;
-webkit-appearance: none;
background: transparent;
border: 1px solid var(--border);
padding: 0;
margin: 0;
display: block;
width: 100%;
cursor: zoom-in;
border-radius: 4px;
overflow: hidden;
transition: border-color 120ms ease, transform 120ms ease;
position: relative;
}
.shot:hover,
.shot:focus-visible {
border-color: var(--accent);
outline: none;
}
.shot:focus-visible {
box-shadow: 0 0 0 2px var(--accent);
}
.shot img {
display: block;
width: 100%;
height: auto;
}
/* Hero screenshot — sits just below the headline CTAs, full landing width. */
.shot-hero {
max-width: 960px;
margin: 0 auto 56px;
padding: 0 24px;
}
.shot--hero .shot__zoom {
position: absolute;
bottom: 10px;
right: 12px;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--bg);
background: var(--accent);
padding: 4px 9px;
border-radius: 3px;
opacity: 0.85;
pointer-events: none;
}
/* Thumbnail tucked at the bottom of each feature card. */
.feature-card__shot {
margin-top: 16px;
}
.feature-card__shot img {
max-height: 200px;
object-fit: cover;
object-position: top left;
}
/* "More views" strip — flex so we can drop in 2-3 extra shots later. */
.shots-section {
margin-top: 8px;
}
.shots-grid {
display: grid;
gap: 18px;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
margin-top: 12px;
}
.shot__caption {
padding: 10px 12px;
font-size: 12.5px;
line-height: 1.5;
color: var(--muted);
background: var(--surface);
border-top: 1px solid var(--border);
text-align: left;
}
.shot__caption strong {
display: block;
font-size: 13px;
color: var(--text);
margin-bottom: 2px;
font-family: var(--font-mono);
letter-spacing: 0.04em;
}
/* Lightbox. <dialog> handles the modal mechanics (focus trap, ESC-to-close,
inert background); we just style the surface. */
.shot-modal {
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
max-width: min(96vw, 1400px);
max-height: 94vh;
padding: 0;
border-radius: 6px;
overflow: hidden;
}
.shot-modal::backdrop {
background: rgba(0, 0, 0, 0.78);
}
.shot-modal img {
display: block;
max-width: 100%;
max-height: 80vh;
width: auto;
height: auto;
margin: 0 auto;
}
.shot-modal p {
margin: 0;
padding: 14px 22px 18px;
font-size: 13.5px;
line-height: 1.6;
color: var(--muted);
border-top: 1px solid var(--border);
background: var(--surface-2);
}
.shot-modal__close {
position: absolute;
top: 6px;
right: 8px;
background: transparent;
border: 0;
color: var(--text);
font-size: 28px;
line-height: 1;
padding: 4px 10px;
cursor: pointer;
font-family: var(--font-mono);
}
.shot-modal__close:hover,
.shot-modal__close:focus-visible {
color: var(--accent);
outline: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

View file

@ -25,6 +25,17 @@
</div> </div>
</section> </section>
<section class="shot-hero">
<button class="shot shot--hero"
data-full="{{ url_for('static', path='/images/dashboard.png') }}"
data-alt="Read the Markets dashboard"
data-caption="The dashboard. An aggregate cross-asset read at the top, hand-picked indicator groups underneath. Reading level toggle (Novice / Intermediate) flips every AI-generated panel between plain-English and terse-pro framing.">
<img src="{{ url_for('static', path='/images/dashboard.png') }}"
alt="Dashboard preview" loading="lazy">
<span class="shot__zoom" aria-hidden="true">Click to enlarge</span>
</button>
</section>
<section class="feature-grid"> <section class="feature-grid">
<div class="feature-card"> <div class="feature-card">
<div class="feature-card__tag">News, aggregated</div> <div class="feature-card__tag">News, aggregated</div>
@ -36,6 +47,13 @@
stuff is easy to find. Ingestion follows the trading calendar &mdash; stuff is easy to find. Ingestion follows the trading calendar &mdash;
off-hours stay quiet. off-hours stay quiet.
</p> </p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/news-feed.png') }}"
data-alt="News feed with auto-tagged headlines"
data-caption="The news feed. Each headline carries one or more theme tags (rates, AI, energy, geopolitics, …) so you can keep the threads you care about and mute the ones you don't. Click a tag to include; shift-click to exclude.">
<img src="{{ url_for('static', path='/images/news-feed.png') }}"
alt="News feed thumbnail" loading="lazy">
</button>
</div> </div>
<div class="feature-card"> <div class="feature-card">
@ -47,6 +65,13 @@
explains what the move <em>means</em>, not what it was. Anchored explains what the move <em>means</em>, not what it was. Anchored
in earnings, policy, valuation &mdash; not chart patterns. in earnings, policy, valuation &mdash; not chart patterns.
</p> </p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/indicators-read.png') }}"
data-alt="Indicators panel with AI commentary"
data-caption="The indicators panel. Tabs across asset classes (equity, rates, commodities, FX, bonds, …); each tab carries a one-paragraph 'read' written by the model on top of the live prices. The numbers anchor the prose so the commentary is checkable, not floating.">
<img src="{{ url_for('static', path='/images/indicators-read.png') }}"
alt="Indicators panel thumbnail" loading="lazy">
</button>
</div> </div>
<div class="feature-card"> <div class="feature-card">
@ -61,6 +86,30 @@
intermediate. This is editorial commentary on public data &mdash; intermediate. This is editorial commentary on public data &mdash;
not a forecast and not advice on any investment decision. not a forecast and not advice on any investment decision.
</p> </p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/strategic-log.png') }}"
data-alt="Strategic log — the hourly AI read"
data-caption="The strategic log. The model writes a fresh interpretation each hour, taking yesterday's draft as context so it updates rather than starts over. Sections are typed: date header, TL;DR, what moved, what to watch, system temperature.">
<img src="{{ url_for('static', path='/images/strategic-log.png') }}"
alt="Strategic log thumbnail" loading="lazy">
</button>
</div>
</section>
<section class="public-section shots-section">
<h2 class="public-section__head">More views</h2>
<div class="shots-grid">
<button class="shot"
data-full="{{ url_for('static', path='/images/chat-with-log.png') }}"
data-alt="Ask follow-up questions against any past log"
data-caption="Ask follow-up questions against any past log. The chat panel inherits the log's full context, so you can pull on a thread without re-pasting headlines or re-explaining the setup.">
<img src="{{ url_for('static', path='/images/chat-with-log.png') }}"
alt="Chat-with-log thumbnail" loading="lazy">
<div class="shot__caption">
<strong>Ask anything about a log</strong>
<span>Conversational follow-ups with the day's context loaded.</span>
</div>
</button>
</div> </div>
</section> </section>
@ -100,4 +149,34 @@
</div> </div>
</section> </section>
<dialog id="shot-modal" class="shot-modal" aria-label="Screenshot preview">
<button class="shot-modal__close" type="button" aria-label="Close">&times;</button>
<img id="shot-modal__img" alt="">
<p id="shot-modal__caption"></p>
</dialog>
<script>
(function () {
var dlg = document.getElementById('shot-modal');
var img = document.getElementById('shot-modal__img');
var cap = document.getElementById('shot-modal__caption');
if (!dlg || !dlg.showModal) return; // gracefully skip on ancient browsers
document.querySelectorAll('.shot').forEach(function (btn) {
btn.addEventListener('click', function () {
img.src = btn.dataset.full;
img.alt = btn.dataset.alt || '';
cap.textContent = btn.dataset.caption || '';
dlg.showModal();
});
});
// Backdrop click closes; clicking the image itself does not.
dlg.addEventListener('click', function (e) {
if (e.target === dlg) dlg.close();
});
dlg.querySelector('.shot-modal__close').addEventListener('click', function () {
dlg.close();
});
})();
</script>
{% endblock %} {% endblock %}