Apply your DVP design skills — color system, typography, card styling, hover effects, copy button
Day 34 of 80
You've been designing web interfaces for years. Today the Python work is done — the routes are built, the data flows correctly, Claude generates prompts. Now you make the Prompt Vault look as polished as any DVP tool you've shipped. The inline styles from Day 28 were functional. Today you replace them with a proper design system.
A polished tool gets used. An ugly tool gets abandoned. This isn't cosmetic work — it's product work.
Before touching code, make these four decisions. Write them down. Good design starts with constraints.
| Decision | Recommendation | Your Choice |
|---|---|---|
| Background color | #0a0a0a (near-black) |
|
| Card surface color | #111 or #141414 |
|
| Accent / primary color | Your DVP brand blue, or #2563eb |
|
| Body font | Inter (already loaded via Google Fonts) | |
| Code / monospace font | JetBrains Mono (already loaded) |
Each AI video platform gets its own color. Consistent, recognizable, and instantly scannable when browsing the card grid:
/* Platform tags */
.platform-tag {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 0.2rem 0.65rem;
border-radius: 20px;
}
.platform-kling { background: #1d4ed8; color: #fff; } /* Blue */
.platform-runway { background: #7c3aed; color: #fff; } /* Purple */
.platform-veo { background: #059669; color: #fff; } /* Green */
.platform-sora { background: #b45309; color: #fff; } /* Amber — add if needed */
.platform-pika { background: #be185d; color: #fff; } /* Pink — add if needed */
/* Card grid */
.prompt-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
.prompt-card {
background: #111;
border: 1px solid #1e1e1e;
border-radius: 10px;
padding: 1.25rem;
transition: border-color 0.15s, transform 0.15s;
cursor: default;
}
.prompt-card:hover {
border-color: #2a2a2a;
transform: translateY(-1px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.shot-label {
font-size: 0.8rem;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.5rem;
}
.prompt-text {
font-size: 0.9rem;
color: #bbb;
line-height: 1.6;
margin-bottom: 0.75rem;
}
.card-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
padding-top: 0.75rem;
border-top: 1px solid #1e1e1e;
}
.btn-copy {
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #888;
padding: 0.3rem 0.8rem;
border-radius: 5px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.15s;
}
.btn-copy:hover { color: #e5e5e5; border-color: #444; }
.btn-delete {
background: transparent;
border: 1px solid transparent;
color: #444;
padding: 0.3rem 0.8rem;
border-radius: 5px;
font-size: 0.8rem;
text-decoration: none;
transition: all 0.15s;
}
.btn-delete:hover { color: #ef4444; border-color: #3f1f1f; background: #1a0a0a; }
translateY(-1px) lifts the card 1 pixel on hover. Combined with the border color change, it gives tactile feedback without being distracting. Keep transforms small on data-dense UIs.
#444) until hover, where it turns red. The visual weight matches the action's danger level: it's there when you need it, invisible when you don't.
/* Primary action */
.btn-primary {
background: #2563eb;
color: white;
border: none;
padding: 0.65rem 1.5rem;
border-radius: 7px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.15s;
}
.btn-primary:hover { background: #1d4ed8; }
/* Generate button — slightly different style */
.btn-generate {
background: #1e3a8a;
color: #93c5fd;
border: 1px solid #1d4ed8;
padding: 0.65rem 1.25rem;
border-radius: 7px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
}
.btn-generate:hover { background: #1d4ed8; color: white; }
/* Save generated prompt */
.btn-save {
background: #052e16;
color: #4ade80;
border: 1px solid #166534;
padding: 0.6rem 1.25rem;
border-radius: 7px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.15s;
}
.btn-save:hover { background: #166534; color: white; }
Add this JavaScript to the bottom of index.html, just before </body>, and the corresponding button in each card's card-actions section:
<script>
function copyPrompt(btn, text) {
navigator.clipboard.writeText(text)
.then(() => {
const original = btn.textContent;
btn.textContent = "Copied!";
btn.style.color = "#4ade80";
btn.style.borderColor = "#166534";
setTimeout(() => {
btn.textContent = original;
btn.style.color = "";
btn.style.borderColor = "";
}, 1500);
})
.catch(() => {
// Fallback: select the text programmatically
const el = document.createElement("textarea");
el.value = text;
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
});
}
</script>
navigator.clipboard.writeText() — The modern Clipboard API. Returns a Promise. Only works on HTTPS or localhost — which is exactly where you're running Flask in dev.
original before changing means this works correctly even if the user clicks the button twice quickly.
.catch() fallback — Some browsers (older, non-HTTPS) don't support the Clipboard API. The fallback creates a hidden textarea, selects its text, and uses the old execCommand("copy") approach. It's less elegant but universally compatible.
In each prompt card in index.html, add the copy button using a data- attribute approach (cleaner than inline string escaping):
<div class="card-actions">
<button class="btn-copy"
data-prompt="{{ p.prompt | e }}"
onclick="copyPrompt(this, this.dataset.prompt)">
Copy
</button>
<a href="/delete/{{ loop.index0 }}"
class="btn-delete"
onclick="return confirm('Delete this prompt?')">
Delete
</a>
</div>
data-prompt="{{ p.prompt | e }}" — Stores the prompt text in a data- attribute on the button element. The | e filter (short for escape) encodes HTML special characters so quotes in the prompt don't break the attribute. Then JavaScript reads it with this.dataset.prompt — clean, no inline escaping gymnastics.
data- attribute with HTML escaping, read with dataset in JavaScript. Always safer than embedding strings directly in onclick handlers.
Toast notifications — Instead of the button changing text, pop a small "Copied!" toast in the bottom-right corner. This involves creating a <div> dynamically in JavaScript, animating it in, and removing it after 2 seconds. A nice exercise in DOM manipulation.
Keyboard shortcut for search — Add a keydown listener: pressing / focuses the search input. Standard convention on many search-heavy tools (GitHub uses it). About 5 lines of JavaScript.
Responsive mobile layout — The grid already uses auto-fill with minmax(340px, 1fr), which collapses to a single column on narrow screens. Add a media query to adjust font sizes and padding for small screens. Test by narrowing your browser window.
Empty state per filter — When filtering to a platform with no prompts, show a friendly message: "No Runway prompts yet. Generate one above." Uses {% if not prompts %} and the active_filter variable.
Day 35 is the Week 7 review. You'll consolidate everything you built this week, verify all features work together cleanly, and get a preview of Week 8: error handling, edge cases, and deploying the Prompt Vault somewhere people can actually use it.