Custom Code Experiment •
French: Imperfect Tense
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>French: Imperfect Tense</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0b0b14;
--surface: #13131f;
--surface-2: #1a1a2e;
--surface-3: #22223a;
--border: rgba(255,255,255,0.07);
--border-hover: rgba(255,255,255,0.13);
--purple: #7c4dff;
--purple-dim: rgba(124,77,255,0.18);
--text-primary: #f0ecff;
--text-secondary: #9090b8;
--text-muted: #4a4a72;
--accent-front: #b39ddb;
--accent-back: #80cbc4;
}
html, body {
background: var(--bg);
font-family: 'Inter', sans-serif;
min-height: 100%;
color: var(--text-primary);
}
/* ── Header ── */
header {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
height: 56px;
background: rgba(11,11,20,0.85);
backdrop-filter: blur(12px);
border-bottom: 0.5px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
padding: 0 2rem;
}
.header-flag { font-size: 1.3rem; }
.header-title {
font-family: 'Playfair Display', serif;
font-size: 1rem; font-weight: 400;
color: var(--text-primary); letter-spacing: 0.2px;
}
.header-title em { font-style: italic; color: var(--accent-front); }
.header-count { font-size: 0.72rem; color: var(--text-muted); font-weight: 500; letter-spacing: 0.5px; }
/* ── Layout ── */
main {
min-height: 100vh;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 80px 1.5rem 3rem;
gap: 0;
}
/* ── Progress bar ── */
.progress-track {
width: min(34rem, 92vw);
height: 2px;
background: var(--surface-3);
border-radius: 2px;
margin-bottom: 2rem;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--purple);
border-radius: 2px;
transition: width 0.5s cubic-bezier(0.4,0,0.2,1);
}
/* ── Flip scene ── */
.scene {
width: min(34rem, 92vw);
height: 18rem;
perspective: 1000px;
cursor: pointer;
}
.flipper {
width: 100%; height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.65s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.flipper.flipped { transform: rotateY(180deg); }
.face {
position: absolute; inset: 0;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
border-radius: 20px;
border: 0.5px solid var(--border);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 2.5rem 3rem;
transition: border-color 0.2s;
}
.scene:hover .face { border-color: var(--border-hover); }
/* Front face */
.face-front {
background: var(--surface);
}
.face-front::before {
content: '';
position: absolute; inset: 0;
border-radius: 20px;
background: radial-gradient(ellipse at 30% 40%, rgba(124,77,255,0.06) 0%, transparent 65%);
pointer-events: none;
}
.face-label {
position: absolute; top: 1rem; left: 1.25rem;
font-size: 0.65rem; font-weight: 600; letter-spacing: 1.5px;
text-transform: uppercase; color: var(--text-muted);
}
.flip-hint {
position: absolute; top: 0.9rem; right: 1.25rem;
font-size: 0.65rem; font-weight: 500; letter-spacing: 0.5px;
color: var(--text-muted);
display: flex; align-items: center; gap: 4px;
}
.flip-hint svg { width: 11px; height: 11px; opacity: 0.5; }
.french-text {
font-family: 'Playfair Display', serif;
font-size: clamp(1.4rem, 4vw, 2rem);
font-weight: 600; color: var(--text-primary);
text-align: center; line-height: 1.35;
letter-spacing: -0.3px;
}
/* Back face */
.face-back {
background: var(--surface-2);
transform: rotateY(180deg);
}
.face-back::before {
content: '';
position: absolute; inset: 0;
border-radius: 20px;
background: radial-gradient(ellipse at 70% 60%, rgba(128,203,196,0.05) 0%, transparent 65%);
pointer-events: none;
}
.english-text {
font-size: clamp(1.2rem, 3.5vw, 1.65rem);
font-weight: 400; color: var(--text-primary);
text-align: center; line-height: 1.4;
}
.note-pill {
margin-top: 1.1rem;
font-size: 0.76rem; color: var(--text-secondary);
font-style: italic;
background: var(--surface-3);
border: 0.5px solid var(--border);
border-radius: 100px;
padding: 0.35rem 0.9rem;
text-align: center;
max-width: 90%;
}
/* ── Navigation ── */
.nav {
display: flex; align-items: center;
gap: 1rem; margin-top: 2rem;
}
.nav-btn {
display: flex; align-items: center; justify-content: center;
width: 44px; height: 44px; border-radius: 50%;
background: var(--surface);
border: 0.5px solid var(--border);
color: var(--text-secondary);
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.12s;
user-select: none;
}
.nav-btn:hover { background: var(--surface-3); border-color: var(--border-hover); color: var(--text-primary); }
.nav-btn:active { transform: scale(0.92); }
.nav-btn:disabled { opacity: 0.25; cursor: default; pointer-events: none; }
.nav-btn svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.nav-counter {
font-size: 0.8rem; font-weight: 500;
color: var(--text-muted); min-width: 3.5rem;
text-align: center; letter-spacing: 0.5px;
}
/* ── Dots ── */
.dots {
display: flex; justify-content: center; gap: 6px;
flex-wrap: wrap; max-width: min(34rem, 92vw);
margin-top: 1.5rem;
}
.dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--surface-3);
border: 0.5px solid transparent;
cursor: pointer;
transition: background 0.2s, transform 0.2s, border-color 0.2s;
}
.dot.active {
background: var(--purple);
transform: scale(1.25);
border-color: rgba(124,77,255,0.4);
}
.dot:hover:not(.active) { background: var(--text-muted); }
/* ── Keyboard hint ── */
.hint {
margin-top: 1.6rem;
font-size: 0.7rem; color: var(--text-muted);
text-align: center; letter-spacing: 0.4px;
}
kbd {
display: inline-block;
padding: 1px 5px;
border: 0.5px solid var(--border-hover);
border-radius: 4px;
font-size: 0.68rem;
color: var(--text-muted);
font-family: inherit;
}
/* ── Slide transition (between cards) ── */
@keyframes slideInRight {
from { opacity: 0; transform: translateX(32px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideInLeft {
from { opacity: 0; transform: translateX(-32px); }
to { opacity: 1; transform: translateX(0); }
}
.slide-right { animation: slideInRight 0.35s cubic-bezier(0.22,1,0.36,1); }
.slide-left { animation: slideInLeft 0.35s cubic-bezier(0.22,1,0.36,1); }
</style>
</head>
<body>
<header>
<span class="header-flag">🇫🇷</span>
<span class="header-title">French · <em>L'imparfait</em></span>
<span class="header-count" id="hcount">12 cards</span>
</header>
<main>
<div class="progress-track">
<div class="progress-fill" id="progress"></div>
</div>
<div class="scene" id="scene" role="button" aria-label="Flashcard — tap to flip" tabindex="0">
<div class="flipper" id="flipper">
<div class="face face-front">
<span class="face-label">Français</span>
<span class="flip-hint">
tap to flip
<svg viewBox="0 0 24 24"><path d="M1 4v6h6M23 20v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>
</span>
<div class="french-text" id="french-text"></div>
</div>
<div class="face face-back">
<span class="face-label">English</span>
<div class="english-text" id="english-text"></div>
<div class="note-pill" id="note-text"></div>
</div>
</div>
</div>
<nav class="nav" aria-label="Card navigation">
<button class="nav-btn" id="prev-btn" aria-label="Previous card">
<svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<span class="nav-counter" id="counter">1 / 12</span>
<button class="nav-btn" id="next-btn" aria-label="Next card">
<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</button>
</nav>
<div class="dots" id="dots" role="tablist" aria-label="Jump to card"></div>
<p class="hint">
<kbd>Space</kbd> flip · <kbd>←</kbd> <kbd>→</kbd> navigate
</p>
</main>
<script>
const CARDS = [
{ fr: "Je mangeais une pomme.", en: "I was eating an apple.", note: "Ongoing past action — used for was doing" },
{ fr: "Elle jouait au tennis chaque samedi.", en: "She used to play tennis every Saturday.", note: "chaque + time word = habitual imparfait trigger" },
{ fr: "Nous allions à la plage chaque été.", en: "We used to go to the beach every summer.", note: "aller → nous allons → stem: all-" },
{ fr: "Il prenait le train tous les matins.", en: "He used to take the train every morning.", note: "tous les matins (every morning) signals routine" },
{ fr: "Il faisait froid et le ciel était gris.", en: "It was cold and the sky was grey.", note: "Imparfait sets the scene — background description" },
{ fr: "C'était une belle journée.", en: "It was a beautiful day.", note: "être is the only irregular imparfait stem: ét-" },
{ fr: "Je lisais quand le téléphone a sonné.", en: "I was reading when the phone rang.", note: "lisais (imparfait) vs a sonné (passé composé)" },
{ fr: "Quand j'étais jeune, je parlais français.", en: "When I was young, I used to speak French.", note: "parler → nous parlons → stem: parl-" },
{ fr: "Elle finissait ses devoirs avant le dîner.", en: "She used to finish her homework before dinner.", note: "finir → nous finissons → stem: finiss-" },
{ fr: "Je savais la réponse.", en: "I knew the answer.", note: "Stative verbs (savoir, croire, aimer…) always use imparfait" },
{ fr: "Elle voulait partir tôt.", en: "She wanted to leave early.", note: "vouloir = state of wanting — not a completed event" },
{ fr: "D'habitude, il regardait la télé le soir.", en: "Usually, he watched TV in the evenings.", note: "d'habitude (usually) is a strong imparfait trigger" },
];
let idx = 0;
let flipped = false;
let direction = 1;
const flipper = document.getElementById('flipper');
const scene = document.getElementById('scene');
const frText = document.getElementById('french-text');
const enText = document.getElementById('english-text');
const noteText = document.getElementById('note-text');
const counter = document.getElementById('counter');
const progress = document.getElementById('progress');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const dotsEl = document.getElementById('dots');
function buildDots() {
dotsEl.innerHTML = '';
CARDS.forEach((_, i) => {
const d = document.createElement('button');
d.className = 'dot' + (i === 0 ? ' active' : '');
d.setAttribute('role', 'tab');
d.setAttribute('aria-label', 'Card ' + (i + 1));
d.addEventListener('click', () => goTo(i, i > idx ? 1 : -1));
dotsEl.appendChild(d);
});
}
function updateDots() {
dotsEl.querySelectorAll('.dot').forEach((d, i) => {
d.classList.toggle('active', i === idx);
});
}
function setCard(animClass) {
const card = CARDS[idx];
frText.textContent = card.fr;
enText.textContent = card.en;
noteText.textContent = card.note;
counter.textContent = (idx + 1) + ' / ' + CARDS.length;
progress.style.width = ((idx + 1) / CARDS.length * 100) + '%';
prevBtn.disabled = idx === 0;
nextBtn.disabled = idx === CARDS.length - 1;
updateDots();
scene.classList.remove('slide-right', 'slide-left');
void scene.offsetWidth;
if (animClass) scene.classList.add(animClass);
}
function goTo(newIdx, dir) {
if (newIdx < 0 || newIdx >= CARDS.length) return;
direction = dir;
idx = newIdx;
if (flipped) {
flipper.classList.remove('flipped');
flipped = false;
setTimeout(() => setCard(dir > 0 ? 'slide-right' : 'slide-left'), 340);
} else {
setCard(dir > 0 ? 'slide-right' : 'slide-left');
}
}
function flipCard() {
flipped = !flipped;
flipper.classList.toggle('flipped', flipped);
}
scene.addEventListener('click', flipCard);
scene.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); flipCard(); } });
prevBtn.addEventListener('click', e => { e.stopPropagation(); goTo(idx - 1, -1); });
nextBtn.addEventListener('click', e => { e.stopPropagation(); goTo(idx + 1, 1); });
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight') goTo(idx + 1, 1);
if (e.key === 'ArrowLeft') goTo(idx - 1, -1);
if (e.key === ' ') { e.preventDefault(); flipCard(); }
});
buildDots();
setCard(null);
</script>
</body>
</html>