multikb

 multike

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">

    <meta name="google-site-verification" content="YOykyV7jEUg5Nj2GeyhHUcewf9WW-Ruyke1v23KePrY" />

    <title>Nejashi Keyboard | Full Mapping & Dual Layouts & Speech</title>

    

    <link rel="preconnect" href="https://fonts.googleapis.com">

    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

    <!-- Imported bold weights for Noto Sans Arabic for maximum visibility -->

    <link href="https://fonts.googleapis.com/css2?family=Abyssinica+SIL&family=Noto+Sans+Arabic:wght@400;600;700&family=Roboto:wght@400;700&display=swap" rel="stylesheet">


    <style>

        :root {

            --bg-app: #f0f2f5;

            --bg-keyboard: #d1d4d9;

            --key-bg: #ffffff;

            --key-bg-func: #adb5bd;

            --key-bg-active: #e9ecef;

            --key-accent: #0d6efd; 

            --key-vowel: #d32f2f;

            --text-main: #1a1a1a;

            --text-func: #333;

            --radius-key: 6px;

            --shadow-key: 0 1px 2px rgba(0,0,0,0.3);

            --font-ethiopic: 'Abyssinica SIL', serif;

            --font-arabic: 'Noto Sans Arabic', sans-serif;

        }


        * { 

            box-sizing: border-box; 

            -webkit-tap-highlight-color: transparent; 

            user-select: none;

            touch-action: manipulation;

        }


        body {

            font-family: 'Roboto', var(--font-ethiopic), sans-serif;

            background-color: var(--bg-app);

            margin: 0;

            height: 100dvh; 

            display: flex;

            flex-direction: column;

            overflow: hidden;

        }


        /* --- Output Container & Area --- */

        .output-container {

            position: relative;

            flex: 1;

            display: flex;

            width: 100%;

            background: #fff;

            overflow: hidden; /* Contains the scrollable output safely */

        }


        #output {

            width: 100%;

            flex: 1;

            padding: 30px 20px;

            font-size: 32px; 

            text-align: center; 

            line-height: 1.5;

            outline: none;

            overflow-y: auto;

            word-wrap: break-word;

            user-select: text;

            border: none;

            font-family: var(--font-ethiopic);

        }

        #output:empty:before { 

            content: attr(placeholder); 

            color: #bbb; 

        }


        /* --- Speech Recognition UI --- */

        .mic-btn {

            position: absolute;

            bottom: 20px;

            right: 20px;

            width: 55px;

            height: 55px;

            border-radius: 50%;

            background: var(--key-accent);

            color: white;

            border: none;

            box-shadow: 0 4px 10px rgba(0,0,0,0.25);

            cursor: pointer;

            display: flex;

            align-items: center;

            justify-content: center;

            z-index: 10;

            transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);

        }

        .mic-btn svg { width: 28px; height: 28px; fill: white; transition: transform 0.2s; }

        

        .mic-btn.recording {

            background: var(--key-vowel);

            animation: pulse-recording 1.5s infinite;

        }

        .mic-btn.recording svg { transform: scale(1.1); }

        

        @keyframes pulse-recording {

            0% { box-shadow: 0 0 0 0 rgba(211, 47, 47, 0.6); }

            70% { box-shadow: 0 0 0 15px rgba(211, 47, 47, 0); }

            100% { box-shadow: 0 0 0 0 rgba(211, 47, 47, 0); }

        }


        .speech-interim {

            position: absolute;

            bottom: 25px;

            left: 50%;

            transform: translateX(-50%);

            background: rgba(0, 0, 0, 0.75);

            color: #fff;

            padding: 10px 20px;

            border-radius: 25px;

            font-size: 16px;

            font-family: 'Roboto', sans-serif;

            max-width: 70%;

            white-space: nowrap;

            overflow: hidden;

            text-overflow: ellipsis;

            z-index: 20;

            pointer-events: none;

            transition: opacity 0.3s, transform 0.3s;

            box-shadow: 0 4px 6px rgba(0,0,0,0.1);

        }

        .speech-interim.hidden { 

            opacity: 0; 

            transform: translate(-50%, 10px); 

        }


        /* --- Keyboard Area --- */

        .keyboard-area {

            background: var(--bg-keyboard);

            padding: 8px 4px calc(8px + env(safe-area-inset-bottom, 12px)) 4px;

            flex-shrink: 0;

            border-top: 1px solid #bbb;

        }


        .keyboard-grid {

            display: flex;

            flex-direction: column;

            gap: 5px;

            max-width: 850px;

            margin: 0 auto;

        }

        .row { display: flex; justify-content: center; gap: 5px; width: 100%; }


        .key {

            background: var(--key-bg);

            color: var(--text-main);

            border-radius: var(--radius-key); 

            height: 60px; 

            flex: 1;

            display: flex;

            flex-direction: column;

            align-items: center;

            justify-content: center;

            box-shadow: var(--shadow-key);

            font-family: var(--font-ethiopic);

            position: relative;

            cursor: pointer;

        }

        .key:active, .key.pressed { background: var(--key-bg-active); transform: translateY(1px); box-shadow: none; }


        /* Label Scaling */

        .key-hint {

            font-size: 16px; 

            line-height: 1;

            color: #666;

            margin-bottom: 2px;

        }

        .key-main {

            font-size: 24px; /* Standard English Size */

            line-height: 1;

            font-weight: 700; 

            color: var(--text-main);

            font-family: 'Roboto', sans-serif;

        }

        .key-main.ethio {

            font-family: var(--font-ethiopic);

            font-weight: 400;

        }


        /* Specific Arabic Highlight Styles */

        .ar-hint {

            font-family: var(--font-arabic);

            font-size: 18px; /* Smaller than English but bold */

            font-weight: 700; 

            line-height: 1;

            margin-bottom: 2px;

            color: #555; 

        }

        

        .ar-main {

            font-family: var(--font-arabic);

            font-size: 34px; 

            font-weight: 700; 

            line-height: 1;

            color: var(--text-main);

        }


        /* Vowel Colors */

        .key.vowel .key-main, 

        .key.vowel .ar-hint, 

        .key.vowel .ar-main { 

            color: var(--key-vowel); 

        }

        

        /* Functional Key Styling */

        .key.func { background: var(--key-bg-func); color: var(--text-func); font-weight: 700; font-family: 'Roboto', sans-serif; }

        .key.action { background: var(--key-accent); color: #fff; flex-grow: 1.4; font-weight: 600; font-size: 16px; }

        .key.space { flex-grow: 3; font-size: 14px; color: #444; font-family: 'Roboto', sans-serif; font-weight: 700; }

        .key.shift-on { background: #fff; color: var(--key-accent); border: 2px solid var(--key-accent); }


        /* Layout Variations styling */

        .key.variation-active {

            background-color: var(--key-bg);

            border: 2px solid var(--key-accent);

            color: var(--key-accent);

            font-weight: 700; 

            z-index: 2; 

        }

        .key.variation-active .key-main {

            color: var(--key-accent);

        }


        svg { width: 22px; height: 22px; fill: currentColor; }


        .footer-credit {

            text-align: center;

            font-size: 10px;

            color: #777;

            margin-top: 8px;

            font-weight: 600;

            letter-spacing: 0.5px;

            text-transform: uppercase;

        }

    </style>

</head>

<body>


    <div class="output-container">

        <div id="output" contenteditable="true" inputmode="none" placeholder="Type here..."></div>

        

        <!-- Modern Floating Speech Overlay UI -->

        <div id="speech-interim" class="speech-interim hidden">Listening...</div>

        

        <button id="mic-btn" class="mic-btn" title="Start Speech Typing" onclick="toggleRecording()">

            <svg viewBox="0 0 24 24">

                <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5-3c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>

            </svg>

        </button>

    </div>


    <div class="keyboard-area">

        <div class="keyboard-grid" id="kb-grid"></div>

        <div class="footer-credit">AHMEDHAJI SADIK © 2026</div>

    </div>


<script>

    // --- AMHARIC MAPPING (Layout 1 - Phonetic) ---

    const AM_MAP_1 = { 

        'q':'ቀ', 'Q':'ቀ', 'w':'ወ', 'W':'ወ', 'r':'ረ', 'R':'ረ', 't':'ተ', 'T':'ጠ',

        'y':'የ', 'Y':'የ', 'p':'ፐ', 'P':'ጰ',

        's':'ሰ', 'S':'ሠ', 'd':'ደ', 'D':'ደ', 'f':'ፈ', 'F':'ፈ', 'g':'ገ', 'G':'ገ',

        'h':'ሀ', 'H':'ሐ', 'j':'ጀ', 'J':'ጀ', 'k':'ከ', 'K':'ኸ', 

        'l':'ለ', 'L':'ጸ', 

        'z':'ዘ', 'Z':'ዠ', 'x':'አ', 'X':'ዐ', 'c':'ቸ', 'C':'ጨ',

        'v':'ቨ', 'V':'ሸ', 

        'b':'በ', 'B':'ኀ', 

        'n':'ነ', 'N':'ኘ', 

        'm':'መ', 'M':'ፀ'  

    };


    // --- AMHARIC MAPPING (Layout 2 - Visual Grids) ---

    const AM_MAP_2 = { 

        'q':'ሀ', 'w':'ለ', 'e':'ሐ', 'r':'መ', 't':'ሠ', 'y':'ረ', 'u':'ሰ', 'i':'ሸ', 'o':'ቀ', 'p':'በ', '[':'ቨ', 

        'a':'ተ', 's':'ቸ', 'd':'የ', 'f':'ነ', 'g':'ኘ', 'h':'አ', 'j':'ከ', 'k':'ኸ', 'l':'ወ', ';':'ዘ', '\'':'ዠ', 

        'z':'።', 'x':'ደ', 'c':'ጀ', 'v':'ገ', 'b':'ጠ', 'n':'ጨ', 'm':'ጰ', ',':'ጸ', '.':'ፈ', 

        '/':'ፀ', '=':'ፐ', 'backslash_alt': 'ዐ', 'shift_alt': 'ኀ' 

    };

    

    // --- AMHARIC FIDEL DICTIONARY ---

    const AM_FIDEL = {

        'ሀ':['ሀ','ሁ','ሂ','ሃ','ሄ','ህ','ሆ','ኋ'],'ለ':['ለ','ሉ','ሊ','ላ','ሌ','ል','ሎ','ሏ'],'ሐ':['ሐ','ሑ','ሒ','ሓ','ሔ','ሕ','ሖ','ሗ'],'መ':['መ','ሙ','ሚ','ማ','ሜ','ም','ሞ','ሟ'],'ሠ':['ሠ','ሡ','ሢ','ሣ','ሤ','ሥ','ሦ','ሧ'],'ረ':['ረ','ሩ','ሪ','ራ','ሬ','ር','ሮ','ሯ'],'ሰ':['ሰ','ሱ','ሲ','ሳ','ሴ','ስ','ሶ','ሷ'],'ሸ':['ሸ','ሹ','ሺ','ሻ','ሼ','ሽ','ሾ','ሿ'],'ቀ':['ቀ','ቁ','ቂ','ቃ','ቄ','ቅ','ቆ','ቋ'],'በ':['በ','ቡ','ቢ','ባ','ቤ','ብ','ቦ','ቧ'],'ቨ':['ቨ','ቩ','ቪ','ቫ','ቬ','ቭ','ቮ','ቯ'],'ተ':['ተ','ቱ','ቲ','ታ','ቴ','ት','ቶ','ቷ'],'ቸ':['ቸ','ቹ','ቺ','ቻ','ቼ','ች','ቾ','ቿ'],'ኀ':['ኀ','ኁ','ኂ','ኃ','ኄ','ኅ','ኆ','ኋ'],'ነ':['ነ','ኑ','ኒ','ና','ኔ','ን','ኖ','ኗ'],'ኘ':['ኘ','ኙ','ኚ','ኛ','ኜ','ኝ','ኞ','ኟ'],'አ':['አ','ኡ','ኢ','ኣ','ኤ','እ','ኦ','ኧ'],'ከ':['ከ','ኩ','ኪ','ካ','ኬ','ክ','ኮ','ኳ'],'ኸ':['ኸ','ኹ','ኺ','ኻ','ኼ','ኽ','ኾ','ዃ'],'ወ':['ወ','ዉ','ዊ','ዋ','ዌ','ው','ዎ','ዋ'],'ዐ':['ዐ','ዑ','ዒ','ዓ','ዔ','ዕ','ዖ','ፇ'],'ዘ':['ዘ','ዙ','ዚ','ዛ','ዜ','ዝ','ዞ','ዟ'],'ዠ':['ዠ','ዡ','ዢ','ዣ','ዤ','ዥ','ዦ','ዧ'],'የ':['የ','ዩ','ዪ','ያ','ዬ','ይ','ዮ','ያ'],'ደ':['ደ','ዱ','ዲ','ዳ','ዴ','ድ','ዶ','ዷ'],'ጀ':['ጀ','ጁ','ጂ','ጃ','ጄ','ጅ','ጆ','ጇ'],'ገ':['ገ','ጉ','ጊ','ጋ','ጌ','ግ','ጎ','ጓ'],'ጠ':['ጠ','ጡ','ጢ','ጣ','ጤ','ጥ','ጦ','ጧ'],'ጨ':['ጨ','ጩ','ጪ','ጫ','ጬ','ጭ','ጮ','ጯ'],'ጰ':['ጰ','ጱ','ጲ','ጳ','ጴ','ጵ','ጶ','ጷ'],'ጸ':['ጸ','ጹ','ጺ','ጻ','ጼ','ጽ','ጾ','ጿ'],'ፀ':['ፀ','ፁ','ፂ','ፃ','ፄ','ፅ','ፆ','ጿ'],'ፈ':['ፈ','ፉ','ፊ','ፋ','ፌ','ፍ','ፎ','ፏ'],'ፐ':['ፐ','ፑ','ፒ','ፓ','ፔ','ፕ','ፖ','ፗ'],'።':['።']

    };


    // --- ARABIC MAPPING ---

    const AR_MAP = {

        'a': { d: 'ا', s: 'أ' }, 'y': { d: 'ي', s: 'ئ' }, 'h': { d: 'ح', s: 'ه' }, 'q': { d: 'ق' }, 'w': { d: 'و', s: 'ؤ' }, 

        'e': { d: '\u064E', s: 'إ' }, 'r': { d: 'ر', s: 'ز' }, 

        't': { d: 'ت', s: 'ة' }, 'u': { d: '\u064F' }, 'i': { d: '\u0650' }, 

        'o': { d: '\u0652' }, 's': { d: 'س', s: 'ص' }, 'd': { d: 'د', s: 'ض' }, 'f': { d: 'ف' }, 

        'g': { d: 'غ', s: 'ع' }, 'j': { d: 'ج' }, 'k': { d: 'ك', s: 'خ' }, 'l': { d: 'ل' }, 'z': { d: 'ذ', s: 'ظ' }, 

        'x': { d: 'ء', s: 'ء' }, 'c': { d: 'ث', s: 'ش' }, 'v': { d: 'ظ' }, 'b': { d: 'ب' }, 'n': { d: 'ن' }, 

        'm': { d: 'م' }, 'p': { d: 'ّ', s: 'ـ' }

    };


    const VOWELS = { 'e': 0, 'u': 1, 'i': 2, 'a': 3, 'y': 4, 'o': 6 };

    const PURE_VOWELS = ['a', 'e', 'i', 'o', 'u'];

    const PUNCT_DATA = {

        'amharic': { left: '፣', right: '።' },

        'english': { left: ',', right: '.' },

        'arabic': { left: '،', right: '.' }

    };

    

    const DEFAULT_TOP_ROW = ['፡', '።', '፣', '፤', '፥', '፦', '፧', '፨', '?'];


    let state = { 

        lang: 'amharic', 

        layoutAm: 1, 

        layoutAr: 1, 

        layoutEn: 1, 

        isShift: false, 

        isNum: false, 

        pendingConsonant: null, 

        lastVowel: null,

        lastArKey: null, 

        lastArTime: 0,

        currentTopRow: [...DEFAULT_TOP_ROW],

        isVariationActive: false 

    };


    const LAYOUT = {

        alpha: [ 

            ['q','w','e','r','t','y','u','i','o','p'],

            ['a','s','d','f','g','h','j','k','l'],

            ['Shift','z','x','c','v','b','n','m','Back'],

            ['!#1','Globe','Layout','punct_L','Space','punct_R','Enter']

        ],

        amharic2: [

            ['q','w','e','r','t','y','u','i','o','p','['], 

            ['a','s','d','f','g','h','j','k','l',';','\''], 

            ['z','x','c','v','b','n','m',',','.','Back'], 

            ['!#1','Globe','Layout','/','=','Space','backslash_alt','shift_alt','Enter']

        ],

        en2: [ 

            ['q','w','e','r','t','z','u','i','o','p'],

            ['a','s','d','f','g','h','j','k','l'],

            ['Shift','y','x','c','v','b','n','m','Back'],

            ['!#1','Globe','Layout','punct_L','Space','punct_R','Enter']

        ],

        ar2: [ 

            ['ض','ص','ث','ق','ف','غ','ع','ه','خ','ح'],

            ['ش','س','ي','ب','ل','ا','ت','ن','م','ك'],

            ['Shift','ظ','ط','ذ','د','ز','ر','و','ة','Back'],

            ['!#1','Globe','Layout','punct_L','Space','punct_R','Enter']

        ],

        num: [

            ['1','2','3','4','5','6','7','8','9','0'],

            ['@','#','$','%','&','-','+','(',')','/'],

            ['*','"',"'",':',';','!','?','_','=','Back'],

            ['ABC','Globe','punct_L','Space','punct_R','Enter']

        ]

    };


    const output = document.getElementById('output');


    // --- SPEECH RECOGNITION LOGIC ---

    const WebSpeechAPI = window.SpeechRecognition || window.webkitSpeechRecognition;

    let recognition = null;

    let isRecording = false;

    const micBtn = document.getElementById('mic-btn');

    const speechInterim = document.getElementById('speech-interim');


    if (WebSpeechAPI) {

        recognition = new WebSpeechAPI();

        recognition.continuous = true;

        recognition.interimResults = true;


        recognition.onstart = () => {

            isRecording = true;

            micBtn.classList.add('recording');

            speechInterim.classList.remove('hidden');

            speechInterim.textContent = "Listening...";

        };


        recognition.onresult = (event) => {

            let interimTranscript = '';

            let finalTranscript = '';


            for (let i = event.resultIndex; i < event.results.length; ++i) {

                if (event.results[i].isFinal) {

                    finalTranscript += event.results[i][0].transcript;

                } else {

                    interimTranscript += event.results[i][0].transcript;

                }

            }


            if (interimTranscript) {

                speechInterim.textContent = interimTranscript;

            } else {

                speechInterim.textContent = "Listening...";

            }


            if (finalTranscript) {

                // Formatting pad with space

                type(finalTranscript + ' ');

            }

        };


        recognition.onerror = (event) => {

            console.error("Speech recognition error", event.error);

            speechInterim.textContent = "Error: " + event.error;

            setTimeout(stopRecording, 1500);

        };


        recognition.onend = () => {

            if (isRecording) {

                try { recognition.start(); } catch(e) {} // Auto-restart continuous if not stopped manually

            } else {

                micBtn.classList.remove('recording');

                speechInterim.classList.add('hidden');

            }

        };

    } else {

        micBtn.style.display = 'none'; // Hide UI if browser is unsupported

    }


    function toggleRecording() {

        if (!recognition) {

            alert("Speech typing is not supported in this browser. Please try Google Chrome or Safari.");

            return;

        }

        if (isRecording) stopRecording();

        else startRecording();

    }


    function startRecording() {

        try {

            // Set Dynamic Input Dictionary per mapped languages natively

            if (state.lang === 'amharic') recognition.lang = 'am-ET';

            else if (state.lang === 'arabic') recognition.lang = 'ar-SA';

            else recognition.lang = 'en-US';

            

            recognition.start();

        } catch(e) { console.error(e); }

    }


    function stopRecording() {

        isRecording = false;

        micBtn.classList.remove('recording');

        speechInterim.classList.add('hidden');

        try { recognition.stop(); } catch(e) {}

    }


    // --- KEYBOARD LOGIC ---

    function type(text) {

        output.focus();

        document.execCommand('insertText', false, text);

        output.scrollTop = output.scrollHeight;

    }


    function resetState() { 

        state.pendingConsonant = null; 

        state.lastVowel = null; 

        state.lastArKey = null;

    }

    

    function resetTopRow() {

        state.currentTopRow = [...DEFAULT_TOP_ROW];

        state.isVariationActive = false;

    }


    function handleKey(k) {

        if (state.lang === 'amharic' && state.layoutAm === 2 && state.currentTopRow.includes(k) && !state.isNum) {

            if (state.isVariationActive) {

                document.execCommand('delete');

                type(k);

                resetTopRow();

            } else {

                type(k);

            }

            render();

            return;

        }


        if (k === 'Shift') { state.isShift = !state.isShift; render(); return; }

        

        if (k === '!#1' || k === 'ABC') { state.isNum = !state.isNum; resetTopRow(); render(); return; }

        

        if (k === 'Layout') { 

            if (state.lang === 'amharic') state.layoutAm = state.layoutAm === 1 ? 2 : 1;

            else if (state.lang === 'arabic') state.layoutAr = state.layoutAr === 1 ? 2 : 1;

            else if (state.lang === 'english') state.layoutEn = state.layoutEn === 1 ? 2 : 1;

            resetState(); 

            resetTopRow(); 

            render(); 

            return; 

        }

        if (k === 'Globe') {

            stopRecording(); // Stop recording when switching layout/languages

            const langs = ['amharic','arabic','english'];

            state.lang = langs[(langs.indexOf(state.lang) + 1) % 3];

            output.dir = state.lang === 'arabic' ? 'rtl' : 'ltr';

            resetState(); 

            resetTopRow(); 

            render(); 

            return;

        }

        if (k === 'Back') { 

            document.execCommand('delete'); 

            resetState(); 

            resetTopRow(); 

            render(); 

            return; 

        }

        if (k === 'Enter') { type('\n'); resetState(); resetTopRow(); render(); return; }

        if (k === 'Space') { type(' '); resetState(); resetTopRow(); render(); return; }

        if (k === 'punct_L') { type(PUNCT_DATA[state.lang].left); resetState(); return; }

        if (k === 'punct_R') { type(PUNCT_DATA[state.lang].right); resetState(); return; }


        if (state.isNum && k.length === 1) { type(k); return; }


        let inputChar = state.isShift ? k.toUpperCase() : k.toLowerCase();

        

        if (state.lang === 'amharic' && state.layoutAm === 2) {

            if (state.isVariationActive) state.isVariationActive = false; 


            let mapKey = k.toLowerCase();

            if (AM_MAP_2[mapKey] || AM_MAP_2[k]) {

                let amChar = AM_MAP_2[mapKey] || AM_MAP_2[k];

                if (amChar === '።') {

                    type(amChar);

                    resetTopRow();

                } else if (AM_FIDEL[amChar]) {

                    type(AM_FIDEL[amChar][0]);

                    state.currentTopRow = AM_FIDEL[amChar];

                    state.isVariationActive = true;

                } else {

                    type(amChar);

                    resetTopRow();

                }

            } else {

                type(inputChar);

                resetTopRow();

            }

        }

        else if (state.lang === 'amharic' && state.layoutAm === 1) {

            let keyLow = k.toLowerCase();

            if (state.pendingConsonant && state.lastVowel === 'u' && keyLow === 'a') {

                const base = AM_MAP_1[state.pendingConsonant];

                if (AM_FIDEL[base] && AM_FIDEL[base][7]) {

                    document.execCommand('delete');

                    type(AM_FIDEL[base][7]);

                }

                resetState();

            } else if (state.pendingConsonant && VOWELS.hasOwnProperty(keyLow)) {

                document.execCommand('delete');

                const base = AM_MAP_1[state.pendingConsonant];

                type(AM_FIDEL[base][VOWELS[keyLow]]);

                if (keyLow === 'u') { state.lastVowel = 'u'; } else { resetState(); }

            } else if (PURE_VOWELS.includes(keyLow)) {

                // Ignore pure vowels alone

            } else if (AM_MAP_1[inputChar]) {

                const base = AM_MAP_1[inputChar];

                type(AM_FIDEL[base][5]); 

                state.pendingConsonant = inputChar;

                state.lastVowel = null;

            } else {

                type(inputChar);

                resetState();

            }

        } 

        else if (state.lang === 'arabic') {

            if (state.layoutAr === 2) {

                type(k);

                state.lastArKey = null;

            } else {

                let keyLow = k.toLowerCase();

                let now = Date.now();

                let isDoubleTap = !state.isShift && (state.lastArKey === keyLow) && (now - state.lastArTime < 800);


                const doubleTapMap = {

                    'y': 'ى', 'e': 'ً', 'u': 'ٌ', 'i': 'ٍ', 'o': 'ٰ'

                };


                if (isDoubleTap && doubleTapMap[keyLow]) {

                    document.execCommand('delete'); 

                    type(doubleTapMap[keyLow]);

                    state.lastArKey = keyLow + keyLow; 

                } else {

                    const entry = AR_MAP[keyLow];

                    type(entry ? (state.isShift ? (entry.s !== undefined ? entry.s : inputChar) : entry.d) : inputChar);

                    state.lastArKey = keyLow;

                }

                state.lastArTime = now;

            }

        } 

        else {

            type(inputChar);

        }

        

        if (state.isShift) { state.isShift = false; }

        render(); 

    }


    function render() {

        const grid = document.getElementById('kb-grid');

        grid.innerHTML = '';

        

        let rows = [];

        if (state.isNum) {

            rows = LAYOUT.num; 

        } else if (state.lang === 'amharic' && state.layoutAm === 2) {

            rows = [state.currentTopRow, ...LAYOUT.amharic2];

        } else if (state.lang === 'arabic' && state.layoutAr === 2) {

            rows = LAYOUT.ar2;

        } else if (state.lang === 'english' && state.layoutEn === 2) {

            rows = LAYOUT.en2;

        } else {

            rows = LAYOUT.alpha;

        }


        rows.forEach((row, rowIndex) => {

            const rowEl = document.createElement('div');

            rowEl.className = 'row';

            row.forEach(key => {

                const kEl = document.createElement('div');

                kEl.className = 'key';

                kEl.dataset.key = key.toLowerCase();


                if (!state.isNum) {

                    let kLow = key.toLowerCase();

                    if (state.lang === 'amharic' && state.layoutAm === 1) {

                        if (['a', 'e', 'i', 'o', 'u', 'y'].includes(kLow)) kEl.classList.add('vowel');

                    } else if (state.lang === 'arabic' && state.layoutAr === 1) {

                        if (['a', 'e', 'i', 'o', 'u', 'p'].includes(kLow)) kEl.classList.add('vowel');

                    }

                }


                if (['Shift','Back','!#1','ABC','Globe','Layout'].includes(key)) kEl.classList.add('func');

                if (key === 'Enter') kEl.classList.add('action');

                if (key === 'Space') kEl.classList.add('space');

                if (key === 'Shift' && state.isShift) kEl.classList.add('shift-on');

                

                if (!state.isNum && state.lang === 'amharic' && state.layoutAm === 2 && rowIndex === 0 && state.isVariationActive) {

                    kEl.classList.add('variation-active');

                }


                if (['Back', 'Shift', 'Globe'].includes(key)) {

                    if (key === 'Back') kEl.innerHTML = `<svg viewBox="0 0 24 24"><path d="M22 3H7c-.69 0-1.23.35-1.59.88L0 12l5.41 8.11c.36.53.9.89 1.59.89h15c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-3 12.59L17.59 17 12 11.41 6.41 17 5 15.59 10.59 10 5 4.41 6.41 3 12 8.59 17.59 3 19 4.41 13.41 10 19 15.59z"/></svg>`;

                    else if (key === 'Shift') kEl.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12 8.41L16.59 13 18 11.59l-6-6-6 6L7.41 13 12 8.41zM6 18h12v-2H6v2z"/></svg>`;

                    else if (key === 'Globe') kEl.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>`;

                } else if (key === 'Layout') {

                    let currentLay = state.lang === 'amharic' ? state.layoutAm : (state.lang === 'arabic' ? state.layoutAr : state.layoutEn);

                    kEl.innerHTML = `Lay ${currentLay}`;

                } else if (key === 'Space') {

                    let currentLay = state.lang === 'amharic' ? state.layoutAm : (state.lang === 'arabic' ? state.layoutAr : state.layoutEn);

                    kEl.textContent = `${state.lang.toUpperCase()} (L${currentLay})`;

                } else if (key === 'Enter' || key === '!#1' || key === 'ABC') {

                    kEl.textContent = key;

                } else if (key === 'punct_L' || key === 'punct_R') {

                    const char = key === 'punct_L' ? PUNCT_DATA[state.lang].left : PUNCT_DATA[state.lang].right;

                    kEl.innerHTML = `<span class="key-main ethio">${char}</span>`;

                } else if (!state.isNum && state.lang === 'amharic' && state.layoutAm === 2) {

                    if (rowIndex === 0) {

                        kEl.innerHTML = `<span class="key-main ethio">${key}</span>`;

                    } else {

                        let amChar = '';

                        if (key === 'backslash_alt') amChar = AM_MAP_2['backslash_alt'];

                        else if (key === 'shift_alt') amChar = AM_MAP_2['shift_alt'];

                        else if (AM_MAP_2[key.toLowerCase()]) {

                             const base = AM_MAP_2[key.toLowerCase()];

                             amChar = AM_FIDEL[base] ? AM_FIDEL[base][0] : base;

                        } else {

                             amChar = key;

                        }

                        kEl.innerHTML = `<span class="key-main ethio">${amChar}</span>`;

                    }

                } else if (!state.isNum && state.lang === 'amharic' && state.layoutAm === 1) {

                    const engChar = state.isShift ? key.toUpperCase() : key.toLowerCase();

                    let amChar = '';

                    if (key.toLowerCase() === 'y') {

                        amChar = state.pendingConsonant ? 'ኤ' : 'ይ';

                    } else if (!PURE_VOWELS.includes(key.toLowerCase()) && AM_MAP_1[engChar]) {

                        const base = AM_MAP_1[engChar];

                        amChar = AM_FIDEL[base][5];

                    }

                    kEl.innerHTML = `<span class="key-hint">${amChar}</span><span class="key-main">${engChar}</span>`;

                } else if (!state.isNum && state.lang === 'arabic') {

                    if (state.layoutAr === 1) {

                        const engChar = state.isShift ? key.toUpperCase() : key.toLowerCase();

                        let arChar = '';

                        if (AR_MAP[key.toLowerCase()]) {

                            const entry = AR_MAP[key.toLowerCase()];

                            const rawAr = state.isShift ? (entry.s !== undefined ? entry.s : entry.d) : entry.d;

                            arChar = /^[\u064B-\u065F]/.test(rawAr) ? '◌' + rawAr : rawAr;

                        }

                        kEl.innerHTML = `<span class="ar-hint">${arChar}</span><span class="key-main">${engChar}</span>`;

                    } else {

                        kEl.innerHTML = `<span class="ar-main">${key}</span>`;

                    }

                } else {

                    const char = state.isShift ? key.toUpperCase() : key;

                    kEl.innerHTML = `<span class="key-main">${char}</span>`;

                }


                kEl.onpointerdown = (e) => { e.preventDefault(); handleKey(key); };

                rowEl.appendChild(kEl);

            });

            grid.appendChild(rowEl);

        });

    }


    document.addEventListener('keydown', function(e) {

        if (e.ctrlKey || e.metaKey || e.altKey) return;

        let k = e.key;

        if (k === 'Backspace') { e.preventDefault(); handleKey('Back'); return; }

        if (k === 'Enter') { e.preventDefault(); handleKey('Enter'); return; }

        if (k === ' ') { e.preventDefault(); handleKey('Space'); return; }

        if (k === ',') { e.preventDefault(); handleKey('punct_L'); return; }

        if (k === '.') { e.preventDefault(); handleKey('punct_R'); return; }

        if (['Shift', 'CapsLock'].includes(k)) return;

        state.isShift = e.shiftKey || e.getModifierState("CapsLock");

        

        if (k.length === 1) {

            e.preventDefault();

            handleKey(k); 

            const btn = document.querySelector(`.key[data-key="${k.toLowerCase()}"]`);

            if (btn) { btn.classList.add('pressed'); setTimeout(() => btn.classList.remove('pressed'), 100); }

        }

    });


    render();

</script>

</body>

</html>