Web implementation appears
This commit is contained in:
@@ -282,6 +282,15 @@ Reference implementations
|
||||
program file name is optional and used for preloading MU8 program files into
|
||||
VM's memory. The Busybox AWK implementation is compact but quite slow, so use
|
||||
it as a last resort solution when no other programming environment is available.
|
||||
* [Web (HTML5/JS) implementation](https://mu808.luxferre.top) aka mu8web:
|
||||
supports the entire mu808 spec except the I/O port 2 and offers a way to
|
||||
load program files in the same MU8 format via a convenient UI.
|
||||
Uses a convoluted flow to achieve true synchronous input.
|
||||
Since there's no exiting to the OS environment, the -5 instruction number
|
||||
resets the VM instead (by zeroing out both memory areas and the counter).
|
||||
Entry conventions within UI are pretty much the same as for 808UL. Tested
|
||||
on various devices with physical keypads and touch screens. The source code
|
||||
can be found in the [mu8web](mu8web/) directory of this repo.
|
||||
|
||||
### Assembler reference implementations
|
||||
|
||||
@@ -293,6 +302,29 @@ Reference implementations
|
||||
These lists are going to be expanded as soon as new reference implementations
|
||||
appear.
|
||||
|
||||
### Web reference implementation keyboard controls
|
||||
|
||||
The virtual keyboard (unavailable on the smallest screens) is duplicated with
|
||||
the following keys at all times in the mu808 Web reference implementation:
|
||||
|
||||
* `0`-`9`: decimal digits input;
|
||||
* `*` (star), comma, dot: decimal point input;
|
||||
* Space, `+`, `/`, down arrow, Call key (if present): whitespace input;
|
||||
* Backspace, Delete, left arrow, `-`: minus or backspace input (as the minus sign
|
||||
can only be entered at the beginning of the instruction or data input line);
|
||||
* `#` (pound, hash sign), Enter: newline input;
|
||||
* `l`, `L`, up arrow, Left Soft key (if present): open a program file loading dialogue.
|
||||
|
||||
### 12-key (DTMF) input method proposal
|
||||
|
||||
For hobbyist projects and other environments where all input is limited to 12
|
||||
keys of the standard phone keypad, the following convention is recommended:
|
||||
|
||||
* the `*` (star) key is inputting a minus if this is the first position in
|
||||
the entry, otherwise it's inputting a decimal point;
|
||||
* the `#` (pound) key actually confirms entering a single number field, and
|
||||
all instructions are required to contain exactly five numbers.
|
||||
|
||||
FAQ
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="width=device-width">
|
||||
<title>mu808 VM (Web version)</title>
|
||||
<link rel=icon href=icon.png />
|
||||
<link rel=stylesheet href=style.css />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>mu808</h1>
|
||||
</header>
|
||||
<main>
|
||||
<hr>
|
||||
<section id=workspace>
|
||||
<pre id=screenfield></pre>
|
||||
<input type=file id=programloadfield />
|
||||
</section>
|
||||
<section id=keypad>
|
||||
<div>
|
||||
<button type=button id=b1>1</button>
|
||||
<button type=button id=b2>2</button>
|
||||
<button type=button id=b3>3</button>
|
||||
<button type=button id=bprogload>LOAD</button>
|
||||
</div>
|
||||
<div>
|
||||
<button type=button id=b4>4</button>
|
||||
<button type=button id=b5>5</button>
|
||||
<button type=button id=b6>6</button>
|
||||
<button type=button id=bbksp>(-)/←</button>
|
||||
</div>
|
||||
<div>
|
||||
<button type=button id=b7>7</button>
|
||||
<button type=button id=b8>8</button>
|
||||
<button type=button id=b9>9</button>
|
||||
<button type=button id=benter>ENT</button>
|
||||
</div>
|
||||
<div>
|
||||
<button type=button id=bpoint>.</button>
|
||||
<button type=button id=b0>0</button>
|
||||
<button type=button id=bspc>SPACE</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<hr><div><a href="https://codeberg.org/luxferre/mu808">Created</a> by <a href="https://luxferre.top">Luxferre</a> in 2025</div>
|
||||
</footer>
|
||||
<script src=mu808-core.js></script>
|
||||
<script src=mu808-ui.js></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* mu808: an ultralight numeric-only VM
|
||||
* This is the core JS file for the reference implementation
|
||||
* of mu808 VM for the Web and other ES5 engines
|
||||
* See the README.md file in the repo root for all documentation
|
||||
* Created by Luxferre in 2025, released into public domain
|
||||
*/
|
||||
|
||||
/* Due to the modular nature, this file only defines the mu808 VM kernel
|
||||
* which accepts all I/O routines as the parameters for its construction */
|
||||
function createmu808(lineOut, charOut, lineIn) {
|
||||
var MEMLIMIT = 16384,
|
||||
PMEM = new Uint16Array(MEMLIMIT << 2), /* program memory */
|
||||
DMEM = new Float64Array(MEMLIMIT), /* data memory */
|
||||
runlimit = MEMLIMIT,
|
||||
traceflag = false,
|
||||
runcount = 0
|
||||
|
||||
/* memory range input function */
|
||||
/* accepts the continuation callback as the last parameter */
|
||||
function dataRangeInput(start, end, contfunc) {
|
||||
(function inp(loc) {
|
||||
lineIn(function(v) {
|
||||
DMEM[loc] = parseFloat(v) || 0.0
|
||||
if(loc < end) inp(loc+1)
|
||||
else {
|
||||
if(contfunc) contfunc()
|
||||
}
|
||||
})
|
||||
})(start)
|
||||
}
|
||||
|
||||
/* port output function */
|
||||
function portout(port, data) {
|
||||
if(port == 0) lineOut(data + "\n")
|
||||
else if(port == 1) charOut(data & 255)
|
||||
}
|
||||
|
||||
/* instruction line execution function (the main 808UL logic defined here) */
|
||||
/* accepts the continuation callback as the last parameter */
|
||||
function ilexec(lno, cmd, x, y, z, contfunc) {
|
||||
var halt = 0, data_override = 0, bcheck, i, v1, v2, v3
|
||||
while(halt == 0) {
|
||||
v1 = v2 = v3 = data_override = DMEM[0] = 0 /* force the value at 0 to be always 0 */
|
||||
if(lno == 0) halt = 1 /* immediate instruction */
|
||||
if(runlimit > 0 && runcount > runlimit) {
|
||||
halt = 1
|
||||
if(traceflag) lineOut("Runlimit hit, halting...\n")
|
||||
} else runcount++
|
||||
if(traceflag)
|
||||
lineOut("PC: " + lno + " INSTR: " + [cmd, x, y, z].join(" ") + "\n")
|
||||
bcheck = (x < MEMLIMIT) && (y < MEMLIMIT) && (z < MEMLIMIT)
|
||||
if(bcheck) { /* boundary checks passed */
|
||||
v1 = DMEM[x]; v2 = DMEM[y]; v3 = DMEM[z] /* prefetch the memory values */
|
||||
if(cmd == 1) { /* JMP */
|
||||
if((v2 == 0 && x == 0) || (v2 > 0 && x == 1) || (v2 < 0 && x == 2)
|
||||
|| (v2 >= 0 && x == 3) || (v2 <= 0 && x == 4) || (v2 != 0 && x == 5)
|
||||
|| x == 6) { halt = 0; lno = z - 1 }
|
||||
else if((v2 == 0 && x == 7) || (v2 > 0 && x == 8) || (v2 < 0 && x == 9)
|
||||
|| (v2 >= 0 && x == 10) || (v2 <= 0 && x == 11) || (v2 != 0 && x == 12)
|
||||
|| x == 13) { halt = 0; lno = (v3|0) - 1 }
|
||||
} else if(cmd == 2) data_override = 1 /* IAT */
|
||||
else if(cmd == 3) for(i=x;i<=y;i++) portout(z, DMEM[i]) /* OUT */
|
||||
else if(cmd == 4) { /* INP */
|
||||
dataRangeInput(x, y, function() {
|
||||
/* continuation function duplicating what's after the switch */
|
||||
/* except the data override branch */
|
||||
lno++ /* increment the program counter */
|
||||
if(lno >= MEMLIMIT || lno < 1 || (runlimit > 0 && runcount > runlimit)) {
|
||||
if(traceflag) lineOut("Memory limit or runlimit hit, halting...\n")
|
||||
halt = 1
|
||||
} else { /* fetch the next instruction and execute it */
|
||||
cmd = PMEM[lno << 2]
|
||||
x = PMEM[(lno << 2) + 1]
|
||||
y = PMEM[(lno << 2) + 2]
|
||||
z = PMEM[(lno << 2) + 3]
|
||||
ilexec(lno, cmd, x, y, z, contfunc)
|
||||
}
|
||||
})
|
||||
return
|
||||
} else if(cmd == 5) DMEM[z] = (x % 10000) + (y % 10000) / 10000.0 /* SET */
|
||||
else if(cmd == 6) { /* CPY */
|
||||
if(x == 0) DMEM[z] = y
|
||||
else if(x == 1) DMEM[z] = v2
|
||||
else if(v2 < MEMLIMIT && v3 < MEMLIMIT) DMEM[0|v3] = DMEM[0|v2]
|
||||
} else if(cmd == 7) DMEM[z] = v1 + v2 * v3 /* FMA */
|
||||
else if(cmd == 8) DMEM[z] = v1 - v2 /* SUB */
|
||||
else if(cmd == 9) DMEM[z] = (v2 == 0) ? 0 : (v1 / v2) /* DIV */
|
||||
else if(cmd == 10) DMEM[z] = (v2 == 0) ? (0|v1) : ((0|v1) % (0|v2)) /* MDF */
|
||||
else if(cmd == 11) DMEM[z] = Math.abs(v2) /* ABS */
|
||||
else if(cmd == 12) DMEM[z] = Math.sqrt(Math.abs(v2)) /* SQR */
|
||||
else if(cmd == 13) { /* NEL */
|
||||
if(x == 0) DMEM[z] = Math.exp(v2)
|
||||
else DMEM[z] = (v2 == 0) ? 0 : Math.log(Math.abs(v2))
|
||||
} else if(cmd == 14) { /* TRI */
|
||||
if(x == 0) DMEM[z] = Math.sin(v2)
|
||||
else if(x == 1) DMEM[z] = Math.cos(v2)
|
||||
else DMEM[z] = Math.atan(v2)
|
||||
} else if(cmd == 15) { /* RND */
|
||||
v1 = 0|v1
|
||||
DMEM[z] = v1 + (0|(Math.random() * ((0|v2) - v1 + 1)))
|
||||
}
|
||||
} /* command switch ends here */
|
||||
lno++ /* increment the program counter */
|
||||
if(lno >= MEMLIMIT || lno < 1 || (runlimit > 0 && runcount > runlimit)) {
|
||||
if(traceflag) lineOut("Memory limit or runlimit hit, halting...\n")
|
||||
halt = 1
|
||||
} else { /* fetch the next instruction */
|
||||
if(data_override && bcheck) {
|
||||
cmd = PMEM[lno << 2]
|
||||
x = (0|v1) % MEMLIMIT
|
||||
y = (0|v2) % MEMLIMIT
|
||||
z = (0|v3) % MEMLIMIT
|
||||
} else {
|
||||
cmd = PMEM[lno << 2]
|
||||
x = PMEM[(lno << 2) + 1]
|
||||
y = PMEM[(lno << 2) + 2]
|
||||
z = PMEM[(lno << 2) + 3]
|
||||
}
|
||||
}
|
||||
}
|
||||
if(contfunc) contfunc()
|
||||
}
|
||||
|
||||
/* instruction line entry function (interactive mode) */
|
||||
/* accepts the continuation callback as the last parameter */
|
||||
function ilenter(lno, cmd, x, y, z, contfunc) {
|
||||
if(lno > 0) { /* record the instruction in memory */
|
||||
PMEM[lno << 2] = cmd
|
||||
PMEM[(lno << 2) + 1] = x
|
||||
PMEM[(lno << 2) + 2] = y
|
||||
PMEM[(lno << 2) + 3] = z
|
||||
if(contfunc) contfunc()
|
||||
} else if(lno == 0) { /* immediate execution */
|
||||
runcount = 0
|
||||
ilexec(lno, cmd, x, y, z, contfunc)
|
||||
} else if(lno == -1) { /* display a range of instructions from cmd to x */
|
||||
if(cmd < MEMLIMIT && x < MEMLIMIT) for(y=cmd;y<=x;y++) {
|
||||
z = y << 2
|
||||
lineOut("@" + y + ":\t" + [PMEM[z], PMEM[z+1], PMEM[z+2], PMEM[z+3]].join(' ') + '\n')
|
||||
}
|
||||
if(contfunc) contfunc()
|
||||
} else if(lno == -2) { /* clear a range of data or instructions from x to y */
|
||||
if(cmd < MEMLIMIT && x < MEMLIMIT) for(z=x;z<=y;z++) {
|
||||
if(cmd == 0) PMEM[z*4] = PMEM[z*4+1] = PMEM[z*4+2] = PMEM[z*4+3] = 0
|
||||
else DMEM[z] = 0.0
|
||||
}
|
||||
if(contfunc) contfunc()
|
||||
} else if(lno == -3) { /* tracing on/off */
|
||||
if(cmd == 0) {traceflag = 0; lineOut("Tracing off\n")}
|
||||
else {traceflag = 1; lineOut("Tracing on\n")}
|
||||
if(contfunc) contfunc()
|
||||
} else if(lno == -4) { /* set the runlimit to the value of cmd */
|
||||
runlimit = cmd
|
||||
lineOut("Runlimit set to " + cmd + "\n")
|
||||
if(contfunc) contfunc()
|
||||
} else if(lno == -5) { /* can't exit to the environment, reset instead */
|
||||
lineOut("Resetting the machine...\n")
|
||||
for(y=0;y<MEMLIMIT;y++) {
|
||||
DMEM[y] = 0.0
|
||||
PMEM[y << 2] = 0
|
||||
PMEM[(y << 2) + 1] = 0
|
||||
PMEM[(y << 2) + 2] = 0
|
||||
PMEM[(y << 2) + 3] = 0
|
||||
}
|
||||
runcount = 0
|
||||
runlimit = MEMLIMIT
|
||||
traceflag = false
|
||||
lineOut("Machine reset complete\n")
|
||||
if(contfunc) contfunc()
|
||||
}
|
||||
}
|
||||
|
||||
/* now, export the ilenter function as the outside API, as well as debug methods */
|
||||
return {
|
||||
ilenter: ilenter,
|
||||
savemem: function() {return {"PMEM":PMEM,"DMEM":DMEM}},
|
||||
loadmem: function(memobj) {
|
||||
PMEM.set(memobj.PMEM)
|
||||
DMEM.set(memobj.DMEM)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
;(function(w, d) {
|
||||
|
||||
var screenBuffer = "", inputBuffer = "", screenDom, inputRes = "",
|
||||
floader, vm
|
||||
|
||||
/* screen rendering routine */
|
||||
function renderScreen() {
|
||||
screenDom.textContent = screenBuffer + inputBuffer + "\u258f"
|
||||
screenDom.scrollTop = screenDom.scrollHeight
|
||||
}
|
||||
|
||||
/* external program loading routine */
|
||||
function loadProgFileHandler(e) {
|
||||
if(e.target.files.length > 0) {
|
||||
var fobj = e.target.files[0], rdr = new FileReader()
|
||||
rdr.onload = function() {
|
||||
var progsrc = rdr.result
|
||||
if(progsrc.length > 0) {
|
||||
var instrs = progsrc.split(/\s+/), i, l = instrs.length,
|
||||
instr, lno, cmd, x, y, z, icount = 0
|
||||
for(i=0;i<l;i++) {
|
||||
instr = parseInt(instrs[i] || 0)
|
||||
if(icount == 0) lno = instr
|
||||
else if(icount == 1) cmd = instr
|
||||
else if(icount == 2) x = instr
|
||||
else if(icount == 3) y = instr
|
||||
else if(icount == 4) z = instr
|
||||
icount++
|
||||
if(icount == 5) {
|
||||
vm.ilenter(lno, cmd, x, y, z)
|
||||
icount = 0
|
||||
}
|
||||
}
|
||||
lineOut("\nProgram " + fobj.name + " loaded successfully\n> ")
|
||||
}
|
||||
}
|
||||
if(fobj) rdr.readAsText(fobj)
|
||||
}
|
||||
}
|
||||
|
||||
/* main input routine:
|
||||
* digits 0 to 9, 10 is space,
|
||||
* 11 is minus/backspace, 12 is enter, 13 is decimal point,
|
||||
* 14 is program load action */
|
||||
function doInput(number) {
|
||||
if(number >= 0 && number < 10) /* normal digit */
|
||||
inputBuffer += ("" + number)
|
||||
else if(number == 10) /* space */
|
||||
inputBuffer += " "
|
||||
else if(number == 11) { /* backspace or minus */
|
||||
if(inputBuffer.length == 0)
|
||||
inputBuffer += "-"
|
||||
else
|
||||
inputBuffer = inputBuffer.slice(0, inputBuffer.length - 1)
|
||||
} else if(number == 12) { /* enter */
|
||||
screenBuffer += inputBuffer + "\n"
|
||||
inputRes = inputBuffer
|
||||
inputBuffer = ""
|
||||
} else if(number == 13) /* decimal point */
|
||||
inputBuffer += "."
|
||||
else if(number == 14) floader.click() /* trigger program loader */
|
||||
renderScreen()
|
||||
}
|
||||
|
||||
function setupInput() {
|
||||
/* setup physical keys */
|
||||
w.addEventListener("keydown", function(e) {
|
||||
switch(e.key) {
|
||||
case "0":
|
||||
case "1":
|
||||
case "2":
|
||||
case "3":
|
||||
case "4":
|
||||
case "5":
|
||||
case "6":
|
||||
case "7":
|
||||
case "8":
|
||||
case "9":
|
||||
e.preventDefault()
|
||||
doInput(parseInt(e.key))
|
||||
break
|
||||
case "+":
|
||||
case "/":
|
||||
case " ":
|
||||
case "Call":
|
||||
case "ArrowDown":
|
||||
e.preventDefault()
|
||||
doInput(10)
|
||||
break
|
||||
case "Backspace":
|
||||
case "Back":
|
||||
case "ArrowLeft":
|
||||
case "Delete":
|
||||
case "-":
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
doInput(11)
|
||||
break
|
||||
case "#":
|
||||
case "Enter":
|
||||
e.preventDefault()
|
||||
doInput(12)
|
||||
break
|
||||
case "*":
|
||||
case ",":
|
||||
case ".":
|
||||
e.preventDefault()
|
||||
doInput(13)
|
||||
break
|
||||
case "l":
|
||||
case "L":
|
||||
case "ArrowUp":
|
||||
case "SoftLeft":
|
||||
e.preventDefault()
|
||||
doInput(14)
|
||||
break
|
||||
}
|
||||
}, false)
|
||||
/* setup virtual keys */
|
||||
w.addEventListener("click", function(e) {
|
||||
switch(e.target.id) {
|
||||
case "b0":
|
||||
case "b1":
|
||||
case "b2":
|
||||
case "b3":
|
||||
case "b4":
|
||||
case "b5":
|
||||
case "b6":
|
||||
case "b7":
|
||||
case "b8":
|
||||
case "b9":
|
||||
e.preventDefault()
|
||||
doInput(parseInt(e.target.id.slice(1)))
|
||||
break
|
||||
case "bspc":
|
||||
e.preventDefault()
|
||||
doInput(10)
|
||||
break
|
||||
case "bbksp":
|
||||
e.preventDefault()
|
||||
doInput(11)
|
||||
break
|
||||
case "benter":
|
||||
e.preventDefault()
|
||||
doInput(12)
|
||||
break
|
||||
case "bpoint":
|
||||
e.preventDefault()
|
||||
doInput(13)
|
||||
break
|
||||
case "bprogload":
|
||||
e.preventDefault()
|
||||
doInput(14)
|
||||
break
|
||||
}
|
||||
}, false)
|
||||
}
|
||||
|
||||
function lineOut(s) { /* line output routine */
|
||||
screenBuffer += s
|
||||
renderScreen()
|
||||
}
|
||||
|
||||
function charOut(c) { /* character output routine */
|
||||
screenBuffer += String.fromCharCode(+c)
|
||||
renderScreen()
|
||||
}
|
||||
|
||||
function lineIn(cb) {
|
||||
inputRes = ""
|
||||
var inv = setInterval(function resWait() {
|
||||
if(inputRes != "") {
|
||||
clearInterval(inv)
|
||||
cb(inputRes)
|
||||
inputRes = ""
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function main() { /* UI entry point */
|
||||
screenDom = d.getElementById("screenfield")
|
||||
floader = d.getElementById("programloadfield")
|
||||
floader.addEventListener("change", loadProgFileHandler, false)
|
||||
setupInput()
|
||||
prompt = "> "
|
||||
vm = createmu808(lineOut, charOut, lineIn)
|
||||
lineOut("mu808 v1 by Luxferre\nPROGMEM: 16383 steps\nDATAMEM: 16384 floats\n")
|
||||
;(function repl() {
|
||||
lineOut(prompt)
|
||||
lineIn(function(entryString) {
|
||||
var eparts = entryString.split(/[^\-0-9]+/).slice(0, 5),
|
||||
lno = parseInt(eparts[0] || 0),
|
||||
cmd = parseInt(eparts[1] || 0),
|
||||
x = parseInt(eparts[2] || 0),
|
||||
y = parseInt(eparts[3] || 0),
|
||||
z = parseInt(eparts[4] || 0)
|
||||
vm.ilenter(lno, cmd, x, y, z, repl)
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
w.addEventListener("DOMContentLoaded", main, false)
|
||||
})(window, document)
|
||||
@@ -0,0 +1,83 @@
|
||||
* {box-sizing: border-box; margin: 0; padding: 0}
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "Noto Sans", Roboto, "Droid Sans", sans-serif;
|
||||
font-size: 12pt;
|
||||
text-align: center;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
#workspace {
|
||||
padding: 1rem 0.6rem;
|
||||
}
|
||||
|
||||
#screenfield {
|
||||
display: inline-block;
|
||||
min-width: 240px;
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
font-family: "Fira Code", "Droid Sans Mono", monospace;
|
||||
font-size: inherit;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
overflow-y: scroll;
|
||||
text-align: left;
|
||||
text-wrap: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#keypad {
|
||||
display: inline-block;
|
||||
width: 50vw;
|
||||
min-width: 206px;
|
||||
max-width: 400px;
|
||||
height: 20vh;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
#keypad, #keypad * {
|
||||
display:none!important;
|
||||
}
|
||||
#workspace {
|
||||
padding: 0.5rem 0!important;
|
||||
}
|
||||
}
|
||||
|
||||
#keypad div {
|
||||
padding: 1px 0;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
#keypad button {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
width: calc(25% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#keypad button:hover {
|
||||
color: cyan;
|
||||
border-color: cyan;
|
||||
}
|
||||
|
||||
#keypad #bspc {
|
||||
width: calc(50% - 4px);
|
||||
}
|
||||
|
||||
header {padding: 0.6rem 0}
|
||||
footer {padding-top: 0.6rem}
|
||||
footer div {padding: 0.6em 0}
|
||||
|
||||
#programloadfield {display:none}
|
||||
|
||||
a,a:active,a:visited {color: inherit}
|
||||
a:hover {color:cyan}
|
||||
Reference in New Issue
Block a user