Time frame studio • present perfect practice — Construct have/has plus the supplied past participle and connect the sentence to an explicitly stated up-to-now or continuing time frame…

HTML for this example

Select the code and use your copy shortcut. Changes in this preview are temporary.

<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Time frame studio • present perfect practice</title><style>*{box-sizing:border-box}body{margin:0;background:var(--bg,#f3f7fa);color:var(--ink,#203342);font:16px/1.55 system-ui,Segoe UI,sans-serif}main{max-width:1100px;margin:auto;padding:28px 30px 50px}h1{font-size:clamp(27px,3.5vw,40px);line-height:1.18;letter-spacing:-.035em;margin:8px 0 12px}h2{font-size:21px;line-height:1.35;margin:0 0 16px}h3{font-size:18px}.eyebrow{color:var(--accent,#176783);font-size:12px;letter-spacing:.12em;font-weight:800;text-transform:uppercase}.intro,.muted,small{color:#516272}.intro{margin:0 0 24px}.panel{padding:24px;border:1px solid var(--line,#d6e0e7);border-radius:12px;background:white;margin:18px 0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:22px}.grid>*{min-width:0}.controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:16px 0}button,input,textarea,select{font:inherit}button{border:1px solid var(--accent,#176783);background:var(--accent,#176783);color:white;padding:10px 16px;min-height:44px;border-radius:7px;font-weight:700;cursor:pointer}button.secondary{background:white;color:var(--accent,#176783)}button[aria-pressed=true]{background:var(--tint,#e3f2f8);color:var(--ink,#203342);box-shadow:inset 0 0 0 2px var(--accent,#176783)}button:hover{filter:brightness(.96)}:focus-visible{outline:3px solid #d56b00;outline-offset:3px}label{display:block;margin:12px 0 5px;font-size:14px;font-weight:650}input:not([type=checkbox]),select,textarea{width:100%;padding:10px 12px;min-height:44px;border:1px solid #94a5b2;background:white;color:inherit;border-radius:7px}textarea{resize:vertical;min-height:120px}input[type=checkbox]{width:20px;height:20px;margin-right:9px}label.inline{display:flex;align-items:center;font-weight:500;gap:7px}.result{padding:24px;background:var(--tint,#e3f2f8);border:1px solid var(--line,#c8dce7);border-radius:12px}.big{font-size:clamp(30px,5vw,48px);font-weight:780;line-height:1.25;letter-spacing:-.035em;margin:10px 0}.trace{font:15px/1.7 ui-monospace,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.error{color:#a12820;font-weight:650;min-height:24px}.table-wrap{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:15px}th,td{text-align:left;border-bottom:1px solid var(--line,#d6e0e7);padding:12px 9px;vertical-align:top}th{font-size:12px;text-transform:uppercase;letter-spacing:.03em;color:#516272}code,pre,.code{font:14px/1.65 ui-monospace,Consolas,monospace}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--tint,#eef3f7);padding:16px;border-radius:6px}details{border-top:1px solid var(--line,#d6e0e7);margin:24px 0;padding-top:16px}summary{cursor:pointer;font-weight:700;min-height:44px}footer{border-top:1px solid var(--line,#d6e0e7);margin-top:26px;padding-top:16px;font-size:13px;color:#516272}.bar{height:16px;background:#dce4ea;border-radius:20px;overflow:hidden;margin:12px 0}.bar span{display:block;height:100%;background:var(--accent,#176783)}[hidden]{display:none!important}@media(max-width:720px){main{padding:20px}.grid{grid-template-columns:1fr;gap:12px}.panel,.result{padding:20px}}@media(max-width:400px){main{padding:16px}.panel,.result{padding:16px}button{padding:10px 13px}}
body{--accent:#277b70;--bg:#f7f5f0;--ink:#283a37;--tint:#e7f2ed;--line:#d4dfd8}.context-card{border-top:5px solid #277b70;background:white;padding:25px 28px;border-radius:4px}.context-card h2{font-family:Georgia,serif;font-size:clamp(22px,2.7vw,30px);font-weight:500;line-height:1.4;margin-top:10px}.timeline{width:100%;height:auto;min-height:65px;display:block}.timeline text{font:16px system-ui,sans-serif;fill:#465e58}.sentence{font-family:Georgia,serif;font-size:clamp(23px,3vw,32px);padding:10px 0 22px;line-height:1.5}.feedback{background:#edf4ef;border-left:4px solid #277b70;padding:18px;line-height:1.7}.panel{border-radius:4px}nav button{border-radius:24px}.timeline-caption{font-size:14px;color:#465e58}@media(max-width:720px){.timeline text{display:none}.timeline{min-height:45px}.context-card{padding:18px}}</style></head><body><main>
<header><span class="eyebrow">Sentence studio / context first</span><h1>Connect the past to now.</h1><p class="intro">Build the requested form from the supplied choices, then check the sentence against its time frame.</p></header>
<nav class="controls" aria-label="Practice context"><button type="button" id="nora" class="secondary">1 · Nora</button><button type="button" id="kai" class="secondary">2 · Kai</button><button type="button" id="yesterday" class="secondary">3 · Yesterday</button><button type="button" id="plural" class="secondary">4 · Nora and Kai</button></nav>
<article class="context-card"><span class="eyebrow" id="time-label">Continuing situation</span><h2 id="context">Nora started work at the library in 2024 and still works there.</h2><svg viewBox="0 0 700 95" class="timeline" role="img" id="timeline" aria-label="Work begins in 2024 and continues through now, October 13, 2026."><line x1="45" y1="35" x2="650" y2="35" stroke="#c6d9d5" stroke-width="4"/><g id="timeline-marks"></g><text x="40" y="80">Past</text><text x="490" y="80">Now · Oct 13, 2026</text></svg><p id="timeline-caption" class="timeline-caption"></p><p id="target"><b>Requested form:</b> present perfect simple using work.</p></article>
<section class="panel"><h2>Build the sentence</h2><div class="sentence" id="sentence" aria-live="polite">Nora ___ ___ at the library since 2024.</div><div class="grid"><div><label for="aux">Auxiliary / no auxiliary</label><select id="aux"><option value="">Choose…</option><option value="has">has</option><option value="have">have</option><option value="none">No auxiliary</option></select></div><div><label for="verb">Supplied verb form</label><select id="verb"><option value="">Choose…</option><option value="worked">worked</option><option value="works">works</option><option value="working">working</option></select></div></div><div class="controls"><button type="button" id="check">Check this sentence</button><button type="button" id="retry" class="secondary">Clear choices</button><button type="button" id="reset" class="secondary">Reset practice</button></div><div id="feedback" class="feedback" aria-live="polite">Choose both parts, then check the requested construction.</div></section>
<details><summary>How this practice handles alternatives</summary><p>The first, second, and fourth cards request present perfect simple. Present perfect continuous can be valid in other contexts; it is outside these specified targets. The yesterday card explicitly requests simple past.</p><p>This page checks supplied choices against authored targets. It does not grade arbitrary prose, diagnose ability, or reject US simple-past uses merely because already, just, or yet appears.</p></details>
<footer>Fictional teaching contexts as of October 13, 2026. Original cards and reviewed targets stay with each prompt. Feedback is local and not saved or collected.</footer>
</main><script>const $=id=>document.getElementById(id);const on=(id,e,fn)=>$(id).addEventListener(e,fn);const txt=(id,v)=>$(id).textContent=v;const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));const positive=x=>String(x).trim()!==''&&Number.isFinite(Number(x))&&Number(x)>0;const num=x=>Number(x.toPrecision(10)).toString();function enter(id,fn){on(id,'keydown',e=>{if(e.key==='Enter'&&!e.isComposing&&e.keyCode!==229){e.preventDefault();fn()}})}function selectText(id){$(id).focus();$(id).select()}

const cards={nora:{subject:'Nora',context:'Nora started work at the library in 2024 and still works there.',target:'present perfect simple using work',tail:'at the library since 2024.',aux:'has',verb:'worked',options:['worked','works','working'],time:'Continuing situation',reason:'The work started in 2024 and continues now.',timeline:'Work begins in 2024 and continues through now, October 13, 2026.'},kai:{subject:'Kai',context:'Kai’s experience up to now includes two workshop visits; no finished date is specified.',target:'present perfect simple using visit',tail:'the workshop twice.',aux:'has',verb:'visited',options:['visited','visits','visiting'],time:'Experience up to now',reason:'The two visits belong to experience up to now; no finished date is given.',timeline:'Two workshop visits occurred before now, within experience up to October 13, 2026.'},yesterday:{subject:'Yesterday Nora',context:'Yesterday Nora finished putting labels on two boxes. The event is explicitly finished.',target:'simple past using label',tail:'two boxes.',aux:'none',verb:'labeled',options:['labeled','labels','labeling'],time:'Finished yesterday',reason:'Yesterday is an explicitly finished past time; this contrast asks for simple past.',timeline:'The labeling is bounded in yesterday, separate from now, October 13, 2026.'},plural:{subject:'Nora and Kai',context:'Nora and Kai’s experience up to now includes two workshop visits together; no finished date is specified.',target:'present perfect simple using visit',tail:'the workshop twice.',aux:'have',verb:'visited',options:['visited','visits','visiting'],time:'Plural subject · experience up to now',reason:'Nora and Kai is a plural subject, so use have plus the supplied participle visited.',timeline:'Two shared workshop visits before now, within experience up to October 13, 2026.'}};let current='nora';
function sentence(c,a,v){return c.subject+' '+(a==='none'?'':a?a+' ':'___ ')+(v||'___')+' '+c.tail}
function gradeCard(id,a,v){const c=cards[id];if(!a||!v)return 'Unanswered: choose both the auxiliary option and the verb form.';if(a===c.aux&&v===c.verb)return 'Correct: '+sentence(c,a,v)+' '+c.reason;const form=c.aux==='none'?'no auxiliary + labeled':c.aux+' + '+c.verb;return 'Revise the requested form. Your choice: '+sentence(c,a,v)+' Target: '+c.target+'. Use '+form+'. '+c.reason+' This feedback concerns this item’s specified construction, not every possible English phrasing.'}
function preview(){txt('sentence',sentence(cards[current],$('aux').value,$('verb').value));txt('feedback','Choices changed. Check the requested construction.')}
function clear(){$('aux').value='';$('verb').value='';txt('sentence',sentence(cards[current],'',''));txt('feedback','Choose both parts, then check the requested construction.')}
function choose(id){current=id;const c=cards[id];for(const k of Object.keys(cards))$(k).setAttribute('aria-pressed',String(k===id));txt('context',c.context);txt('time-label',c.time);txt('timeline-caption',id==='nora'?'2024 → Now · October 13, 2026':id==='yesterday'?'Yesterday: completed event | Now: October 13, 2026':'Two visits before now · October 13, 2026');txt('target','Requested form: '+c.target+'.');$('verb').innerHTML='<option value="">Choose…</option>'+c.options.map(v=>'<option value="'+v+'">'+v+'</option>').join('');$('timeline').setAttribute('aria-label',c.timeline);$('timeline-marks').innerHTML=id==='nora'?'<line x1="80" y1="35" x2="640" y2="35" stroke="#277b70" stroke-width="10"/><circle cx="80" cy="35" r="8" fill="#277b70"/><path d="M625 22 L645 35 L625 48" fill="none" stroke="#277b70" stroke-width="5"/><text x="60" y="15">2024</text>':id==='yesterday'?'<rect x="290" y="22" width="115" height="26" rx="5" fill="#bd7649"/><text x="280" y="15">Yesterday</text>':'<line x1="80" y1="35" x2="640" y2="35" stroke="#277b70" stroke-width="4"/><circle cx="220" cy="35" r="9" fill="#277b70"/><circle cx="450" cy="35" r="9" fill="#277b70"/><text x="175" y="15">Visit 1</text><text x="410" y="15">Visit 2</text>';clear()}
for(const id of Object.keys(cards))on(id,'click',()=>choose(id));for(const id of ['aux','verb'])on(id,'change',preview);on('check','click',()=>txt('feedback',gradeCard(current,$('aux').value,$('verb').value)));on('retry','click',clear);on('reset','click',()=>choose('nora'));choose('nora');
</script></body></html>