Latitude and longitude intersection practice
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html>
<html lang="en-US"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Latitude and longitude intersection practice</title><style>*{box-sizing:border-box}body{margin:0;background:#eef2f6;color:#20334b;font:16px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{max-width:1040px;margin:auto;padding:26px 26px 55px}header{display:flex;align-items:end;justify-content:space-between;gap:20px;margin-bottom:20px}.eyebrow{font-size:11px;font-weight:750;text-transform:uppercase;letter-spacing:.13em;color:#58708a}h1{font-size:clamp(25px,4vw,34px);line-height:1.12;letter-spacing:-.035em;margin:7px 0 0}h2{font-size:21px;line-height:1.25;margin:0 0 14px}h3{font-size:17px;margin:0 0 10px}p{margin:8px 0 14px}button,input,select{font:inherit}button{border:1px solid #9aacc0;border-radius:6px;background:#fff;color:#203a58;padding:10px 15px;font-weight:700;cursor:pointer}button.primary{background:#244a79;border-color:#244a79;color:#fff}button:hover{filter:brightness(.95)}button:disabled{opacity:.5;cursor:not-allowed}button:focus-visible,select:focus-visible,summary:focus-visible,a:focus-visible{outline:3px solid #b54f25;outline-offset:4px}select{width:100%;border:1px solid #9aacc0;border-radius:6px;background:white;padding:11px;color:#20334b;min-width:0}label{display:block;font-size:14px;font-weight:700;margin:0 0 6px}.activity{background:white;border:1px solid #c4d1df;border-radius:13px;overflow:hidden;box-shadow:0 12px 36px #243b5510}.task{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:22px 25px;background:#e4edf8;border-bottom:1px solid #c4d1df}.task h2{margin:4px 0 0;font-size:27px}.pair{display:flex;gap:20px;flex-wrap:wrap}.pair span{display:block;font-size:12px;font-weight:650;color:#58708a}.pair strong{display:block;font-size:28px;line-height:1.1;letter-spacing:-.03em}.workspace{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);gap:30px;padding:24px}.progress{font-size:14px;font-weight:750;white-space:nowrap}.rounds{display:flex;gap:6px;margin-top:8px;justify-content:end}.rounds span{border:1px solid #a9b9cb;border-radius:50%;width:25px;height:25px;text-align:center;font-size:12px;line-height:23px;background:#fff}.rounds [aria-current=step]{background:#244a79;border-color:#244a79;color:white}.rounds .complete{background:#c5e5d9;color:#214c42}.instruction{font-size:14px;margin:0 0 15px;color:#536a82}.gridWrap{min-width:0}.axisTitle{font-size:12px;font-weight:750;text-align:center;color:#526a85;letter-spacing:.04em}.coordinateGrid{border-collapse:collapse;table-layout:fixed;width:100%;isolation:isolate;margin:2px 0 0}.coordinateGrid th{font-weight:700;font-size:12px;color:#3d5877;text-align:center;height:41px;white-space:nowrap}.coordinateGrid thead th:first-child{width:64px;text-align:left;font-size:10px}.coordinateGrid tbody th{padding-right:9px;text-align:right}.coordinateGrid td{position:relative;text-align:center;padding:0;height:57px}.coordinateGrid td:before{content:'';position:absolute;top:50%;left:0;right:0;height:1px;background:#bed0df;z-index:-1}.coordinateGrid td:after{content:'';position:absolute;left:50%;top:0;bottom:0;width:1px;background:#bed0df;z-index:-1}.coordinateGrid td:first-of-type:before{left:50%}.coordinateGrid td:last-of-type:before{right:50%}.coordinateGrid tbody tr:first-child td:after{top:50%}.coordinateGrid tbody tr:last-child td:after{bottom:50%}.point{width:44px;height:44px;padding:0;border:0;border-radius:50%;background:transparent;font-size:0;display:inline-grid;place-items:center;position:relative;vertical-align:middle}.point:before{content:'○';font-size:25px;line-height:1;background:white;color:#46698e;border-radius:50%;width:23px;height:23px;display:grid;place-items:center}.point[aria-pressed=true]{background:#dce9f7;box-shadow:inset 0 0 0 2px #315f92}.point[aria-pressed=true]:before{content:'●';color:#244a79;background:transparent}.point.correct:before{content:'✓';font-size:20px;font-weight:850;color:#1c5e48;background:#c6e9d8}.point:disabled{opacity:1}.point:disabled:before{color:#738499}.point.correct:disabled:before{color:#1c5e48}.selectedPair{min-height:30px;font-size:13px;text-align:center;margin-top:15px;font-variant-numeric:tabular-nums;color:#4e6681}.choices{display:grid;grid-template-columns:1fr 1fr;gap:14px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:18px}.feedback{min-height:100px;margin:20px 0 0;padding:15px;background:#f1f5fa;border-left:3px solid #b3c5d9;font-size:14px;white-space:pre-line}.feedback:empty{padding:0;background:transparent;border:0;min-height:0;margin:0}.feedback[data-kind=correct],.feedback[data-kind=finished],.feedback[data-kind=already]{border-color:#4d876f;background:#edf6f0}.feedback[data-kind=wrong],.feedback[data-kind=missing],.feedback[data-kind=invalid]{border-color:#b36137;background:#fcf2e7}.legend{font-size:12px;color:#5c7188;display:flex;gap:16px;flex-wrap:wrap;margin-top:12px}.hint{margin-top:25px;padding-top:17px;border-top:1px solid #dae2eb;font-size:14px;color:#526981}.below{margin-top:27px}.paper{background:white;border:1px solid #c4d1df;border-radius:10px;padding:24px}.muted{font-size:14px;color:#596e85}.keyTable{width:100%;border-collapse:collapse;font-size:14px}.keyTable td,.keyTable th{text-align:left;border-bottom:1px solid #d6e0e9;padding:12px 9px;vertical-align:top}.keyTable th{font-size:12px;color:#526b84}details>summary{font-weight:750;cursor:pointer}details[open]>summary{margin-bottom:20px}.worksheetGrid{margin:18px auto;max-width:500px}.worksheetGrid td{font-size:22px;color:#4e6783}.worksheetGrid:before{content:none}.worksheetGrid td:after,.worksheetGrid td:before{z-index:0}.worksheetGrid td span{position:relative;background:white;z-index:1}.worksheetGrid th{font-size:12px}.answerBlank{display:inline-block;border-bottom:1px solid #a9b7c6;min-width:130px;min-height:20px}.worksheetPrompts{font-size:14px}.worksheetPrompts p{margin:15px 0}a{color:#275c8b;overflow-wrap:anywhere}.sources{font-size:13px}.sources a{display:inline-block;margin-right:16px}.signed{font-variant-numeric:tabular-nums;background:#f1f5fa;padding:12px 15px;border-radius:5px}.manual{font-size:14px;padding-left:22px}.manual li{margin:9px 0}[hidden]{display:none!important}@media(max-width:720px){main{padding:20px 18px 40px}.workspace{grid-template-columns:1fr;gap:16px;padding:20px}.task{padding:18px 20px}.coordinateGrid td{height:48px}.gridWrap{max-width:480px;width:100%;margin:auto}.feedback{min-height:0}.hint{margin-top:18px}.choicePanel{border-top:1px solid #dae2eb;padding-top:16px}header{align-items:start}.task h2{font-size:24px}.pair strong{font-size:25px}.pair{gap:15px}.paper{padding:20px}.keyTable td,.keyTable th{padding:10px 5px}}@media(max-width:400px){main{padding:16px 13px 35px}header{margin-bottom:15px}.eyebrow{font-size:10px}h1{font-size:25px}.task{padding:14px;gap:10px}.task h2{font-size:22px}.pair{gap:11px}.pair strong{font-size:23px}.pair span{font-size:11px}.workspace{padding:14px;gap:13px}.coordinateGrid thead th:first-child{width:55px}.coordinateGrid th{font-size:11px;height:35px}.coordinateGrid td{height:44px}.coordinateGrid tbody th{padding-right:5px}.progress{font-size:12px}.rounds span{width:22px;height:22px;line-height:20px}.rounds{gap:4px}.instruction{font-size:13px;margin-bottom:8px}.point{width:40px;height:42px}.point:before{font-size:23px;width:21px;height:21px}.choices{gap:10px}.actions{margin-top:12px}button{padding:10px 13px}.selectedPair{margin-top:9px;min-height:20px}.legend{margin-top:7px}.hint{font-size:13px}.paper{padding:17px}.keyTable{font-size:13px}.keyTable th{font-size:11px}}@media(max-width:400px){.task{display:grid;grid-template-columns:1fr auto;gap:10px}.task>.pair{grid-row:2;grid-column:1/-1;flex-wrap:nowrap;gap:28px}.task>div:last-child{grid-column:2;grid-row:1}.choices{align-items:end}}</style></head><body><main><header><div><div class="eyebrow">Classroom geography / Fictional markers</div><h1>Find the labeled intersection</h1></div></header>
<section class="activity" aria-labelledby="roundId"><div class="task"><div><span class="eyebrow" id="taskState">Round 1 / 3</span><h2 id="roundId">Find P</h2></div><div class="pair" aria-label="Target coordinate, latitude first"><div><span>Latitude first</span><strong id="targetLatitude">42°N</strong></div><div><span>Longitude second</span><strong id="targetLongitude">102°W</strong></div></div><div><div class="progress" id="progress" aria-live="polite">0 of 3 complete</div><div class="rounds" aria-label="Round progress"><span id="round-P" aria-current="step">P</span><span id="round-Q">Q</span><span id="round-R">R</span></div></div></div>
<div class="workspace"><div class="gridWrap"><p class="instruction">Choose an intersection or use both lists, then Check.</p><div class="axisTitle">Longitude columns (°W)</div><table class="coordinateGrid" aria-label="Schematic grid: five north-latitude rows and five west-longitude columns"><thead><tr><th scope="col">Latitude ↓</th><th scope="col">104°W</th><th scope="col">103°W</th><th scope="col">102°W</th><th scope="col">101°W</th><th scope="col">100°W</th></tr></thead><tbody id="gridBody"></tbody></table><div id="selection" class="selectedPair">No intersection selected</div><div class="legend"><span>○ Unselected</span><span>● Selected</span><span>✓ Checked correct</span></div></div>
<div class="choicePanel"><form id="form" novalidate><div class="choices"><div><label for="latitude">Latitude (°N)</label><select id="latitude"><option value="">Choose…</option></select></div><div><label for="longitude">Longitude (°W)</label><select id="longitude"><option value="">Choose…</option></select></div></div><div class="actions"><button id="check" type="submit" class="primary">Check</button><button id="next" type="button" disabled>Next →</button><button id="reset" type="button">Reset</button></div><p id="feedback" class="feedback" role="status" aria-live="polite"></p></form><p class="hint" id="readTip">Read the latitude row first. Then find the longitude column.</p></div></div></section>
<section class="paper below scope"><h2>Read the labels, not a generic x,y pair</h2><p>Moving up increases north latitude: 40°N to 44°N. Moving right decreases the west-longitude magnitude: 104°W to 100°W.</p><p class="muted">This is an angular-label schematic with fictional markers. Equal display cells do not mean equal ground distances or a stated map projection. No real location, navigation, GPS, map tiles or distance calculation is involved. The completed-round count is local practice progress, not a school mark or a claim of mastery.</p></section>
<details class="paper below studentWorksheet"><summary>Student worksheet / unmarked grid</summary><div class="worksheetContent"><h2>Locate P, Q and R</h2><p class="muted">Use (latitude, longitude), keeping N and W attached. Mark each intersection, then record its latitude row and longitude column.</p><div class="axisTitle">Longitude columns (°W)</div><table class="coordinateGrid worksheetGrid" aria-label="Blank student worksheet coordinate grid"><thead><tr><th scope="col">Latitude ↓</th><th scope="col">104°W</th><th scope="col">103°W</th><th scope="col">102°W</th><th scope="col">101°W</th><th scope="col">100°W</th></tr></thead><tbody><tr><th scope="row">44°N</th><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td></tr><tr><th scope="row">43°N</th><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td></tr><tr><th scope="row">42°N</th><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td></tr><tr><th scope="row">41°N</th><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td></tr><tr><th scope="row">40°N</th><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td><td><span>○</span></td></tr></tbody></table><div class="worksheetPrompts"><p><strong>P:</strong> Latitude 42°N, longitude 102°W<br>Row / column: <span class="answerBlank"></span></p><p><strong>Q:</strong> Latitude 40°N, longitude 100°W<br>Row / column: <span class="answerBlank"></span></p><p><strong>R:</strong> Latitude 44°N, longitude 104°W<br>Row / column: <span class="answerBlank"></span></p></div><p class="muted">Fictional markers on a schematic grid. Equal cells are not equal physical distances.</p></div></details>
<details class="paper below teacherKey"><summary>Teacher key / separate from the student grid</summary><table class="keyTable"><thead><tr><th>Round</th><th>(Latitude, longitude)</th><th>Intersection</th></tr></thead><tbody><tr><td>P</td><td>(42°N, 102°W)</td><td>Center / row 3, column 3</td></tr><tr><td>Q</td><td>(40°N, 100°W)</td><td>Bottom right / row 5, column 5</td></tr><tr><td>R</td><td>(44°N, 104°W)</td><td>Top left / row 1, column 1</td></tr></tbody></table><p class="muted">Row and column positions above count from 1 at the top left. A P answer of (43°N, 102°W) has the correct longitude and is one displayed row north of the requested latitude. Latitude 104 and longitude 44 is invalid; never silently swap the numbers.</p><p class="signed">Teacher comparison only: under a north-positive / west-negative convention, P is (+42, −102). The activity accepts the labeled N/W values only; this is not a signed-input or degrees/minutes/seconds converter.</p></details>
<details class="paper below manualSection"><summary>Manual browser checks / pending parent review</summary><ol class="manual"><li>Use Tab and native keyboard activation on both lists and the 25 coordinate buttons; check visible focus and labels.</li><li>Check an unanswered P and then (43°N, 102°W). Neither completes the round; the second explains the latitude error.</li><li>Complete P, check again, then Next. Count stays one; selections unlock and clear for Q. Complete Q and R; no fourth round appears.</li><li>Reset from the finished state. P, zero progress, unanswered choices and empty feedback return.</li><li>Inspect this activity at 360, 720 and 1440 px and at the actual article width; check labels, touch controls and page overflow.</li></ol><p class="muted">Local scripted checks are retained separately. Real browser rendering, keyboard/touch behavior and hosted execution are still pending.</p></details>
<section class="below sources"><a href="https://www.usgs.gov/educational-resources/27-ideas-teaching-topographic-maps-0">USGS coordinate-reading ideas</a><a href="https://www.ion.org/outreach/upload/lesson1.pdf">Institute of Navigation grid-reading lesson</a></section><footer class="below muted">Local practice only. Reset clears progress; reloading starts a new activity.</footer></main><script data-app>
(()=>{
const $=id=>document.getElementById(id),text=(id,v)=>$(id).textContent=String(v),on=(id,event,fn)=>$(id).addEventListener(event,fn);const latitudes=[44,43,42,41,40],longitudes=[104,103,102,101,100];const rounds=[{id:'P',latitude:42,longitude:102},{id:'Q',latitude:40,longitude:100},{id:'R',latitude:44,longitude:104}];const pair=(lat,lon)=>'('+lat+'°N, '+lon+'°W)';
function validateRounds(data){if(!Array.isArray(data)||data.length!==3)throw Error('Setup needs exactly three rounds: P, Q and R.');const ids=new Set;data.forEach((r,i)=>{if(!r||r.id!==['P','Q','R'][i]||ids.has(r.id))throw Error('Setup round IDs must be unique and ordered P, Q, R.');ids.add(r.id);if(!Number.isInteger(r.latitude)||!latitudes.includes(r.latitude)||!Number.isInteger(r.longitude)||!longitudes.includes(r.longitude))throw Error('Setup coordinates must use the five declared north latitudes and west longitudes. Transposed values are not repaired.');});return true}
function readChoice(value,axis){if(value===''||value===null||value===undefined)return null;if(typeof value==='string'&&!/^[0-9]+$/.test(value))throw Error('Choose a labeled '+axis+' value from the grid.');const number=Number(value),list=axis==='latitude'?latitudes:axis==='longitude'?longitudes:null;if(!list||!Number.isInteger(number)||!list.includes(number))throw Error('Choose one of the five declared '+axis+' values.');return number}
class Geography{constructor(data=rounds){validateRounds(data);this.rounds=structuredClone(data);this.reset()}reset(){this.index=0;this.completed=new Set;this.latitude=null;this.longitude=null;this.feedback='';this.kind='idle'}get current(){return this.rounds[this.index]}get locked(){return this.completed.has(this.current.id)}get finished(){return this.completed.size===this.rounds.length}select(latitude,longitude){if(this.locked)return false;const lat=readChoice(latitude,'latitude'),lon=readChoice(longitude,'longitude');this.latitude=lat;this.longitude=lon;this.feedback='';this.kind='idle';return true}check(){const r=this.current;if(this.locked){this.kind='already';this.feedback=this.finished?'Activity finished. All 3 unique rounds are complete; checking again adds nothing.':r.id+' is already complete. '+this.completed.size+' of 3 unique rounds complete; choose Next.';return {kind:this.kind,count:this.completed.size}}if(this.latitude===null||this.longitude===null){this.kind='missing';this.feedback='Choose '+(this.latitude===null&&this.longitude===null?'both latitude and longitude':this.latitude===null?'a latitude row':'a longitude column')+' before checking. No round completed.';return {kind:this.kind,count:this.completed.size}}const latitude=readChoice(this.latitude,'latitude'),longitude=readChoice(this.longitude,'longitude');if(latitude===r.latitude&&longitude===r.longitude){this.completed.add(r.id);this.kind=this.finished?'finished':'correct';this.feedback=r.id+' complete at '+pair(latitude,longitude)+'. '+this.completed.size+' of 3 unique rounds complete.'+(this.finished?' Activity finished.':' Choose Next for the next prompt.');return {kind:this.kind,count:this.completed.size}}const rowDelta=latitude-r.latitude,columnDelta=longitude-r.longitude;const row=rowDelta===0?'Latitude '+latitude+'°N is correct.':'Latitude '+latitude+'°N is '+Math.abs(rowDelta)+' displayed '+(Math.abs(rowDelta)===1?'row':'rows')+' '+(rowDelta>0?'north':'south')+' of '+r.latitude+'°N. Move '+Math.abs(rowDelta)+' '+(Math.abs(rowDelta)===1?'row':'rows')+' '+(rowDelta>0?'south':'north')+'.';const column=columnDelta===0?'Longitude '+longitude+'°W is correct.':'Longitude '+longitude+'°W is '+Math.abs(columnDelta)+' displayed '+(Math.abs(columnDelta)===1?'column':'columns')+' '+(columnDelta>0?'west':'east')+' of '+r.longitude+'°W. Move '+Math.abs(columnDelta)+' '+(Math.abs(columnDelta)===1?'column':'columns')+' '+(columnDelta>0?'east':'west')+'.';this.kind='wrong';this.feedback=columnDelta===0?column+'\n'+row:row+'\n'+column;return {kind:this.kind,count:this.completed.size}}next(){if(!this.locked||this.finished)return false;this.index++;this.latitude=null;this.longitude=null;this.feedback='';this.kind='idle';return true}}
let game;try{game=new Geography()}catch(e){text('feedback','Setup error: '+e.message);document.querySelectorAll('button,select').forEach(el=>el.disabled=true);throw e}
const tbody=$('gridBody');latitudes.forEach(lat=>{const row=document.createElement('tr'),heading=document.createElement('th');heading.scope='row';heading.textContent=lat+'°N';row.append(heading);longitudes.forEach(lon=>{const td=document.createElement('td'),b=document.createElement('button');b.type='button';b.className='point';b.id='point-'+lat+'-'+lon;b.dataset.latitude=lat;b.dataset.longitude=lon;b.setAttribute('aria-label','Latitude '+lat+'°N, longitude '+lon+'°W');b.setAttribute('aria-pressed','false');b.addEventListener('click',()=>select(lat,lon));td.append(b);row.append(td)});tbody.append(row)});for(const [id,values,suffix]of [['latitude',latitudes,'°N'],['longitude',longitudes,'°W']])values.forEach(value=>{const o=document.createElement('option');o.value=value;o.textContent=value+suffix;$(id).append(o)});
function render(){const r=game.current;text('roundId','Find '+r.id);text('targetLatitude',r.latitude+'°N');text('targetLongitude',r.longitude+'°W');text('progress',game.completed.size+' of 3 complete');text('taskState',game.finished?'All rounds complete':'Round '+(game.index+1)+' / 3');['P','Q','R'].forEach(id=>{const el=$('round-'+id);el.className=game.completed.has(id)?'complete':'';if(r.id===id)el.setAttribute('aria-current','step');else el.removeAttribute('aria-current');el.setAttribute('aria-label','Round '+id+': '+(game.completed.has(id)?'complete':r.id===id?'current':'upcoming'))});$('latitude').value=game.latitude===null?'':game.latitude;$('longitude').value=game.longitude===null?'':game.longitude;$('latitude').disabled=game.locked;$('longitude').disabled=game.locked;$('next').disabled=!game.locked||game.finished;text('next',game.finished?'Finished':'Next →');text('check',game.locked?'Check again':'Check');text('feedback',game.feedback);$('feedback').dataset.kind=game.kind;text('selection',game.latitude===null&&game.longitude===null?'No intersection selected':pair(game.latitude===null?'—':game.latitude,game.longitude===null?'—':game.longitude));document.querySelectorAll('.point').forEach(b=>{const selected=+b.dataset.latitude===game.latitude&&+b.dataset.longitude===game.longitude;b.setAttribute('aria-pressed',selected);b.disabled=game.locked;b.classList.toggle('correct',selected&&game.locked);b.setAttribute('aria-label','Latitude '+b.dataset.latitude+'°N, longitude '+b.dataset.longitude+'°W'+(selected?game.locked?': correct intersection for '+r.id:': selected':''))});text('readTip',game.finished?'P, Q and R are complete. Reset to practice again.':game.locked?'The checked intersection is locked. Next starts the following round with both choices unanswered.':'Read the latitude row first. Then find the longitude column. Checking explains each component separately.');}
function select(lat,lon){try{game.select(lat,lon);render()}catch(e){game.kind='invalid';game.feedback=e.message;render()}}
on('latitude','change',()=>select($('latitude').value,$('longitude').value));on('longitude','change',()=>select($('latitude').value,$('longitude').value));on('form','submit',e=>{e.preventDefault();try{game.check();render()}catch(e){game.kind='invalid';game.feedback=e.message;render()}});on('next','click',()=>{if(game.next()){render();$('latitude').focus()}});on('reset','click',()=>{game.reset();render();$('latitude').focus()});window.app={Geography,validateRounds,readChoice,rounds,latitudes,longitudes,game,select,render};render();
})();
</script>
<script>
// Local calculation controls: no native form submission or sandbox permissions required.
(()=>{const form=document.getElementById('form');if(!form)throw Error('Calculation form missing');const dispatch=()=>form.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));const buttons=[...form.querySelectorAll('button')].filter(button=>button.type==='submit');if(buttons.length!==1)throw Error('Expected one calculation control');for(const button of buttons){button.type='button';button.addEventListener('click',dispatch);}form.addEventListener('keydown',event=>{if(event.key==='Enter'&&!event.isComposing&&['INPUT','SELECT'].includes(event.target.tagName)){event.preventDefault();dispatch();}});})();
</script>
</body></html>