diff --git a/php-sendmail.php b/php-sendmail.php
old mode 100644
new mode 100755
diff --git a/viewer.css b/viewer.css
index 44e91c67e17a816820100ea0b41ab13efb2e5b92..1d1b8b0ebe3a140aaa64f13755e5dad2b22224ec 100644
--- a/viewer.css
+++ b/viewer.css
@@ -1,37 +1,69 @@
+:root{
+
+    --darker-bg: #e0e0e8;
+
+
+    --main-bg: #f8f8f8;
+    --main-color: #333;
+    --a-color: #44c;
+
+    --titlebar-bg: #779;
+    --titlebar-border-bottom: 4px solid rgba(0,0,0,0.2);
+    --titlebar-color: #fff;
+
+    --msg-active-bg: #fff;
+    --msg-hover-bg: #fcfcfc;
+
+    --box-bg: var(--darker-bg);
+
+    --button-bg: #e8e8f0;
+    --button-color: #667;
+    --button-close-bg: #fff;
+    --button-close-color: #633;
+
+    --footer-bg: #fff;
+
+    --pre-bg: #e8ecec;
+
+    --simglemail-bg: #f8f8f8;
+    --simglemail-head-bg: #fff;
+
+    --table-head-bg: var(--darker-bg);
+    --table-link-color: #447;
+
+}
 
 body{
-    background-color: #f8f8f8;
-    color: #333;
+    background-color: var(--main-bg);
+    color: var (--main-color);
     font-family: Arial, Helvetica, sans-serif;
     font-size: 1.0m;
     margin: 0;
 }
+
 a{
-    color: #44c;
+    color: var(--a-color);
 }
 a.button{
-    background-color: #cce;
-    background: linear-gradient(#ccd,  #dde);
-
-    border: 3px solid rgba(0,0,0,0.05);
-    box-shadow: 0 0 1em #ccc;
-    color: #444;
+    background: var(--button-bg);
+    border: 2px solid rgba(0,0,0,0.05);
+    border-radius: 0.3em;
+    color: var(--button-color);
     text-decoration: none;
-    padding: 0.2em 0.4em;
-    border-radius: 0.5em;
+    padding: 0.4em;
     transition: all 0.2s linear;
 }
 a.button.close{
-    background-color: #ebb;
-    background: linear-gradient(#ebb,  #fca);
-    color: #633;
+    background: var(--button-close-bg);
+    color: var(--button-close-color);
 }
 a.button:hover{
     box-shadow: none;
-    border: 3px solid #fff;
+    border-color: rgba(0,0,80,0.3);
 }
+
 footer{
-    background-color: #fff;
+    background-color: var(--footer-bg);
     position: fixed; bottom: 0;
     text-align: center;
     width: 100%;
@@ -40,69 +72,83 @@ footer{
 }
 
 h1{
-    background-color: #ebb;
-    background: linear-gradient(to right, #9be, #fcc);
+    background: var(--titlebar-bg);
     margin: 0;
     padding-left: 0.5em;
-    border-bottom: 4px solid rgba(0,0,0,0.05);
+    border-bottom: var(--titlebar-border-bottom);
 }
 h1 a{
-    color:#424;
+    color: var(--titlebar-color);
     text-decoration: none;
 }
 
+
 pre{
-    background-color: #e8ecec;
+    background-color: var(--pre-bg);
     overflow: scroll;
     padding: 1em;
     border-radius: 1em;
 }
-.email{
-    border-bottom: 1px solid #ddd;
-    border-radius: 1em;
-    padding: 0.3em 1em;
+
+th{
+    background-color: var(--table-head-bg);
+    padding: 0.5em;
 }
-.email.open{
-    background-color: #fff;
+th.date{
+    width: 10em;
 }
-.email a{
-    color:#446;
-    text-decoration: none;
+td {
+    padding: 0.0;
 }
-.email:hover{
-    background-color: #ddd;
+td a{
+    color: var(--table-link-color);
+    text-decoration: none;
 }
-.right{
-    float: right;
+td a, td span{
+    display: block;
+    padding: 0.5em;
 }
 
 
+
 #messages{
-    background-color: #f0f0f0;
-    border: 2px solid rgba(0,0,0,0.1);
-    border-radius: 1em;
     margin: 1em;
-    padding: 0.5em;
+    margin-bottom: 8em;
 }
 #messages a{
-    display: block;;
+    display: block;
+}
+
+#messages table{
+    border: 0;
+    border-collapse: collapse;    
+    min-width: 47%;
 }
+#messages tr.active{
+    background-color: var(--msg-active-bg);
+    border-radius: 1em;
+}
+#messages tr:hover{
+    background-color: var(--msg-hover-bg);
+}
+
+
 #singlemessage{
-    background-color: #f8f8f8;
-    box-shadow: 0 0 3em #555;
+    background: var(--simglemail-bg);
+    border-top-left-radius: 1em;
+    border: 3px solid rgba(0,0,0,0.2);
+    box-shadow: -0.3em 0 3em rgba(0,0,0,0.3);
     display: block;
     padding: 0;
-    height: 100%;
     overflow: scroll;
     position: absolute;
-    right: 0;
-    top: 0em;
+    right: 1em;
+    top: 3em;
+    bottom: 4em;
     width: 50%;
-    border-bottom-left-radius: 2em;
 }
 #singlemessage .header{
-    background-color: #cdc;
-    background: linear-gradient(to right, #cec, #cee);
+    background: var(--simglemail-head-bg);
     width: 100%;
     padding: 0.5em;
     border-bottom: 4px solid rgba(0,0,0,0.05);
@@ -115,4 +161,26 @@ pre{
 }
 #singlemessage .content{
     padding: 0.5em;
-}
\ No newline at end of file
+}
+#search{
+    border: 2px solid #ddd;
+    border-radius: 0.5em;
+    margin-top: 1em;
+    margin-left: 3em;
+    padding: 0.5em;
+    font-size: 110%;
+}
+
+.box{
+    display: block;
+    float: left;
+    margin: 0 1em 1em 0;
+    padding: 1em;
+    background-color: var(--box-bg);
+    border-radius: 0.5em;
+    text-align: center;
+}
+
+.right{
+    float: right;
+}
diff --git a/viewer.js b/viewer.js
new file mode 100644
index 0000000000000000000000000000000000000000..e9389ecd6d784bfb254627e7740da195a050f7f9
--- /dev/null
+++ b/viewer.js
@@ -0,0 +1,54 @@
+/*
+
+PHP EMAIL CATCHER
+
+search functionality
+
+*/
+
+// ----------------------------------------------------------------------
+// cnstants
+// ----------------------------------------------------------------------
+const tableId = 'messagestable';
+const searchId = 'search';
+const lsVar = 'searchEmailCatcher';
+
+
+// ----------------------------------------------------------------------
+// functions
+// ----------------------------------------------------------------------
+
+/**
+ * read search field and hide non matching rows
+ * @returns void
+ */
+function filterTable(){
+    var sFilter=document.getElementById(searchId).value;
+    localStorage.setItem(lsVar, sFilter);
+    var table=document.getElementById(tableId);
+    if (!table){
+        return false;
+    }
+    var rows=table.rows;
+    for(var i=1;i<rows.length;i++){
+        if(rows[i].innerText.toLowerCase().indexOf(sFilter.toLowerCase()) == -1){
+            rows[i].style.display='none';
+        }
+        else{
+            rows[i].style.display='table-row';
+        }
+    }
+}
+
+
+// ----------------------------------------------------------------------
+// main
+// ----------------------------------------------------------------------
+
+document.getElementById(searchId).value=''+localStorage.getItem(lsVar);
+
+document.getElementById(searchId).addEventListener('keyup', filterTable);
+document.getElementById(searchId).addEventListener('keypress', filterTable);
+filterTable();
+
+// ----------------------------------------------------------------------
diff --git a/viewer.php b/viewer.php
index 15ef6759d70f7bca6d437fd0cdcea7dc2b31736e..f2307fcbf63659fa9196b69151a9880a1582de23 100644
--- a/viewer.php
+++ b/viewer.php
@@ -12,7 +12,8 @@
  * 
  * ----------------------------------------------------------------------
  * 2024-10-08  v0.1  initial version
- * 2024-10-09  v0.1  add links
+ * 2024-10-09  v0.2  add links
+ * 2024-10-21  v0.3  add tiles on top; add email search
  * =======================================================================
  */
 require_once('classes/emailcatcher.class.php');
@@ -34,13 +35,13 @@ function showEmail($sId)
     $sReturn='';
     $oMail=new emailcatcher();
     if(!$oMail->setId($sId)){
-        $sReturn.="❌ ERROR: wrong email id: $sId<br>";
+        $sReturn.="❌ ERROR: Unable to open non existing email id<br>";
     } else {
 
         $bIsHtml=strstr( $oMail->getBody(), '<html>');
         $sReturn.= '<div id="singlemessage">
             <div class="header">
-                <span class="right"><a href="?" class="button close">❌ Close</a>&nbsp;&nbsp;&nbsp;</span>
+                <span class="right"><a href="?" class="button close">❌</a>&nbsp;&nbsp;&nbsp;</span>
                 <table>
                     <tr><td class="small">🕜 DATE</td><td>'.$oMail->getField('date').'</td></tr>
                     <tr><td class="small">👤 TO</td><td>'.$oMail->getField('to').'</td></tr>
@@ -71,11 +72,11 @@ function showHtmlEmail($sId): void
     $oMail=new emailcatcher();
     echo '<button onclick="history.back();return false;">back</button><br>';
     if(!$oMail->setId($sId)){
-        echo "❌ ERROR: wrong email id: $sId<br>";
+        echo "❌ ERROR: Unable to open non existing email id<br>";
     } else {
         echo $oMail->getBody();
-        die();
     }
+    die();
 }
 // ----------------------------------------------------------------------
 // MAIN
@@ -100,48 +101,82 @@ if(!count($aEmails)){
     }
 
     // show list of emails
-    $sOut='Messages: <strong>'.count($aEmails).'</strong><br>';
+    $sTable='';
+    $sLatest='';
     foreach($aEmails as $aEmail){
 
+        // --- age of last email
         $sId=$aEmail['id'];
+        if(!$sLatest){
+            $iAge=date('U') - date('U', strtotime($aEmail['date']));
+            $sLatest='Less than 1 min ago.';
+            if($iAge>60){
+                $sLatest=round($iAge / 60).' min ago';
+            }
+            if($iAge>60*60){
+                $sLatest=round($iAge / 60 / 60 ).' h ago';
+            }
+            if($iAge>60*60*24){
+                $sLatest=round($iAge / 60 / 60 / 24 ).' d ago';
+            }
+        }
 
-        $sOut.='
-        <div id="'.$sId.'" class="email'.($sId==$sOpen ? ' open':'').'">
-            '.(
-                $sId!=$sOpen
-                    ? '<a href="?open='.$sId.'">✉️ '.$aEmail['date'].' - to '.$aEmail['to'].': '.$aEmail['subject'].'</a>'
-                    : '🔶 '. $aEmail['date'].' - to '.$aEmail['to'].': '.$aEmail['subject']
-            )
-        .'</div>';
-
+        // --- table with emails
+        $sTable.=($sId!=$sOpen 
+            ? '<tr>
+                <td><a href="?open='.$sId.'">✉️ '.htmlentities($aEmail['subject']).'</a></td>
+                <td><a href="?open='.$sId.'">'.htmlentities($aEmail['to']).'</a></td>
+                <td><a href="?open='.$sId.'">'.$aEmail['date'].'</a></td>
+                </tr>
+                '
+            : '<tr class="active">
+                <td><span>🔶 '. htmlentities($aEmail['subject']).'</span></td>
+                <td><span>'.htmlentities($aEmail['to']).'</span></td>
+                <td><span>'.$aEmail['date'].'</span></td>
+            </tr>'
+        );
     }
+    $sOut='<div class="box">Messages<br><strong>'.count($aEmails).'</strong></div>'
+        .'<div class="box">Last<br><strong>'.$sLatest .'</strong></div>'
+        . '<div><input type="text" id="search" size="30" placeholder="Search..."></div>'
+        . '<br><br>' 
+        ;
+    $sOut.='<table id="messagestable">
+    <thead>
+        <tr><th>Subject</th><th>To</th><th class="date">Date</th></tr>
+    </thead>
+    <tbgody>'
+    .$sTable
+    .'</tbody></table>' 
+    ;
 }
 
 
-
+// ----------------------------------------------------------------------
 // write html page
+// ----------------------------------------------------------------------
 
-echo "<!doctype html>
+?><!doctype html>
 <html>
 <head>
     <title>Email catcher :: viewer</title>
-    <link rel=\"stylesheet\" href=\"viewer.css\">
+    <link rel="stylesheet" href="viewer.css">
+
 </head>
 <body>
-    <h1><a href=\"?\">🕶️ Email catcher :: viewer</a></h1>
+    
+    <h1><a href="?">🕶️ Email catcher :: viewer</a></h1>
 
-    <div id=\"messages\">$sOut</div>
+    <div id="messages"><?php echo $sOut ?></div>
 
     <footer>
         Email catcher
-        📄 <a href=\"https://git-repo.iml.unibe.ch/iml-open-source/php-emailcatcher\" target=\"source\">source</a>
-        📗 <a href=\"https://os-docs.iml.unibe.ch/php-emailcatcher/\" target=\"docs\">docs</a>
+        📄 <a href="https://git-repo.iml.unibe.ch/iml-open-source/php-emailcatcher" target="source">source</a>
+        📗 <a href="https://os-docs.iml.unibe.ch/php-emailcatcher/" target="docs">docs</a>
     </footer>
 
-    $sMessage
+    <?php echo $sMessage ?>
+    <script src="viewer.js"></script>
 
 </body>
 </html>
-";
-
-// ----------------------------------------------------------------------