From 838cb9743ed4d37df955be9fe9a4c16e680f5b40 Mon Sep 17 00:00:00 2001 From: Sebastian Molenda Date: Tue, 5 May 2026 16:22:20 +0200 Subject: [PATCH] init --- .gitignore | 1 + .htaccess | 44 +++++ README.md | 15 ++ app.js | 480 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 162 ++++++++++++++++++ styles.css | 74 +++++++++ 6 files changed, 776 insertions(+) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 README.md create mode 100644 app.js create mode 100644 index.html create mode 100644 styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..225396d --- /dev/null +++ b/.htaccess @@ -0,0 +1,44 @@ +# Disable directory listing +Options -Indexes + +# Default directory index +DirectoryIndex index.html + +# Serve index.html for SPA client-side routing (if file/dir doesn't exist) + + RewriteEngine On + RewriteBase / + # If the request is for an existing file or directory, serve it + RewriteCond %{REQUEST_FILENAME} -f [OR] + RewriteCond %{REQUEST_FILENAME} -d + RewriteRule ^ - [L] + # Otherwise rewrite to index.html + RewriteRule ^ index.html [L] + + +# Optional: small caching hints for static assets + + ExpiresActive On + ExpiresByType text/css "access plus 1 week" + # Keep JS cache short during development to avoid stale scripts on mobile + ExpiresByType application/javascript "access plus 60 seconds" + ExpiresByType image/* "access plus 1 week" + + + + # Add Cache-Control headers for more precise control + + Header set Cache-Control "max-age=60, public" + + + Header set Cache-Control "max-age=604800, public" + + + Header set Cache-Control "max-age=604800, public" + + + +# Prevent serving hidden files (like .env, .git) + + Require all denied + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bf3db4 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +Matma Trening - Prototyp + +Opis + +Prosty, mobilny prototyp aplikacji do trenowania działań matematycznych (na razie dodawanie i odejmowanie). + +Pliki + +- index.html - główny widok +- styles.css - stylowanie mobilne +- app.js - logika aplikacji (generacja zadań, tryby, klawiatura) + +Jak uruchomić + +Otwórz plik `index.html` w przeglądarce (najlepiej na urządzeniu mobilnym lub w trybie responsywnym). diff --git a/app.js b/app.js new file mode 100644 index 0000000..b5fed7a --- /dev/null +++ b/app.js @@ -0,0 +1,480 @@ +// Minimal app logic for math training (addition & subtraction) +const state = { + ops: new Set(), + mode: null, // 'timed' or 'training' + score: 0, + currentProblem: null, + answerBuffer: '', + timerId: null, + timeLeft: 60, + settings: { + timedSeconds: 60, + maxResult: 40, + maxOperand: 20, + sessionProblems: 20, + allowNegative: false, + allowFraction: false, + } +} + +// elements +const menuScreen = document.getElementById('menu-screen') +const playScreen = document.getElementById('play-screen') +const opsContainer = document.getElementById('ops') +const modeButtons = document.querySelectorAll('.mode-btn') +const opButtons = document.querySelectorAll('.op-btn') +const backBtn = document.getElementById('back-btn') +const problemEl = document.getElementById('problem') +const answerEl = document.getElementById('answer') +const feedbackEl = document.getElementById('feedback') +const timerEl = document.getElementById('timer') +const scoreEl = document.getElementById('score') +const keypad = document.querySelectorAll('.key') +const submitBtn = document.getElementById('submit') +const clearBtn = document.getElementById('clear') +const backspaceBtn = document.getElementById('backspace') +const historyBtn = document.getElementById('history-btn') +const historyScreen = document.getElementById('history-screen') +const historyBack = document.getElementById('history-back') +const historyList = document.getElementById('history-list') +const clearHistoryBtn = document.getElementById('clear-history') +const historyPanel = document.getElementById('history-panel') +const historyToggle = document.getElementById('history-toggle') +const settingTimed = document.getElementById('setting-timed') +const settingMaxResult = document.getElementById('setting-max-result') +const settingMaxOperand = document.getElementById('setting-max-operand') +const settingAllowNegative = document.getElementById('setting-allow-negative') +const settingAllowFraction = document.getElementById('setting-allow-fraction') +const saveSettingsBtn = document.getElementById('save-settings') +const resetSettingsBtn = document.getElementById('reset-settings') +const settingsToggle = document.getElementById('settings-toggle') +const settingsPanel = document.getElementById('settings-panel') +const progressInner = document.getElementById('progress-inner') +const dotBtn = document.getElementById('dot') +const negateBtn = document.getElementById('negate') +const summaryPanel = document.getElementById('summary-panel') +const summaryText = document.getElementById('summary-text') +const summaryBack = document.getElementById('summary-back') + +// load settings from localStorage +function loadSettings(){ + try{ + const raw = localStorage.getItem('matma:settings') + if (raw) { + const s = JSON.parse(raw) + state.settings = Object.assign(state.settings, s) + } + }catch(e){ console.warn('settings load failed', e) } + // reflect to inputs + settingTimed.value = state.settings.timedSeconds + settingMaxResult.value = state.settings.maxResult + settingMaxOperand.value = state.settings.maxOperand + const sess = state.settings.sessionProblems || 20 + const settingSessionProblems = document.getElementById('setting-session-problems') + if (settingSessionProblems) settingSessionProblems.value = sess + if (settingAllowNegative) settingAllowNegative.checked = !!state.settings.allowNegative + if (settingAllowFraction) settingAllowFraction.checked = !!state.settings.allowFraction +} + +function saveSettings(){ + const timed = parseInt(settingTimed.value,10) || state.settings.timedSeconds + const maxResult = parseInt(settingMaxResult.value,10) || state.settings.maxResult + const maxOperand = parseInt(settingMaxOperand.value,10) || state.settings.maxOperand + state.settings.timedSeconds = Math.max(5, Math.min(600, timed)) + state.settings.maxResult = Math.max(1, Math.min(999, maxResult)) + state.settings.maxOperand = Math.max(1, Math.min(999, maxOperand)) + const settingSessionProblems = document.getElementById('setting-session-problems') + if (settingSessionProblems) state.settings.sessionProblems = Math.max(1, Math.min(500, parseInt(settingSessionProblems.value,10) || state.settings.sessionProblems)) + if (settingAllowNegative) state.settings.allowNegative = !!settingAllowNegative.checked + if (settingAllowFraction) state.settings.allowFraction = !!settingAllowFraction.checked + try{ localStorage.setItem('matma:settings', JSON.stringify(state.settings)) }catch(e){console.warn('save failed',e)} + // visual feedback + feedbackEl.textContent = 'Ustawienia zapisane' + setTimeout(()=>feedbackEl.textContent = '',1500) +} + +function resetSettings(){ + state.settings = {timedSeconds:60, maxResult:40, maxOperand:20, sessionProblems:20} + saveSettings() + loadSettings() +} + +saveSettingsBtn.addEventListener('click', saveSettings) +resetSettingsBtn.addEventListener('click', resetSettings) + +// settings toggle +settingsToggle.addEventListener('click', ()=>{ + const collapsed = settingsPanel.classList.toggle('collapsed') + settingsToggle.textContent = collapsed ? 'Pokaż' : 'Ukryj' +}) + +loadSettings() + +// initial wiring +opButtons.forEach(b => { + if (b.classList.contains('disabled')) return + b.addEventListener('click', () => { + const op = b.dataset.op + if (state.ops.has(op)) { state.ops.delete(op); b.classList.remove('active') } + else { state.ops.add(op); b.classList.add('active') } + }) +}) + +modeButtons.forEach(b => b.addEventListener('click', () => { + modeButtons.forEach(x=>x.classList.remove('active')) + b.classList.add('active') + state.mode = b.dataset.mode + // require at least one op selected to start + if (state.ops.size === 0) { + feedbackEl.textContent = 'Wybierz co najmniej jedno działanie.' + return + } + startPlay() +})) + +backBtn.addEventListener('click', ()=>{ + stopTimer() + playScreen.classList.add('hidden') + menuScreen.classList.remove('hidden') + feedbackEl.textContent = '' +}) + +function startPlay(){ + menuScreen.classList.add('hidden') + playScreen.classList.remove('hidden') + state.score = 0 + scoreEl.textContent = state.score + state.answerBuffer = '' + answerEl.textContent = '' + feedbackEl.textContent = '' + state.currentProblem = generateProblem() + renderProblem() + // make problem larger for mobile clarity + problemEl.classList.add('big') + // init progress + if (state.mode === 'timed'){ + progressInner.style.width = '0%' + startTimer(state.settings.timedSeconds) + } else { + progressInner.style.width = '0%' + state.sessionSolved = 0 + state.sessionTarget = Math.max(1, state.settings.sessionProblems || 20) + updateProgress() + hideTimer() + } +} + +function generateProblem(){ + // choose an operation from selected ops + const ops = Array.from(state.ops) + const op = ops[Math.floor(Math.random()*ops.length)] + // generate operands within maxOperand and ensure result within maxResult + const maxOp = Math.max(1, state.settings.maxOperand) + const maxRes = Math.max(1, state.settings.maxResult) + let a = 0, b = 0 + // try a few times to satisfy constraints + for (let i=0;i<200;i++){ + if (op === 'div'){ + // divisor b should be non-zero + b = randInt(1, maxOp) + if (!state.settings.allowFraction){ + // produce integer result q such that a = q * b and a <= maxOp and q <= maxRes + const maxQByOp = Math.floor(maxOp / b) + const maxQ = Math.min(maxRes, maxQByOp) + if (maxQ < 0) continue + const q = randInt(0, maxQ) + a = q * b + // optional negative results + if (state.settings.allowNegative && Math.random() < 0.2) a = -a + return {a,b,op} + } else { + // allow fractional results: generate tenths (one decimal place) + const maxResultNByOp = Math.floor((10 * maxOp) / b) + const maxResultN = Math.min(maxRes * 10, maxResultNByOp) + if (maxResultN < 0) continue + const result_n = randInt(0, maxResultN) + // ensure dividend is integer: (result_n * b) % 10 === 0 + if ((result_n * b) % 10 !== 0) continue + a = (result_n * b) / 10 + if (state.settings.allowNegative && Math.random() < 0.2) a = -a + return {a,b,op} + } + } else if (op === 'mul'){ + a = randInt(0, maxOp) + b = randInt(0, maxOp) + if (a * b <= maxRes) return {a,b,op} + } else if (op === 'add'){ + a = randInt(0, maxOp) + b = randInt(0, maxOp) + if (a + b <= maxRes) return {a,b,op} + } else if (op === 'sub'){ + a = randInt(0, maxOp) + b = randInt(0, maxOp) + if (!state.settings.allowNegative){ + if (a < b) [a,b] = [b,a] + if (a - b <= maxRes) return {a,b,op} + } else { + // negative allowed: result absolute should be <= maxRes + if (Math.abs(a - b) <= maxRes) return {a,b,op} + } + } + } + // fallback: return simple small numbers + a = Math.min(maxOp, Math.floor(maxRes/2)) + b = Math.min(maxOp, 1) + return {a,b,op} +} + +function randInt(min,max){ + return Math.floor(Math.random()*(max-min+1))+min +} + +function renderProblem(){ + const p = state.currentProblem + const map = {add: '+', sub: '−', mul: '×', div: '÷'} + const symbol = map[p.op] || '?' + problemEl.textContent = `${p.a} ${symbol} ${p.b} = ?` +} + +function submitAnswer(){ + const buf = state.answerBuffer.trim() + if (buf.length === 0) return + const given = parseFloat(buf) + const p = state.currentProblem + let correct = null + if (p.op === 'add') correct = p.a + p.b + else if (p.op === 'sub') correct = p.a - p.b + else if (p.op === 'mul') correct = p.a * p.b + else if (p.op === 'div') correct = (p.b === 0) ? null : (p.a / p.b) + const eps = state.settings.allowFraction ? 1e-6 : 1e-9 + const isCorrect = (correct !== null) && (Math.abs(given - correct) < eps) + if (isCorrect) { + state.score += 1 + feedbackEl.textContent = '✔ dobrze' + } else { + feedbackEl.textContent = `✖ poprawne: ${Number.isFinite(correct) ? correct : '—'}` + } + scoreEl.textContent = state.score + // next problem + state.answerBuffer = '' + answerEl.textContent = '' + // increment solved count and handle training limit + if (state.mode === 'training'){ + state.sessionSolved = (state.sessionSolved || 0) + 1 + updateProgress() + if (state.sessionSolved >= state.sessionTarget){ + // reached limit -> show summary instead of next problem + saveSessionToHistory() + showSummary() + return + } + } + // otherwise generate next problem + state.currentProblem = generateProblem() + renderProblem() +} + +submitBtn.addEventListener('click', submitAnswer) + +keypad.forEach(k => { + k.addEventListener('click', ()=>{ + const v = k.textContent.trim() + if (!/^[0-9]$/.test(v)) return + if (state.answerBuffer.length >= 12) return + state.answerBuffer += v + answerEl.textContent = state.answerBuffer + }) +}) + +if (dotBtn){ + dotBtn.addEventListener('click', ()=>{ + if (!state.settings.allowFraction) return + if (state.answerBuffer.includes('.')) return + if (state.answerBuffer.length === 0) state.answerBuffer = '0' + state.answerBuffer += '.' + answerEl.textContent = state.answerBuffer + }) +} + +if (negateBtn){ + negateBtn.addEventListener('click', ()=>{ + if (!state.settings.allowNegative) return + if (state.answerBuffer.startsWith('-')) state.answerBuffer = state.answerBuffer.slice(1) + else state.answerBuffer = '-' + state.answerBuffer + answerEl.textContent = state.answerBuffer + }) +} + +clearBtn.addEventListener('click', ()=>{ + state.answerBuffer = '' + answerEl.textContent = '' +}) + +backspaceBtn.addEventListener('click', ()=>{ + state.answerBuffer = state.answerBuffer.slice(0,-1) + answerEl.textContent = state.answerBuffer +}) + +function startTimer(seconds){ + state.timeLeft = seconds + timerEl.textContent = state.timeLeft + timerEl.classList.remove('hidden') + state.timerId = setInterval(()=>{ + state.timeLeft -= 1 + timerEl.textContent = state.timeLeft + // update progress bar for timed mode + const pct = Math.max(0, ((seconds - state.timeLeft) / seconds) * 100) + progressInner.style.width = pct + '%' + if (state.timeLeft <= 0) { + stopTimer() + endSession() + } + },1000) +} + +function stopTimer(){ + if (state.timerId) clearInterval(state.timerId) + state.timerId = null +} + +function hideTimer(){ + timerEl.classList.add('hidden') +} + +function endSession(){ + feedbackEl.textContent = `Koniec! Wynik: ${state.score}` + saveSessionToHistory() + // fill progress + progressInner.style.width = '100%' + // return to menu after short pause (timed mode) + setTimeout(()=>{ + playScreen.classList.add('hidden') + menuScreen.classList.remove('hidden') + },2500) +} + +function showSummary(){ + // hide problem area and keypad, show summary panel + const summaryTextEl = summaryText + if (summaryTextEl) summaryTextEl.textContent = `Poprawne odpowiedzi: ${state.score}` + // hide play UI sections + const problemArea = document.querySelector('.problem-area') + const answerArea = document.querySelector('.answer-area') + if (problemArea) problemArea.classList.add('hidden') + if (answerArea) answerArea.classList.add('hidden') + if (summaryPanel) summaryPanel.classList.remove('hidden') +} + +function hideSummary(){ + const problemArea = document.querySelector('.problem-area') + const answerArea = document.querySelector('.answer-area') + if (problemArea) problemArea.classList.remove('hidden') + if (answerArea) answerArea.classList.remove('hidden') + if (summaryPanel) summaryPanel.classList.add('hidden') +} + +if (summaryBack) summaryBack.addEventListener('click', ()=>{ + // close summary and go back to menu + hideSummary() + playScreen.classList.add('hidden') + menuScreen.classList.remove('hidden') +}) + +function updateProgress(){ + if (state.mode === 'training'){ + const solved = state.sessionSolved || 0 + const target = state.sessionTarget || 1 + const pct = Math.min(100, Math.round((solved/target)*100)) + progressInner.style.width = pct + '%' + // show count in status + const status = document.getElementById('status') + if (status) status.textContent = `${solved}/${target}` + } +} + +// history persistence +function loadHistory(){ + try{ + const raw = localStorage.getItem('matma:history') + return raw ? JSON.parse(raw) : [] + }catch(e){ return [] } +} + +function saveSessionToHistory(){ + const h = loadHistory() + h.unshift({ts: Date.now(), mode: state.mode, score: state.score, settings: {...state.settings}}) + // keep only last 100 + while(h.length>100) h.pop() + try{ localStorage.setItem('matma:history', JSON.stringify(h)) }catch(e){ } +} + +function renderHistory(){ + const h = loadHistory() + historyList.innerHTML = '' + if (h.length === 0) { + historyList.innerHTML = '
Brak zapisanych sesji
' + if (historyList) historyList.innerHTML = historyList.innerHTML + return + } + h.forEach(item => { + const d = new Date(item.ts).toLocaleString() + const el = document.createElement('div') + el.className = 'history-item' + el.innerHTML = `
${item.mode === 'timed' ? 'Na czas' : 'Trening'} — Wynik: ${item.score}
${d} • czas:${item.settings.timedSeconds}s maxRes:${item.settings.maxResult} maxOp:${item.settings.maxOperand}
` + historyList.appendChild(el) + }) + if (historyList) historyList.innerHTML = historyList.innerHTML +} + +// Toggle in-menu history panel (behaves like settings) +function toggleHistory(){ + if (!historyPanel) return + // toggle collapsed class (collapsed => hidden) + const nowCollapsed = historyPanel.classList.toggle('collapsed') + // nowCollapsed is true when panel is hidden + if (!nowCollapsed) renderHistory() + // keep main menu button label constant; header toggle shows state + historyBtn.textContent = 'Historia' + if (historyToggle) historyToggle.textContent = nowCollapsed ? 'Pokaż' : 'Ukryj' +} + + +// history toggle +historyToggle.addEventListener('click', ()=>{ + const collapsed = historyPanel.classList.toggle('collapsed') + historyToggle.textContent = collapsed ? 'Pokaż' : 'Ukryj' + if (!collapsed) renderHistory() + else historyList.innerHTML = '' +}) + +// historyBtn.addEventListener('click', toggleHistory) +// if (historyToggle) historyToggle.addEventListener('click', toggleHistory) + +historyBack.addEventListener('click', ()=>{ + historyScreen.classList.add('hidden') + menuScreen.classList.remove('hidden') +}) + +clearHistoryBtn.addEventListener('click', ()=>{ + try{ localStorage.removeItem('matma:history') }catch(e){} + renderHistory() +}) + +// keyboard support: allow Enter/Backspace/0-9 +window.addEventListener('keydown', (e)=>{ + if (playScreen.classList.contains('hidden')) return + if (/^[0-9]$/.test(e.key)){ + if (state.answerBuffer.length < 6) { + state.answerBuffer += e.key + answerEl.textContent = state.answerBuffer + } + } else if (e.key === 'Backspace'){ + state.answerBuffer = state.answerBuffer.slice(0,-1) + answerEl.textContent = state.answerBuffer + } else if (e.key === 'Enter'){ + submitAnswer() + } +}) + +// small helper to set initial focus - mobile browsers will not open keyboard for divs +document.addEventListener('touchstart', ()=>{}, {passive:true}) diff --git a/index.html b/index.html new file mode 100644 index 0000000..17acb2d --- /dev/null +++ b/index.html @@ -0,0 +1,162 @@ + + + + + + Trening Matematyczny - Dodawanie/Odejmowanie + + + +
+
+

Matma Trening

+ +
+

Wybierz działania

+
+ + + + +
+
+ +
+

Tryb gry

+
+ + +
+

Wybierz co najmniej jedno działanie, a następnie tryb, aby rozpocząć.

+
+ +
+

Historia + +

+ +
+ +
+

+ Ustawienia + +

+ +
+
+ +
+
+
+ +
+
+
Trening
+
+
+
0
+
+
+
+ +
+ +
+ +
+ +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + +
+ +
+
+
+ +
+
Historia sesji
+
+ +
+
+
+ +
+
+
+ + + + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..4c9648e --- /dev/null +++ b/styles.css @@ -0,0 +1,74 @@ +:root{ + --bg:#f7f7fb; + --card:#ffffff; + --accent:#2b6cb0; + --muted:#6b7280; +} +*{box-sizing:border-box} +html,body{height:100%} +.app-wrap{width:100%;max-width:420px} +body{ + margin:0; font-family:Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; + background:var(--bg); color:#111; display:flex; align-items:center; justify-content:center; padding:16px; +} +.screen{width:100%;background:var(--card);border-radius:16px;box-shadow:0 6px 22px rgba(16,24,40,0.08);position:relative;overflow:auto;-webkit-overflow-scrolling:touch} +.screen.hidden{display:none} +.hidden{display:none} +.app-title{margin:20px;text-align:center;font-size:20px} +.panel{padding:12px 18px;border-top:1px solid #f0f0f3} +.ops{display:flex;gap:8px;flex-wrap:wrap} +.op-btn{flex:1 1 48%;padding:12px;border-radius:10px;border:1px solid #e6e9ef;background:white;font-size:16px} +.op-btn.active{background:var(--accent);color:white;border-color:var(--accent)} +.op-btn.disabled{opacity:0.45} +.modes{display:flex;gap:12px;padding-top:8px} +.mode-btn{flex:1;padding:12px;border-radius:10px;border:1px solid #e6e9ef;background:white;font-size:16px} +.mode-btn.active{background:#111;color:white} +.hint{padding:12px 18px;color:var(--muted);font-size:14px} + +.settings{display:flex;flex-direction:column;gap:8px} +.settings label{display:flex;justify-content:space-between;align-items:center;gap:12px} +.settings input{width:120px;padding:8px;border-radius:8px;border:1px solid #e6e9ef} +.settings-actions{display:flex;gap:8px;margin-top:6px} +.settings.collapsed{display:none} +.collapsed{display:none} +.small{font-size:13px;padding:6px 8px;border-radius:8px;border:1px solid #e6e9ef;background:white} + +.progress-outer{height:12px;background:#eef3fb;border-radius:8px;overflow:hidden;margin:8px 14px} +.progress-inner{height:100%;background:var(--accent);width:0%} + + +.play-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #f0f0f3} +.play-header .small{background:transparent;border:0;color:var(--accent);font-weight:600} +.problem-area{padding:18px;text-align:center} +.problem{font-size:44px;font-weight:700;margin:6px 0} +.problem.big{font-size:64px} +.timer{font-size:18px;color:var(--muted);margin-bottom:8px} +.answer-area{padding:8px 14px 20px} +.answer{min-height:56px;font-size:30px;text-align:center;padding:8px;border-radius:8px;background:#f6f7fb;margin-bottom:10px} +.feedback{height:22px;color:var(--muted)} +.keypad{display:grid;gap:10px} +.row{display:flex;gap:8px} +.key{flex:1;padding:16px;border-radius:12px;border:1px solid #e6e9ef;background:white;font-size:22px} +.key.large{padding:18px;font-size:24px} +.key.special{background:#fff7ed} +.submit-btn{flex:1;padding:16px;border-radius:12px;background:var(--accent);color:white;font-weight:700;font-size:18px;border:0} +.score{font-weight:700} + +.history-list{display:flex;flex-direction:column;gap:8px} +.history-item{padding:10px;border-radius:8px;background:#f6f7fb;border:1px solid #e9eef6} +.history-meta{font-size:12px;color:var(--muted)} + +.summary{padding:18px;text-align:center} +.summary h2{margin:6px 0} +.summary p{font-size:18px} + +@media (orientation:portrait){ + .screen{max-height:94vh} +} + +/* Mobile-specific: ensure the app fills viewport and screens scroll vertically */ +@media (max-width:480px){ + body{padding:0} + .app-wrap{max-width:100%;height:100vh} + .screen{height:100vh;max-height:none;border-radius:0;overflow:auto;-webkit-overflow-scrolling:touch} +}