diff --git a/bootstrap.php b/bootstrap.php
index fe3dd2155b8fdd0e2dbb21d249d5eeba247fadc0..c00e425c35f8278b8f27d7fc1f9edfe01848e071 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -133,6 +133,7 @@ class Bootstrap {
         define('SEQUENCE_TABLE', $prefix.'sequence');
         define('TRANSLATION_TABLE', $prefix.'translation');
         define('QUEUE_TABLE', $prefix.'queue');
+        define('QUEUE_COLUMN_TABLE', $prefix.'queue_column');
 
         define('API_KEY_TABLE',$prefix.'api_key');
         define('TIMEZONE_TABLE',$prefix.'timezone');
diff --git a/include/ajax.search.php b/include/ajax.search.php
index f2175a76dc4b961d7c260b823a8675f4e40fc424..40cd7311050d761e2ad71d76cadbc295db290599 100644
--- a/include/ajax.search.php
+++ b/include/ajax.search.php
@@ -19,6 +19,7 @@ if(!defined('INCLUDE_DIR')) die('403');
 
 include_once(INCLUDE_DIR.'class.ticket.php');
 require_once(INCLUDE_DIR.'class.ajax.php');
+require_once(INCLUDE_DIR.'class.queue.php');
 
 class SearchAjaxAPI extends AjaxController {
 
@@ -30,7 +31,7 @@ class SearchAjaxAPI extends AjaxController {
 
         $search = SavedSearch::create();
         $form = $search->getFormFromSession('advsearch') ?: $search->getForm();
-        $matches = self::_getSupportedTicketMatches();
+        $matches = SavedSearch::getSupportedTicketMatches();
 
         include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
     }
@@ -96,7 +97,7 @@ class SearchAjaxAPI extends AjaxController {
 
         $form = $search->getForm($_POST);
         if (!$form->isValid()) {
-            $matches = self::_getSupportedTicketMatches();
+            $matches = SavedSearch::getSupportedTicketMatches();
             include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
             return;
         }
@@ -148,41 +149,6 @@ class SearchAjaxAPI extends AjaxController {
         )));
     }
 
-    function _getSupportedTicketMatches() {
-        // User information
-        $matches = array(
-            __('Ticket Built-In') => SavedSearch::getExtendedTicketFields(),
-            __('Custom Forms') => array()
-        );
-        foreach (array('ticket'=>'TicketForm', 'user'=>'UserForm', 'organization'=>'OrganizationForm') as $k=>$F) {
-            $form = $F::objects()->one();
-            $fields = &$matches[$form->getLocal('title')];
-            foreach ($form->getFields() as $f) {
-                if (!$f->hasData() || $f->isPresentationOnly())
-                    continue;
-                $fields[":$k!".$f->get('id')] = __(ucfirst($k)).' / '.$f->getLocal('label');
-                /* TODO: Support matches on list item properties
-                if (($fi = $f->getImpl()) && $fi->hasSubFields()) {
-                    foreach ($fi->getSubFields() as $p) {
-                        $fields[":$k.".$f->get('id').'.'.$p->get('id')]
-                            = __(ucfirst($k)).' / '.$f->getLocal('label').' / '.$p->getLocal('label');
-                    }
-                }
-                */
-            }
-        }
-        $fields = &$matches[__('Custom Forms')];
-        foreach (DynamicForm::objects()->filter(array('type'=>'G')) as $form) {
-            foreach ($form->getFields() as $f) {
-                if (!$f->hasData() || $f->isPresentationOnly())
-                    continue;
-                $key = sprintf(':field!%d', $f->get('id'), $f->get('id'));
-                $fields[$key] = $form->getLocal('title').' / '.$f->getLocal('label');
-            }
-        }
-        return $matches;
-    }
-
     function createSearch() {
         global $thisstaff;
 
@@ -208,7 +174,7 @@ class SearchAjaxAPI extends AjaxController {
             $form = $search->loadFromState($state);
             $form->loadState($state);
         }
-        $matches = self::_getSupportedTicketMatches();
+        $matches = SavedSearch::getSupportedTicketMatches();
 
         include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
     }
@@ -231,4 +197,120 @@ class SearchAjaxAPI extends AjaxController {
             'success' => true,
         )));
     }
+
+
+    function editColumn($queue_id, $column) {
+        global $thisstaff;
+
+        if (!$thisstaff) {
+            Http::response(403, 'Agent login is required');
+        }
+        elseif (!($queue = CustomQueue::lookup($queue_id))) {
+            Http::response(404, 'No such queue');
+        }
+
+        $data_form = new QueueDataConfigForm($_POST);
+        include STAFFINC_DIR . 'templates/queue-column.tmpl.php';
+    }
+
+    function previewQueue($id=false) {
+        global $thisstaff;
+
+        if (!$thisstaff) {
+            Http::response(403, 'Agent login is required');
+        }
+        if ($id && (!($queue = CustomQueue::lookup($id)))) {
+            Http::response(404, 'No such queue');
+        }
+
+        if (!$queue) {
+            $queue = CustomQueue::create();
+        }
+
+        $form = $queue->getForm($_POST);
+
+        // TODO: Update queue columns (but without save)
+        foreach ($_POST['columns'] as $colid) {
+            $col = QueueColumn::create(array("id" => $colid));
+            $col->update($_POST);
+            $queue->addColumn($col);
+        }
+
+        $tickets = $queue->getQuery($form);
+        $count = 10; // count($queue->getBasicQuery($form));
+
+        include STAFFINC_DIR . 'templates/queue-tickets.tmpl.php';
+    }
+
+    function addCondition() {
+        global $thisstaff;
+
+        if (!$thisstaff) {
+            Http::response(403, 'Agent login is required');
+        }
+        elseif (!isset($_GET['field'])) {
+            Http::response(400, '`field` parameter is required');
+        }
+        $fields = SavedSearch::getSearchableFields('Ticket');
+        if (!isset($fields[$_GET['field']])) {
+            Http::response(400, sprintf('%s: No such searchable field'),
+                Format::htmlchars($_GET['field']));
+        }
+      
+        $field = $fields[$_GET['field']];
+        $condition = new QueueColumnCondition();
+        include STAFFINC_DIR . 'templates/queue-column-condition.tmpl.php';
+    }
+
+    function addConditionProperty() {
+        global $thisstaff;
+
+        if (!$thisstaff) {
+            Http::response(403, 'Agent login is required');
+        }
+        elseif (!isset($_GET['prop'])) {
+            Http::response(400, '`prop` parameter is required');
+        }
+
+        $prop = $_GET['prop'];
+        include STAFFINC_DIR . 'templates/queue-column-condition-prop.tmpl.php';
+    }
+
+    function addColumn() {
+        global $thisstaff;
+
+        if (!$thisstaff) {
+            Http::response(403, 'Agent login is required');
+        }
+        elseif (!isset($_GET['field'])) {
+            Http::response(400, '`field` parameter is required');
+        }
+
+        $field = $_GET['field'];
+        // XXX: This method should receive a queue ID or queue root so that
+        //      $field can be properly checked
+        $fields = SavedSearch::getSearchableFields('Ticket');
+        if (!isset($fields[$field])) {
+            Http::response(400, 'Not a supported field for this queue');
+        }
+
+        // Get the tabbed column configuration
+        $F = $fields[$field];
+        $column = QueueColumn::create(array(
+            "id"        => (int) $_GET['id'],
+            "heading"   => _S($F->getLabel()),
+            "primary"   => $field,
+            "width"     => 100,
+        ));
+        ob_start();
+        include STAFFINC_DIR .  'templates/queue-column.tmpl.php';
+        $config = ob_get_clean();
+
+        // Send back the goodies
+        Http::response(200, $this->encode(array(
+            'config' => $config,
+            'heading' => _S($F->getLabel()),
+            'width' => $column->getWidth(),
+        )), 'application/json');
+    }
 }
diff --git a/include/class.dept.php b/include/class.dept.php
index 8f09237c4b37fc721d4a9a25367984414e9e9afd..b54913af42ad14783b1b852fc55a9efaa7dab532 100644
--- a/include/class.dept.php
+++ b/include/class.dept.php
@@ -13,9 +13,10 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once INCLUDE_DIR . 'class.search.php';
 
 class Dept extends VerySimpleModel
-implements TemplateVariable {
+implements TemplateVariable, Searchable {
 
     static $meta = array(
         'table' => DEPT_TABLE,
@@ -98,6 +99,17 @@ implements TemplateVariable {
         }
     }
 
+    static function getSearchableFields() {
+        return array(
+            'name' => new TextboxField(array(
+                'label' => __('Name'),
+            )),
+            'manager' => new AgentSelectionField(array(
+                'label' => __('Manager'),
+            )),
+        );
+    }
+
     function getId() {
         return $this->id;
     }
diff --git a/include/class.forms.php b/include/class.forms.php
index c097dd995ab0f8e48336215bb6acf578e0dfb440..9171b5f90978ab2e4cd4d639ad2d08dffcba564e 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -47,7 +47,10 @@ class Form {
     }
 
     function getId() {
-        return static::$id;
+        return @$this->id ?: static::$id;
+    }
+    function setId($id) {
+        $this->id = $id;
     }
 
     function data($source) {
@@ -998,6 +1001,19 @@ class FormField {
         return sprintf($desc, $name, $value);
     }
 
+    function addToQuery($query, $name=false) {
+        return $query->values($name ?: $this->get('name'));
+    }
+
+    /**
+     * Similary to to_php() and parse(), except a row from a queryset is
+     * passed. The value returned should be what would be retured from
+     * parse() or to_php()
+     */
+    function from_query($row, $name=false) {
+        return $row[$name ?: $this->get('name')];
+    }
+
     function getLabel() { return $this->get('label'); }
 
     /**
@@ -1042,12 +1058,25 @@ class FormField {
         $this->getWidget()->value = $value;
     }
 
+    /**
+     * Fetch a pseudo-random id for this form field. It is used when
+     * rendering the widget in the @name attribute emitted in the resulting
+     * HTML. The form element is based on the form id, field id and name,
+     * and the current user's session id. Therefor, the same form fields
+     * will yield differing names for different users. This is used to ward
+     * off bot attacks as it makes it very difficult to predict and
+     * correlate the form names to the data they represent.
+     */
     function getFormName() {
-        if (is_numeric($this->get('id')))
+        $default = $this->get('name') ?: $this->get('id');
+        if ($this->_form && is_numeric($fid = $this->_form->getId()))
+            return substr(md5(
+                session_id() . '-form-field-id-' . $fid . $default), -14);
+        elseif (is_numeric($this->get('id')))
             return substr(md5(
                 session_id() . '-field-id-'.$this->get('id')), -16);
-        else
-            return $this->get('name') ?: $this->get('id');
+
+        return $default;
     }
 
     function setForm($form) {
@@ -1173,17 +1202,6 @@ class FormField {
         return null;
     }
 
-    /**
-     * Indicates if the field provides for searching for something other
-     * than keywords. For instance, textbox fields can have hits by keyword
-     * searches alone, but selection fields should provide the option to
-     * match a specific value or set of values and therefore need to
-     * participate on any search builder.
-     */
-    function hasSpecialSearch() {
-        return true;
-    }
-
     function getConfigurationForm($source=null) {
         if (!$this->_cform) {
             $type = static::getFieldType($this->get('type'));
@@ -1292,10 +1310,6 @@ class TextboxField extends FormField {
         );
     }
 
-    function hasSpecialSearch() {
-        return false;
-    }
-
     function validateEntry($value) {
         parent::validateEntry($value);
         $config = $this->getConfiguration();
@@ -1379,10 +1393,6 @@ class TextareaField extends FormField {
         );
     }
 
-    function hasSpecialSearch() {
-        return false;
-    }
-
     function display($value) {
         $config = $this->getConfiguration();
         if ($config['html'])
@@ -1434,10 +1444,6 @@ class PhoneField extends FormField {
         );
     }
 
-    function hasSpecialSearch() {
-        return false;
-    }
-
     function validateEntry($value) {
         parent::validateEntry($value);
         $config = $this->getConfiguration();
@@ -1847,6 +1853,10 @@ class DatetimeField extends FormField {
                 $value, $datetime->format('T'));
     }
 
+    function from_query($row, $name=false) {
+        return strtotime(parent::from_query($row, $name));
+    }
+
     function format($timestamp, $timezone=false) {
 
         if (!$timestamp || $timestamp <= 0)
@@ -2133,10 +2143,6 @@ class ThreadEntryField extends FormField {
     function isPresentationOnly() {
         return true;
     }
-    function hasSpecialSearch() {
-        return false;
-    }
-
     function getMedia() {
         $config = $this->getConfiguration();
         $media = parent::getMedia() ?: array();
@@ -2753,10 +2759,6 @@ class FileUploadField extends FormField {
         );
     }
 
-    function hasSpecialSearch() {
-        return false;
-    }
-
     /**
      * Called from the ajax handler for async uploads via web clients.
      */
@@ -3037,6 +3039,10 @@ class FileFieldAttachments {
     }
 }
 
+class ColorChoiceField extends FormField {
+    static $widget = 'ColorPickerWidget';
+}
+
 class InlineFormData extends ArrayObject {
     var $_form;
 
@@ -4094,6 +4100,27 @@ class FreeTextWidget extends Widget {
     }
 }
 
+class ColorPickerWidget extends Widget {
+    static $media = array(
+        'css' => array(
+            'css/spectrum.css',
+        ),
+        'js' => array(
+            'js/spectrum.js',
+        ),
+    );
+
+    function render($options=array()) {
+        ?><input type="color"
+            id="<?php echo $this->id; ?>"
+            <?php echo implode(' ', array_filter(array(
+                $classes
+            ))); ?>
+            name="<?php echo $this->name; ?>"
+            value="<?php echo Format::htmlchars($this->value); ?>"/><?php
+    }
+}
+
 class VisibilityConstraint {
     static $operators = array(
         'eq' => 1,
diff --git a/include/class.list.php b/include/class.list.php
index 69106502be674220c0f2c36c2293932554245471..20bc6b87b2ce4269cfd26550e41744e13ab482b7 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -1094,7 +1094,7 @@ CustomListHandler::register('ticket-status', 'TicketStatusList');
 
 class TicketStatus
 extends VerySimpleModel
-implements CustomListItem, TemplateVariable {
+implements CustomListItem, TemplateVariable, Searchable {
 
     static $meta = array(
         'table' => TICKET_STATUS_TABLE,
@@ -1273,6 +1273,18 @@ implements CustomListItem, TemplateVariable {
         return $base;
     }
 
+    // Searchable interface
+    static function getSearchableFields() {
+        return array(
+            'state' => new TicketStateChoiceField(array(
+                'label' => __('State'),
+            )),
+            'name' => new TicketStatusChoiceField(array(
+                'label' => __('Status Name'),
+            )),
+        );
+    }
+
     function getList() {
         if (!isset($this->_list))
             $this->_list = DynamicList::lookup(array('type' => 'ticket-status'));
diff --git a/include/class.organization.php b/include/class.organization.php
index f41e04225c5ce1bf017b647644c7e73d99f3a968..db49618cb95d13701fa65e5a6ca95c394d27de07 100644
--- a/include/class.organization.php
+++ b/include/class.organization.php
@@ -16,6 +16,7 @@ require_once(INCLUDE_DIR . 'class.orm.php');
 require_once(INCLUDE_DIR . 'class.forms.php');
 require_once(INCLUDE_DIR . 'class.dynamic_forms.php');
 require_once(INCLUDE_DIR . 'class.user.php');
+require_once INCLUDE_DIR . 'class.search.php';
 
 class OrganizationModel extends VerySimpleModel {
     static $meta = array(
@@ -160,7 +161,7 @@ class OrganizationCdata extends VerySimpleModel {
 }
 
 class Organization extends OrganizationModel
-implements TemplateVariable {
+implements TemplateVariable, Searchable {
     var $_entries;
     var $_forms;
 
@@ -341,6 +342,20 @@ implements TemplateVariable {
         return $base + $extra;
     }
 
+    static function getSearchableFields() {
+        $uform = OrganizationForm::objects()->one();
+        foreach ($uform->getFields() as $F) {
+            $fname = $F->get('name') ?: ('field_'.$F->get('id'));
+            if (!$F->hasData() || $F->isPresentationOnly())
+                continue;
+            if (!$F->isStorable())
+                $base[$fname] = $F;
+            else
+                $base["cdata__{$fname}"] = $F;
+        }
+        return $base;
+    }
+
     function update($vars, &$errors) {
 
         $valid = true;
@@ -446,6 +461,15 @@ implements TemplateVariable {
         return true;
     }
 
+    static function getLink($id) {
+        global $thisstaff;
+
+        if (!$id || !$thisstaff)
+            return false;
+
+        return ROOT_PATH . sprintf('orgs.php?id=%s', $id);
+    }
+
     static function fromVars($vars) {
 
         $vars['name'] = Format::striptags($vars['name']);
diff --git a/include/class.orm.php b/include/class.orm.php
index 7539c144564aba7d5c5922632fe341dc4e9abd9e..3a51e8cdccffb525c949422f186c49b39b45baae 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -488,6 +488,27 @@ class VerySimpleModel {
         return ($key) ? $M->offsetGet($key) : $M;
     }
 
+    static function getOrmFields($recurse=false) {
+        $fks = $lfields = $fields = array();
+        $myname = get_called_class();
+        foreach (static::getMeta('joins') as $name=>$j) {
+            $fks[$j['local']] = true;
+            if (!$j['reverse'] && !$j['list'] && $recurse) {
+                foreach ($j['fkey'][0]::getOrmFields($recurse - 1) as $name2=>$f) {
+                    $fields["{$name}__{$name2}"] = "{$name} / $f";
+                }
+            }
+        }
+        foreach (static::getMeta('fields') as $f) {
+            if (isset($fks[$f]))
+                continue;
+            if (in_array($f, static::getMeta('pk')))
+                continue;
+            $lfields[$f] = "{$f}";
+        }
+        return $lfields + $fields;
+    }
+
     /**
      * objects
      *
@@ -794,17 +815,23 @@ class SqlCase extends SqlFunction {
 
 class SqlExpr extends SqlFunction {
     function __construct($args) {
-        $this->args = $args;
+        $this->args = (array) $args;
     }
 
     function toSql($compiler, $model=false, $alias=false) {
         $O = array();
         foreach ($this->args as $field=>$value) {
-            list($field, $op) = $compiler->getField($field, $model);
-            if (is_callable($op))
-                $O[] = call_user_func($op, $field, $value, $model);
-            else
-                $O[] = sprintf($op, $field, $compiler->input($value));
+            if ($value instanceof Q) {
+                $ex = $compiler->compileQ($value);
+                $O[] = $ex->text;
+            }
+            else {
+                list($field, $op) = $compiler->getField($field, $model);
+                if (is_callable($op))
+                    $O[] = call_user_func($op, $field, $value, $model);
+                else
+                    $O[] = sprintf($op, $field, $compiler->input($value));
+            }
         }
         return implode(' ', $O) . ($alias ? ' AS ' . $alias : '');
     }
@@ -1951,10 +1978,10 @@ extends ModelResultSet {
         return true;
     }
 
-    // QuerySet delegates
     function count() {
-        return $this->objects()->count();
+        return count($this->asArray());
     }
+    // QuerySet delegates
     function exists() {
         return $this->queryset->exists();
     }
@@ -1977,7 +2004,8 @@ extends ModelResultSet {
     }
     function offsetSet($a, $b) {
         $this->fillTo($a);
-        $this->cache[$a]->delete();
+        if ($obj = $this->cache[$a])
+            $obj->delete();
         $this->add($b, $a);
     }
 
diff --git a/include/class.queue.php b/include/class.queue.php
new file mode 100644
index 0000000000000000000000000000000000000000..38bf53236bd82d4159345fd449827f8aa449f53e
--- /dev/null
+++ b/include/class.queue.php
@@ -0,0 +1,688 @@
+<?php
+/*********************************************************************
+    class.queue.php
+
+    Custom (ticket) queues for osTicket
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+require_once INCLUDE_DIR . 'class.search.php';
+
+class CustomQueue extends SavedSearch {
+    static $meta = array(
+        'joins' => array(
+            'columns' => array(
+                'reverse' => 'QueueColumn.queue',
+            ),
+        ),
+    );
+
+    static function objects() {
+        return parent::objects()->filter(array(
+            'flags__hasbit' => static::FLAG_QUEUE
+        ));
+    }
+
+    static function getDecorations($root) {
+        // Ticket decorations
+        return array(
+            'TicketThreadCount',
+            'ThreadAttachmentCount',
+            'OverdueFlagDecoration',
+            'TicketSourceDecoration'
+        );
+    }
+
+    function getColumns() {
+        if (!count($this->columns)) {
+            foreach (array(
+                new QueueColumn(array(
+                    "id" => 1,
+                    "heading" => "Number",
+                    "primary" => 'number',
+                    "width" => 100,
+                )),
+                new QueueColumn(array(
+                    "id" => 2,
+                    "heading" => "Created",
+                    "primary" => 'created',
+                    "width" => 100,
+                )),
+                new QueueColumn(array(
+                    "id" => 3,
+                    "heading" => "Subject",
+                    "primary" => 'cdata__subject',
+                    "width" => 250,
+                )),
+                new QueueColumn(array(
+                    "id" => 4,
+                    "heading" => "From",
+                    "primary" => 'user__name',
+                    "width" => 150,
+                )),
+                new QueueColumn(array(
+                    "id" => 5,
+                    "heading" => "Priority",
+                    "primary" => 'cdata__priority',
+                    "width" => 120,
+                )),
+                new QueueColumn(array(
+                    "id" => 6,
+                    "heading" => "Assignee",
+                    "primary" => 'assignee',
+                    "secondary" => 'team__name',
+                    "width" => 100,
+                )),
+            ) as $c) {
+                $this->addColumn($c);
+            }
+        }
+        return $this->columns;
+    }
+
+    function addColumn(QueueColumn $col) {
+        $this->columns->add($col);
+        $col->queue = $this;
+    }
+
+    function getRoot() {
+        return 'Ticket';
+    }
+
+    function getBasicQuery($form=false) {
+        $root = $this->getRoot();
+        $query = $root::objects();
+        return $this->mangleQuerySet($query, $form);
+    }
+
+    /**
+     * Retrieve a QuerySet instance based on the type of object (root) of
+     * this Q, which is automatically configured with the data and criteria
+     * of the queue and its columns.
+     *
+     * Returns:
+     * <QuerySet> instance
+     */
+    function getQuery($form=false) {
+        $query = $this->getBasicQuery($form);
+        foreach ($this->getColumns() as $C) {
+            $query = $C->mangleQuery($query);
+        }
+        return $query;
+    }
+}
+
+abstract class QueueDecoration {
+    static $icon = false;
+    static $desc = '';
+
+    var $config;
+
+    function __construct($config) {
+        $this->config = $config;
+    }
+
+    static function fromJson($config) {
+        $class = $config['c'];
+        if (class_exists($class))
+            return new $class($config);
+    }
+
+    static function getDescription() {
+        return __(static::$desc);
+    }
+    static function getIcon() {
+        return static::$icon;
+    }
+    static function getPositions() {
+        return array(
+            "<" => __('Start'),
+            "b" => __('Before'),
+            "a" => __('After'),
+            ">" => __('End'),
+        );
+    }
+
+    function decorate($text, $dec) {
+        static $positions = array(
+            '<' => '<span class="pull-left">%2$s</span>%1$s',
+            '>' => '<span class="pull-right">%2$s</span>%1$s',
+            'a' => '%1$s %2$s',
+            'b' => '%2$s %1$s',
+        );
+
+        $pos = strtolower($this->config['p']);
+        if (!isset($positions[$pos]))
+            return $text;
+
+        return sprintf($positions[$pos], $text, $dec);
+    }
+
+    // Render the annotation with the database record $row. $text is the
+    // text of the cell before decorations were applied.
+    function render($row, $cell) {
+        if ($decoration = $this->getDecoration($row, $cell))
+            return $this->decorate($cell, $decoration);
+
+        return $cell;
+    }
+
+    // Add the annotation to a QuerySet
+    abstract function annotate($query);
+
+    // Fetch some HTML to render the decoration on the page. This function
+    // can return boolean FALSE to indicate no decoration should be applied
+    abstract function getDecoration($row, $text);
+
+    function getPosition() {
+        return strtolower($this->config['p']) ?: 'a';
+    }
+}
+
+class TicketThreadCount
+extends QueueDecoration {
+    static $icon = 'comments-alt';
+    static $qname = '_thread_count';
+    static $desc = /* @trans */ 'Thread Count';
+
+    function annotate($query) {
+        return $query->annotate(array(
+        static::$qname => TicketThread::objects()
+            ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
+            ->exclude(array('entries__flags__hasbit' => ThreadEntry::FLAG_HIDDEN))
+            ->aggregate(array('count' => SqlAggregate::COUNT('entries__id')))
+        ));
+    }
+
+    function getDecoration($row, $text) {
+        $threadcount = $row[static::$qname];
+        if ($threadcount > 1) {
+            return sprintf(
+                '<i class="icon-comments-alt"></i><small>%s</small>',
+                $threadcount
+            );
+        }
+    }
+}
+
+class ThreadAttachmentCount
+extends QueueDecoration {
+    static $icon = 'paperclip';
+    static $qname = '_att_count';
+    static $desc = /* @trans */ 'Attachment Count';
+
+    function annotate($query) {
+        // TODO: Convert to Thread attachments
+        return $query->annotate(array(
+        static::$qname => TicketThread::objects()
+            ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
+            ->filter(array('entries__attachments__inline' => 0))
+            ->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id')))
+        ));
+    }
+
+    function getDecoration($row, $text) {
+        $count = $row[static::$qname];
+        if ($count) {
+            return sprintf(
+                '<i class="small icon-paperclip icon-flip-horizontal" data-toggle="tooltip" title="%s"></i>',
+                $count);
+        }
+    }
+}
+
+class OverdueFlagDecoration
+extends QueueDecoration {
+    static $icon = 'exclamation';
+    static $desc = /* @trans */ 'Overdue Icon';
+
+    function annotate($query) {
+        return $query->values('isoverdue');
+    }
+
+    function getDecoration($row, $text) {
+        return sprintf(
+            '<span class="Icon overdueTicket">%s</span>',
+            $text);
+    }
+}
+
+class TicketSourceDecoration
+extends QueueDecoration {
+    static $icon = 'phone';
+    static $desc = /* @trans */ 'Ticket Source';
+
+    function annotate($query) {
+        return $query->values('source');
+    }
+
+    function getDecoration($row, $text) {
+        return sprintf(
+            '<span class="Icon %sTicket">%s</span>',
+            $row['source'], $text);
+    }
+}
+
+class DataSourceField
+extends ChoiceField {
+    function getChoices() {
+        $config = $this->getConfiguration();
+        $root = $config['root'];
+        $fields = array();
+        foreach (SavedSearch::getSearchableFields($root) as $path=>$f) {
+            $fields[$path] = $f->get('label');
+        }
+        return $fields;
+    }
+}
+
+class QueueColumnCondition {
+    var $config;
+    var $properties = array();
+
+    function __construct($config) {
+        $this->config = $config;
+        if (is_array($config['prop']))
+            $this->properties = $config['prop'];
+    }
+
+    function getProperties() {
+        return $this->properties;
+    }
+
+    function getField() {
+    }
+
+    // Add the annotation to a QuerySet
+    function annotate($query) {
+        $criteria = $this->config['crit'];
+        $searchable = SavedSearch::getSearchableFields('Ticket');
+
+        // Setup a dummy form with a source for field setup
+        $form = new Form($criteria);
+        $fields = array();
+        foreach ($criteria as $k=>$v) {
+            if (substr($k, -7) === '+method') {
+                list($name,) = explode('+', $k, 2);
+                if (!isset($searchable[$name]))
+                    continue;
+
+                // Lookup the field to search this condition
+                $field = $searchable[$name];
+
+                // Get the search method and value
+                $breakout = SavedSearch::getSearchField($field, $name);
+                $method = $breakout["{$name}+method"];
+                $method->setForm($form);
+                if (!($method = $method->getClean()))
+                    continue;
+
+                if (!($value = $breakout["{$name}+{$method}"]))
+                    continue;
+
+                // Fetch a criteria Q for the query
+                $value = $value->getClean();
+                $Q = $field->getSearchQ($method, $value, $name);
+
+                // Add an annotation to the query
+                $query = $query->annotate(array(
+                    $this->getAnnotationName() => new SqlExpr($Q)
+                ));
+
+                // Only one field can be considered in the condition
+                break;
+            }
+        }
+        return $query;
+    }
+
+    function render($row, $text) {
+        $field = $this->getAnnotationName();
+        if ($V = $row[$field]) {
+            $style = array();
+            foreach ($this->getProperties() as $css=>$value) {
+                $style[] = "{$css}:{$value}";
+            }
+            $text = sprintf('<span style="%s">%s</span>',
+                implode(' ', $style), $text);
+        }
+        return $text;
+    }
+
+    function getAnnotationName() {
+        return 'howdy';
+    }
+
+    static function fromJson($config) {
+        if (is_string($config))
+            $config = JsonDataParser::decode($cnofig);
+        if (!is_array($config))
+            throw new BadMethodCallException('$config must be string or array');
+
+        return new static($config);
+    }
+}
+
+class QueueColumnConditionProperty
+extends ChoiceField {
+    static $properties = array(
+        'background-color' => 'ColorChoiceField',
+        'color' => 'ColorChoiceField',
+        'font-family' => array(
+            'monospace', 'serif', 'sans-serif', 'cursive', 'fantasy',
+        ),
+        'font-size' => array(
+            'small', 'medium', 'large', 'smaller', 'larger',
+        ),
+        'font-style' => array(
+            'normal', 'italic', 'oblique',
+        ),
+        'font-weight' => array(
+            'lighter', 'normal', 'bold', 'bolder',
+        ),
+        'text-decoration' => array(
+            'none', 'underline',
+        ),
+        'text-transform' => array(
+            'uppercase', 'lowercase', 'captalize',
+        ),
+    );
+
+    function __construct($property) {
+        $this->property = $property;
+    }
+
+    static function getProperties() {
+        return array_keys(static::$properties[$this->property]);
+    }
+
+    static function getField($prop) {
+        $choices = static::$properties[$prop];
+        if (is_array($choices))
+            return new ChoiceField(array(
+                'choices' => array_combine($choices, $choices),
+            ));
+        elseif (class_exists($choices))
+            return new $choices();
+    }
+
+    function getChoices() {
+        if (isset($this->property))
+            return static::$properties[$this->property];
+
+        $keys = array_keys(static::$properties);
+        return array_combine($keys, $keys);
+    }
+}
+
+
+/**
+ * Object version of JSON-serialized column array which has several
+ * properties:
+ *
+ * {
+ *   "heading": "Header Text",
+ *   "primary": "user__name",
+ *   "secondary": null,
+ *   "width": 100,
+ *   "link": 'ticket',
+ *   "truncate": "wrap",
+ *   "filter": "UsersName"
+ *   "annotations": [
+ *     {
+ *       "c": "ThreadCollabCount",
+ *       "p": ">"
+ *     }
+ *   ],
+ *   "conditions": [
+ *     {
+ *       "crit": {
+ *         "created+method": {"ndaysago": "in the last n days"}, "created+ndaysago": {"until":"7"}
+ *       },
+ *       "prop": {
+ *         "font-weight": "bold"
+ *       }
+ *     }
+ *   ]
+ * }
+ */
+class QueueColumn
+extends VerySimpleModel {
+    static $meta = array(
+        'table' => QUEUE_COLUMN_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'queue' => array(
+                'constraint' => array('queue_id' => 'CustomQueue.id'),
+            ),
+        ),
+    );
+
+    var $_decorations = array();
+    var $_conditions = array();
+
+    function __onload() {
+        if ($this->annotations) {
+            foreach ($this->annotations as $D)
+                $this->_decorations[] = QueueDecoration::fromJson($D) ?: array();
+        }
+        if ($this->conditions) {
+            foreach ($this->conditions as $C)
+                $this->_conditions[] = QueueColumnCondition::fromJson($C) ?: array();
+        }
+    }
+
+    function getId() {
+        return $this->id;
+    }
+
+    function getQueue() {
+        return $this->queue;
+    }
+
+    function getHeading() {
+        return $this->heading;
+    }
+
+    function getWidth() {
+        return $this->width ?: 100;
+    }
+
+    function getLink($row) {
+        $link = $this->link;
+        switch (strtolower($link)) {
+        case 'root':
+        case 'ticket':
+            return Ticket::getLink($row['ticket_id']);
+        case 'user':
+            return User::getLink($row['user_id']);
+        case 'org':
+            return Organization::getLink($row['user__org_id']);
+        }
+    }
+
+    function render($row) {
+        // Basic data
+        $text = $this->renderBasicValue($row);
+
+        // Truncate
+        if ($class = $this->getTruncateClass()) {
+            $text = sprintf('<span class="%s">%s</span>', $class, $text);
+        }
+
+        // Link
+        if ($link = $this->getLink($row)) {
+            $text = sprintf('<a href="%s">%s</a>', $link, $text);
+        }
+
+        // Decorations and conditions
+        foreach ($this->_decorations as $D) {
+            $text = $D->render($row, $text);
+        }
+        foreach ($this->_conditions as $C) {
+            $text = $C->render($row, $text);
+        }
+        return $text;
+    }
+
+    function renderBasicValue($row) {
+        $root = $this->getQueue()->getRoot();
+        $fields = SavedSearch::getSearchableFields($root);
+        $primary = $this->getOrmPath($this->primary);
+        $secondary = $this->getOrmPath($this->secondary);
+
+        // TODO: Consider data filter if configured
+
+        if (($F = $fields[$primary]) && ($T = $F->from_query($row, $primary)))
+            return $F->display($F->to_php($T));
+
+        if (($F = $fields[$secondary]) && ($T = $F->from_query($row, $secondary)))
+            return $F->display($F->to_php($T));
+    }
+
+    function getTruncateClass() {
+        switch ($this->truncate) {
+        case 'ellipsis':
+            return 'trucate';
+        case 'clip':
+            return 'truncate clip';
+        default:
+        case 'wrap':
+            return false;
+        }
+    }
+
+    function mangleQuery($query) {
+        // Basic data
+        $fields = SavedSearch::getSearchableFields($this->getQueue()->getRoot());
+        if ($primary = $fields[$this->primary])
+            $query = $primary->addToQuery($query,
+                $this->getOrmPath($this->primary));
+
+        if ($secondary = $fields[$this->secondary])
+            $query = $secondary->addToQuery($query,
+                $this->getOrmPath($this->secondary));
+
+        switch ($this->link) {
+        // XXX: Consider the ROOT of the related queue
+        case 'ticket':
+            $query = $query->values('ticket_id');
+            break;
+        case 'user':
+            $query = $query->values('user_id');
+            break;
+        case 'org':
+            $query = $query->values('user__org_id');
+            break;
+        }
+
+        // Decorations
+        foreach ($this->_decorations as $D) {
+            $query = $D->annotate($query);
+        }
+
+        // Conditions
+        foreach ($this->_conditions as $C) {
+            $query = $C->annotate($query);
+        }
+
+        return $query;
+    }
+
+    function getDataConfigForm($source=false) {
+        return new QueueColDataConfigForm($source ?: $this->ht,
+            array('id' => $this->id));
+    }
+
+    function getOrmPath($name) {
+        return $name;
+    }
+
+    function getDecorations() {
+        return $this->_decorations;
+    }
+
+    function getConditions() {
+        return $this->_conditions;
+    }
+
+    /**
+     * Create a CustomQueueColumn from vars (_POST) received from an
+     * update request.
+     */
+    static function create($vars=array()) {
+        $inst = parent::create($vars);
+        // TODO: Convert decorations and conditions
+        return $inst;
+    }
+
+    function update($vars) {
+        $form = $this->getDataConfigForm($vars);
+        foreach ($form->getClean() as $k=>$v)
+            $this->set($k, $v);
+    }
+}
+
+class QueueColDataConfigForm
+extends AbstractForm {
+    function buildFields() {
+        return array(
+            'primary' => new DataSourceField(array(
+                'label' => __('Primary Data Source'),
+                'required' => true,
+                'configuration' => array(
+                    'root' => 'Ticket',
+                ),
+                'layout' => new GridFluidCell(6),
+            )),
+            'secondary' => new DataSourceField(array(
+                'label' => __('Secondary Data Source'),
+                'configuration' => array(
+                    'root' => 'Ticket',
+                ),
+                'layout' => new GridFluidCell(6),
+            )),
+            'heading' => new TextboxField(array(
+                'label' => __('Heading'),
+                'required' => true,
+                'layout' => new GridFluidCell(3),
+            )),
+            'link' => new ChoiceField(array(
+                'label' => __('Link'),
+                'required' => false,
+                'choices' => array(
+                    'ticket' => __('Ticket'),
+                    'user' => __('User'),
+                    'org' => __('Organization'),
+                ),
+                'layout' => new GridFluidCell(3),
+            )),
+            'width' => new TextboxField(array(
+                'label' => __('Width'),
+                'default' => 75,
+                'configuration' => array(
+                    'validator' => 'number',
+                ),
+                'layout' => new GridFluidCell(3),
+            )),
+            'truncate' => new ChoiceField(array(
+                'label' => __('Text Overflow'),
+                'choices' => array(
+                    'wrap' => __("Wrap Lines"),
+                    'ellipsis' => __("Add Ellipsis"),
+                    'clip' => __("Clip Text"),
+                ),
+                'default' => 'wrap',
+                'layout' => new GridFluidCell(3),
+            )),
+        );
+    }
+}
diff --git a/include/class.role.php b/include/class.role.php
index 0e0f89af2b493003fe2ef3437a0e554152325c36..0b34853220e1ea29bb2c607a646b1063c50efac9 100644
--- a/include/class.role.php
+++ b/include/class.role.php
@@ -13,6 +13,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once INCLUDE_DIR . 'class.forms.php';
 
 class RoleModel extends VerySimpleModel {
     static $meta = array(
diff --git a/include/class.search.php b/include/class.search.php
index 61b56a41e61f6c14dd4fa44ccaa6fd914d36453b..674533e2a531748f9bfb8a562d34777ed45b4bee 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -21,6 +21,8 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once INCLUDE_DIR . 'class.role.php';
+require_once INCLUDE_DIR . 'class.list.php';
 
 abstract class SearchBackend {
     static $id = false;
@@ -672,6 +674,19 @@ class SavedSearch extends VerySimpleModel {
         )));
     }
 
+    function getName() {
+        return $this->name;
+    }
+
+    function getSearchForm() {
+        if ($state = JsonDataParser::parse($search->config)) {
+            $form = $search->loadFromState($state);
+            $form->loadState($state);
+            return $form;
+        }
+        return $this->getForm();
+    }
+
     function loadFromState($source=false) {
         // Pull out 'other' fields from the state so the fields will be
         // added to the form. The state will be loaded below
@@ -787,6 +802,39 @@ class SavedSearch extends VerySimpleModel {
         }
         return $core;
     }
+    static function getSupportedTicketMatches() {
+        // User information
+        $matches = array(
+            __('Ticket Built-In') => SavedSearch::getExtendedTicketFields(),
+        );
+        foreach (array('ticket'=>'TicketForm', 'user'=>'UserForm', 'organization'=>'OrganizationForm') as $k=>$F) {
+            $form = $F::objects()->one();
+            $fields = &$matches[$form->getLocal('title')];
+            foreach ($form->getFields() as $f) {
+                if (!$f->hasData() || $f->isPresentationOnly())
+                    continue;
+                $fields[":$k!".$f->get('id')] = __(ucfirst($k)).' / '.$f->getLocal('label');
+                /* TODO: Support matches on list item properties
+                if (($fi = $f->getImpl()) && $fi->hasSubFields()) {
+                    foreach ($fi->getSubFields() as $p) {
+                        $fields[":$k.".$f->get('id').'.'.$p->get('id')]
+                            = __(ucfirst($k)).' / '.$f->getLocal('label').' / '.$p->getLocal('label');
+                    }
+                }
+                */
+            }
+        }
+        $fields = &$matches[__('Custom Forms')];
+        foreach (DynamicForm::objects()->filter(array('type'=>'G')) as $form) {
+            foreach ($form->getFields() as $f) {
+                if (!$f->hasData() || $f->isPresentationOnly())
+                    continue;
+                $key = sprintf(':field!%d', $f->get('id'), $f->get('id'));
+                $fields[$key] = $form->getLocal('title').' / '.$f->getLocal('label');
+            }
+        }
+        return $matches;
+    }
 
     static function getExtendedTicketFields() {
         return array(
@@ -823,6 +871,36 @@ class SavedSearch extends VerySimpleModel {
         );
     }
 
+    static function getSearchableFields($base, $recurse=2, $cache=true) {
+        static $cache;
+
+        if (!in_array('Searchable', class_implements($base)))
+            return array();
+
+        // FIXME: The fields from dynamicFormFields seem to be cached, and
+        // setting the label is preserved across multiple calls to this
+        // function. The caching helps with this phenomenon, but a better
+        // mechanism should be employed
+        if ($cache && isset($cache[$base]))
+            return $cache[$base];
+
+        $fields = $base::getSearchableFields();
+        if ($recurse) {
+            foreach ($base::getMeta('joins') as $path=>$j) {
+                $fc = $j['fkey'][0];
+                if ($fc == $base || $j['list'] || $j['reverse'])
+                    continue;
+                foreach (static::getSearchableFields($fc, $recurse-1, false) as $path2=>$F) {
+                    $fields["{$path}__{$path2}"] = $F;
+                    $F->set('label', sprintf("%s / %s", $fc, $F->get('label')));
+                }
+            }
+        }
+        if ($cache)
+            $cache[$base] = $fields;
+        return $fields;
+    }
+
     static function getSearchField($field, $name) {
         $baseId = $field->getId() * 20;
         $pieces = array();
@@ -1125,6 +1203,27 @@ class AssigneeChoiceField extends ChoiceField {
             return parent::describeSearchMethod($method);
         }
     }
+
+    function addToQuery($query, $name=false) {
+        return $query->values('staff__firstname', 'staff__lastname', 'team__name', 'team_id');
+    }
+
+    function from_query($row, $name=false) {
+        if ($row['staff__firstname'])
+            return new AgentsName(array('first' => $row['staff__firstname'], 'last' => $row['staff__lastname']));
+        if ($row['team_id'])
+            return Team::getLocalById($row['team_id'], 'name', $row['team__name']);
+    }
+
+    function display($value) {
+        return (string) $value;
+    }
+}
+
+class AgentSelectionField extends ChoiceField {
+    function getChoices() {
+        return Staff::getStaffMembers();
+    }
 }
 
 class TicketStateChoiceField extends ChoiceField {
@@ -1240,3 +1339,10 @@ class TicketStatusChoiceField extends SelectionField {
         }
     }
 }
+
+interface Searchable {
+    // Fetch an array of [ orm__path => Field() ] pairs. The field label is
+    // used when this list is rendered in a dropdown, and the field search
+    // mechanisms are use to apply query filtering based on the field.
+    static function getSearchableFields();
+}
diff --git a/include/class.staff.php b/include/class.staff.php
index e5ed7e77555e5bb72f1376d10de92bc9b579569d..ce922b1af413944042bdaeef33916a83b25f83e3 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -23,7 +23,7 @@ include_once(INCLUDE_DIR.'class.user.php');
 include_once(INCLUDE_DIR.'class.auth.php');
 
 class Staff extends VerySimpleModel
-implements AuthenticatedUser, EmailContact, TemplateVariable {
+implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
 
     static $meta = array(
         'table' => STAFF_TABLE,
@@ -127,6 +127,14 @@ implements AuthenticatedUser, EmailContact, TemplateVariable {
         }
     }
 
+    static function getSearchableFields() {
+        return array(
+            'email' => new TextboxField(array(
+                'label' => __('Email Address'),
+            )),
+        );
+    }
+
     function getHashtable() {
         $base = $this->ht;
         unset($base['teams']);
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 7cc661f9595c6a68e4dc8ee8d47b04b7fd13ddda..0fc4f021df621418c6b236d38e92c1698f528256 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -245,7 +245,7 @@ class TicketCData extends VerySimpleModel {
 }
 
 class Ticket extends TicketModel
-implements RestrictedAccess, Threadable {
+implements RestrictedAccess, Threadable, Searchable {
 
     static $meta = array(
         'select_related' => array('topic', 'staff', 'user', 'team', 'dept', 'sla', 'thread',
@@ -1947,6 +1947,57 @@ implements RestrictedAccess, Threadable {
         return $base + $extra;
     }
 
+    // Searchable interface
+    static function getSearchableFields() {
+        $base = array(
+            'number' => new TextboxField(array(
+                'label' => __('Ticket Number')
+            )),
+            'ip_address' => new TextboxField(array(
+                'label' => __('IP Address'),
+                'configuration' => array('validator' => 'ip'),
+            )),
+            'source' => new TicketSourceChoiceField(array(
+                'label' => __('Ticket Source'),
+            )),
+            'isoverdue' => new BooleanField(array(
+                'label' => __('Overdue'),
+            )),
+            'isanswered' => new BooleanField(array(
+                'label' => __('Answered'),
+            )),
+            'duedate' => new DatetimeField(array(
+                'label' => __('Due Date'),
+            )),
+            'reopened' => new DatetimeField(array(
+                'label' => __('Reopen Date'),
+            )),
+            'closed' => new DatetimeField(array(
+                'label' => __('Close Date'),
+            )),
+            'lastupdate' => new DatetimeField(array(
+                'label' => __('Last Update'),
+            )),
+            'created' => new DatetimeField(array(
+                'label' => __('Create Date'),
+            )),
+            'assignee' => new AssigneeChoiceField(array(
+                'label' => __('Assignee'),
+            )),
+        );
+        $tform = TicketForm::getInstance();
+        foreach ($tform->getFields() as $F) {
+            $fname = $F->get('name') ?: ('field_'.$F->get('id'));
+            if (!$F->hasData() || $F->isPresentationOnly())
+                continue;
+            if (!$F->isStorable())
+                $base[$fname] = $F;
+            else
+                $base["cdata__{$fname}"] = $F;
+        }
+        return $base;
+    }
+
     //Replace base variables.
     function replaceVars($input, $vars = array()) {
         global $ost;
@@ -3674,5 +3725,14 @@ implements RestrictedAccess, Threadable {
 
         require STAFFINC_DIR.'templates/tickets-actions.tmpl.php';
     }
+
+    static function getLink($id) {
+        global $thisstaff;
+
+        switch (true) {
+        case ($thisstaff instanceof Staff):
+            return ROOT_PATH . sprintf('scp/tickets.php?id=%s', $id);
+        }
+    }
 }
 ?>
diff --git a/include/class.topic.php b/include/class.topic.php
index c25ab788a4288beaae3bd1163ecb72c6b2ea183f..ead2e57b6956a36b0db0483c157f86a405ae3f32 100644
--- a/include/class.topic.php
+++ b/include/class.topic.php
@@ -13,12 +13,12 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-
 require_once INCLUDE_DIR . 'class.sequence.php';
 require_once INCLUDE_DIR . 'class.filter.php';
+require_once INCLUDE_DIR . 'class.search.php';
 
 class Topic extends VerySimpleModel
-implements TemplateVariable {
+implements TemplateVariable, Searchable {
 
     static $meta = array(
         'table' => TOPIC_TABLE,
@@ -91,6 +91,14 @@ implements TemplateVariable {
         );
     }
 
+    static function getSearchableFields() {
+        return array(
+            'name' => new TextboxField(array(
+                'label' => __('Name'),
+            )),
+        );
+    }
+
     function getId() {
         return $this->topic_id;
     }
diff --git a/include/class.user.php b/include/class.user.php
index 24cae58b9122dc8a5ee3af8ceb9183720e7524d6..a3b98f381b61d1e341ae647ce131fa60fcf28528 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -16,8 +16,9 @@
 **********************************************************************/
 require_once INCLUDE_DIR . 'class.orm.php';
 require_once INCLUDE_DIR . 'class.util.php';
-require_once INCLUDE_DIR . 'class.organization.php';
 require_once INCLUDE_DIR . 'class.variable.php';
+require_once INCLUDE_DIR . 'class.search.php';
+require_once INCLUDE_DIR . 'class.organization.php';
 
 class UserEmailModel extends VerySimpleModel {
     static $meta = array(
@@ -191,7 +192,7 @@ class UserCdata extends VerySimpleModel {
 }
 
 class User extends UserModel
-implements TemplateVariable {
+implements TemplateVariable, Searchable {
 
     var $_entries;
     var $_forms;
@@ -362,6 +363,20 @@ implements TemplateVariable {
         return $base + $extra;
     }
 
+    static function getSearchableFields() {
+        $uform = UserForm::getUserForm();
+        foreach ($uform->getFields() as $F) {
+            $fname = $F->get('name') ?: ('field_'.$F->get('id'));
+            if (!$F->hasData() || $F->isPresentationOnly())
+                continue;
+            if (!$F->isStorable())
+                $base[$fname] = $F;
+            else
+                $base["cdata__{$fname}"] = $F;
+        }
+        return $base;
+    }
+
     function addDynamicData($data) {
         return $this->addForm(UserForm::objects()->one(), 1, $data);
     }
@@ -599,6 +614,15 @@ implements TemplateVariable {
         if ($user = static::lookup($id))
             return $user->getName();
     }
+
+    static function getLink($id) {
+        global $thisstaff;
+
+        if (!$id || !$thisstaff)
+            return false;
+
+        return ROOT_PATH . sprintf('users.php?id=%s', $id);
+    }
 }
 
 class EmailAddress
diff --git a/include/staff/queue.inc.php b/include/staff/queue.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..d322a912554b6bc65e91462834a1609e9f3abd27
--- /dev/null
+++ b/include/staff/queue.inc.php
@@ -0,0 +1,243 @@
+<?php
+// vim: expandtab sw=2 ts=2 sts=2:
+
+if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
+
+$info = $qs = array();
+
+if ($_REQUEST['a']=='add'){
+    if (!$queue) {
+        $queue = CustomQueue::create(array(
+            'flags' => CustomQueue::FLAG_QUEUE,
+        ));
+    }
+    $title=__('Add New Queue');
+    $action='create';
+    $submit_text=__('Create');
+}
+else {
+    //Editing Department.
+    $title=__('Manage Custom Queue');
+    $action='update';
+    $submit_text=__('Save Changes');
+    $info['id'] = $queue->getId();
+    $qs += array('id' => $queue->getId());
+}
+?>
+
+<form action="queues.php?<?php echo Http::build_query($qs); ?>" method="post" id="save" autocomplete="off">
+  <?php csrf_token(); ?>
+  <input type="hidden" name="do" value="<?php echo $action; ?>">
+  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
+  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
+
+  <h2><?php echo __('Ticket Queues'); ?> // <?php echo $title; ?>
+      <?php if (isset($queue->id)) { ?><small>
+      — <?php echo $queue->getName(); ?></small>
+      <?php } ?>
+  </h2>
+
+
+  <ul class="clean tabs">
+    <li class="active"><a href="#criteria"><i class="icon-filter"></i>
+      <?php echo __('Criteria'); ?></a></li>
+    <li><a href="#columns"><i class="icon-columns"></i>
+      <?php echo __('Columns'); ?></a></li>
+    <li><a href="#preview-tab"><i class="icon-eye-open"></i>
+      <?php echo __('Preview'); ?></a></li>
+  </ul>
+
+  <div class="tab_content" id="criteria">
+    <table class="table">
+      <td style="width:60%; vertical-align:top">
+        <div><strong><?php echo __('Queue Name'); ?>:</strong></div>
+        <input type="text" name="name" value="<?php
+          echo Format::htmlchars($queue->getName()); ?>"
+          style="width:100%" />
+
+        <br/>
+        <br/>
+        <div><strong><?php echo __("Queue Search Criteria"); ?></strong></div>
+        <hr/>
+        <div class="advanced-search">
+<?php
+            $form = $queue->getSearchForm();
+            $search = $queue;
+            $matches = SavedSearch::getSupportedTicketMatches();
+            include STAFFINC_DIR . 'templates/advanced-search-criteria.tmpl.php';
+?>
+        </div>
+      </td>
+      <td style="width:35%; padding-left:40px; vertical-align:top">
+        <div><strong><?php echo __("Parent Queue"); ?>:</strong></div>
+        <select name="parent_id">
+          <option value="0">— <?php echo __('Top-Level Queue'); ?> —</option>
+<?php foreach (CustomQueue::objects() as $cq) { ?>
+          <option value="<?php echo $cq->id; ?>"><?php echo $cq->getName(); ?></option>
+<?php } ?>
+        </select>
+
+        <br/>
+        <br/>
+        <div><strong><?php echo __("Quick Filter"); ?></strong></div>
+        <hr/>
+        <select name="quick-filter">
+          <option value=":p:">— <?php echo __('Inherit from parent'); ?> —</option>
+<?php foreach (SavedSearch::getSearchableFields('Ticket') as $path=>$f) { ?>
+          <option value="<?php echo $path; ?>"><?php echo $f->get('label'); ?></option>
+<?php } ?>
+        </select>
+        <br/>
+        <br/>
+        <div><strong><?php echo __("Sort Options"); ?></strong></div>
+        <hr/>
+      </td>
+    </table>
+  </div>
+
+  <div class="hidden tab_content" id="columns">
+    <h2><?php echo __("Manage columns in this queue"); ?></h2>
+    <p><?php echo __("Add, remove, and customize the content of the columns in this queue using the options below. Click a column header to manage or resize it"); ?></p>
+
+    <div>
+      <i class="icon-plus-sign"></i>
+      <select id="add-column" data-next-id="0" onchange="javascript:
+        var $this = $(this),
+            selected = $this.find(':selected'),
+            nextId = $this.data('nextId'),
+            columns = $('#resizable-columns');
+        $.ajax({
+          url: 'ajax.php/queue/addColumn',
+          data: { field: selected.val(), id: nextId },
+          dataType: 'json',
+          success: function(json) {
+            var div = $('<div></div>')
+                .addClass('column-header ui-resizable')
+                .text(json.heading)
+                .data({id: nextId, colId: 'colconfig-'+nextId, width: json.width})
+                .append($('<i>')
+                  .addClass('icon-ellipsis-vertical ui-resizable-handle ui-resizable-handle-e')
+                )
+                .append($('<input />')
+                  .attr({type:'hidden', name:'columns[]'})
+                  .val(nextId)
+                );
+              config = $('<div></div>')
+                .addClass('hidden column-configuration')
+                .attr('id', 'colconfig-' + nextId);
+            config.append($(json.config)).insertAfter(columns.append(div));
+            $this.data('nextId', nextId+1);
+          }
+        }); 
+      ">
+        <option value="">— <?php echo __('Add a column'); ?> —</option>
+<?php foreach (SavedSearch::getSearchableFields('Ticket') as $path=>$f) { ?>
+        <option value="<?php echo $path; ?>"><?php echo $f->get('label'); ?></option>
+<?php } ?>
+      </select>
+
+      <div id="resizable-columns">
+<?php foreach ($queue->getColumns() as $column) {
+        $colid = $column->getId();
+        $maxcolid = max(@$maxcolid ?: 0, $colid);
+        echo sprintf('<div data-id="%s" data-col-id="colconfig-%s" class="column-header" '
+          .'data-width="%s">%s'
+          .'<i class="icon-ellipsis-vertical ui-resizable-handle ui-resizable-handle-e"></i>'
+          .'<input type="hidden" name="columns[]" value="%s"/>'
+          .'</div>',
+          $colid, $colid, $column->getWidth(), $column->getHeading(), $colid);
+} ?>
+      </div>
+      <script>
+        $(function() {
+          $('#add-column').data('nextId', <?php echo $maxcolid+1; ?>);
+          var qq = setInterval(function() {
+            var total = 0,
+                container = $('#resizable-columns'),
+                width = container.width(),
+                w2px = 1.25,
+                columns = $('.column-header', container);
+            // Await computation of the <div>'s width
+            if (width)
+              clearInterval(qq);
+            columns.each(function() {
+              total += $(this).data('width') || 100;
+            });
+            container.data('w2px', w2px);
+            columns.each(function() {
+              // FIXME: jQuery will compensate for padding (40px)
+              $(this).width(w2px * ($(this).data('width') || 100) - 42);
+            });
+          }, 20);
+        });
+      </script>
+
+<?php foreach ($queue->getColumns() as $column) {
+        $colid = $column->getId();
+        echo sprintf('<div class="hidden column-configuration" id="colconfig-%s">',
+            $colid);
+        include STAFFINC_DIR . 'templates/queue-column.tmpl.php';
+        echo '</div>';
+} ?>
+    </div>
+
+    <script>
+      var aa = setInterval(function() {
+        var cols = $('#resizable-columns');
+        if (cols.length && cols.sortable)
+          clearInterval(aa);
+        cols.sortable({
+          containment: 'parent'
+        });
+        $('.column-header', cols).resizable({
+          handles: {'e' : '.ui-resizable-handle'},
+          grid: [ 20, 0 ],
+          maxHeight: 16,
+          minHeight: 16,
+          stop: function(event, ui) {
+            var w2px = ui.element.parent().data('w2px'),
+                width = ui.element.width() - 42;
+            ui.element.data('width', width / w2px);
+            // TODO: Update WIDTH text box in the data form
+          }
+        });
+        cols.click('.column-header', function(e) {
+          var $this = $(event.target);
+          $this.parent().children().removeClass('active');
+          $this.addClass('active');
+          $('.column-configuration', $this.closest('.tab_content')).hide();
+          $('#'+$this.data('colId')).fadeIn('fast');
+        });
+      }, 20);
+    </script>
+  </div>
+
+  <div class="hidden tab_content" id="preview-tab">
+
+    <div id="preview">
+    </div>
+
+    <script>
+      $(function() {
+        $('#preview-tab').on('afterShow', function() {
+          $.ajax({
+            url: 'ajax.php/queue/preview',
+            type: 'POST',
+            data: $('#save').serializeArray(),
+            success: function(html) {
+              $('#preview').html(html);
+            }
+          });
+        });
+      });
+    </script>
+
+  </div>
+
+  <p style="text-align:center;">
+    <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
+    <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
+    <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick="window.history.go(-1);">
+  </p>
+
+</form>
diff --git a/include/staff/queues-ticket.inc.php b/include/staff/queues-ticket.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..170edafed6e22445c3a6b2ce74edea5103468272
--- /dev/null
+++ b/include/staff/queues-ticket.inc.php
@@ -0,0 +1,54 @@
+
+<form action="queues.php?t=tickets" method="POST" name="keys">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-right">
+                <a href="queues.php?t=tickets&amp;a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New Queue');?></a>
+                <span class="action-button" data-dropdown="#action-dropdown-more">
+                            <i class="icon-caret-down pull-right"></i>
+                            <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul id="actions">
+                        <li>
+                            <a class="confirm" data-name="enable" href="queues.php?t=tickets&amp;a=enable">
+                                <i class="icon-ok-sign icon-fixed-width"></i>
+                                <?php echo __( 'Enable'); ?>
+                            </a>
+                        </li>
+                        <li>
+                            <a class="confirm" data-name="disable" href="queues.php?t=tickets&amp;a=disable">
+                                <i class="icon-ban-circle icon-fixed-width"></i>
+                                <?php echo __( 'Disable'); ?>
+                            </a>
+                        </li>
+                        <li class="danger">
+                            <a class="confirm" data-name="delete" href="queues.php?t=tickets&amp;a=delete#queues">
+                                <i class="icon-trash icon-fixed-width"></i>
+                                <?php echo __( 'Delete'); ?>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+            <h3><?php echo __('Ticket Queues');?></h3>
+        </div>
+    </div>
+    <div class="clear"></div>
+ <?php csrf_token(); ?>
+ <input type="hidden" name="do" value="mass_process" >
+<input type="hidden" id="action" name="a" value="" >
+ <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
+    <thead>
+        <tr>
+            <th width="4%">&nbsp;</th>
+            <th width="46%"><a <?php echo $key_sort; ?> href="queues.php?t=tickets&amp;<?php echo $qstr; ?>&sort=name#queues"><?php echo __('Name');?></a></th>
+            <th width="12%"><a <?php echo $ip_sort; ?> href="queues.php?t=tickets&amp;<?php echo $qstr; ?>&sort=creator#queues"><?php echo __('Creator');?></a></th>
+            <th width="8%"><a  <?php echo $status_sort; ?> href="queues.php?t=tickets&amp;<?php echo $qstr; ?>&sort=status#queues"><?php echo __('Status');?></a></th>
+            <th width="10%" nowrap><a  <?php echo $date_sort; ?>href="queues.php?t=tickets&amp;<?php echo $qstr; ?>&sort=date#queues"><?php echo __('Created');?></a></th>
+        </tr>
+    </thead>
+    <tbody>
+    </tbody>
+</table>
+</form>
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php
index 8ec82503174c174509d51d3a923240dee7b4b1a2..454b53f64f331f4597120a61824d62dfe65b978b 100644
--- a/include/staff/settings-tickets.inc.php
+++ b/include/staff/settings-tickets.inc.php
@@ -15,6 +15,8 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
         <?php echo __('Autoresponder'); ?></a></li>
     <li><a href="#alerts"><i class="icon-bell-alt"></i>
         <?php echo __('Alerts and Notices'); ?></a></li>
+    <li><a href="#queues"><i class="icon-table"></i>
+        <?php echo __('Queues'); ?></a></li>
 </ul>
 <div class="tab_content" id="settings">
 <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
@@ -239,6 +241,10 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
     <?php include STAFFINC_DIR . 'settings-alerts.inc.php'; ?>
 </div>
 
+<div class="hidden tab_content" id="queues">
+    <?php include STAFFINC_DIR . 'queues-ticket.inc.php'; ?>
+</div>
+
 <p style="text-align:center;">
     <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes');?>">
     <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes');?>">
diff --git a/include/staff/templates/advanced-search-criteria.tmpl.php b/include/staff/templates/advanced-search-criteria.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea343245a463f9e0180db2e72dcbd3d458753575
--- /dev/null
+++ b/include/staff/templates/advanced-search-criteria.tmpl.php
@@ -0,0 +1,108 @@
+<?php
+foreach ($form->errors(true) ?: array() as $message) {
+    ?><div class="error-banner"><?php echo $message;?></div><?php
+}
+
+$info = $search->getSearchFields($form);
+foreach (array_keys($info) as $F) {
+    ?><input type="hidden" name="fields[]" value="<?php echo $F; ?>"/><?php
+}
+$errors = !!$form->errors();
+$inbody = false;
+$first_field = true;
+foreach ($form->getFields() as $name=>$field) {
+    @list($name, $sub) = explode('+', $field->get('name'), 2);
+    if ($sub === 'search') {
+        if (!$first_field) {
+            echo '</div></div>';
+        }
+        echo '<div class="adv-search-field-container">';
+        $inbody = false;
+        $first_field = false;
+    }
+    elseif (!$first_field && !$inbody) {
+        echo sprintf('<div class="adv-search-field-body %s">',
+            !$errors && isset($info[$name]) && $info[$name]['active'] ? 'hidden' : '');
+        $inbody = true;
+    }
+?>
+    <fieldset id="field<?php echo $field->getWidget()->id; ?>" <?php
+        $class = array();
+        if (!$field->isVisible())
+            $class[] = "hidden";
+        if ($sub === 'method')
+            $class[] = "adv-search-method";
+        elseif ($sub === 'search')
+            $class[] = "adv-search-field";
+        elseif ($field->get('__searchval__'))
+            $class[] = "adv-search-val";
+        if ($class)
+            echo 'class="'.implode(' ', $class).'"';
+        ?>>
+        <?php echo $field->render(); ?>
+        <?php if (!$errors && $sub === 'search' && isset($info[$name]) && $info[$name]['active']) { ?>
+            <span style="padding-left: 5px">
+            <a href="#"  data-name="<?php echo Format::htmlchars($name); ?>" onclick="javascript:
+    var $this = $(this),
+        name = $this.data('name'),
+        expanded = $this.data('expanded') || false;
+    $this.closest('.adv-search-field-container').find('.adv-search-field-body').slideDown('fast');
+    $this.find('span.faded').hide();
+    $this.find('i').removeClass('icon-caret-right').addClass('icon-caret-down');
+    return false;
+"><i class="icon-caret-right"></i>
+            <span class="faded"><?php echo $search->describeField($info[$name]); ?></span>
+            </a>
+            </span>
+        <?php } ?>
+        <?php foreach ($field->errors() as $E) {
+            ?><div class="error"><?php echo $E; ?></div><?php
+        } ?>
+    </fieldset>
+    <?php if ($name[0] == ':' && substr($name, -7) == '+search') {
+        list($N,) = explode('+', $name, 2);
+?>
+    <input type="hidden" name="fields[]" value="<?php echo $N; ?>"/>
+    <?php }
+}
+if (!$first_field)
+    echo '</div></div>';
+?>
+<div id="extra-fields"></div>
+<hr/>
+<i class="icon-plus-sign"></i>
+<select id="search-add-new-field" name="new-field" style="max-width: 300px;">
+    <option value="">— <?php echo __('Add Other Field'); ?> —</option>
+<?php
+if (is_array($matches)) {
+foreach ($matches as $name => $fields) { ?>
+    <optgroup label="<?php echo $name; ?>">
+<?php
+    foreach ($fields as $id => $desc) { ?>
+        <option value="<?php echo $id; ?>" <?php
+            if (isset($state[$id])) echo 'disabled="disabled"';
+        ?>><?php echo ($desc instanceof FormField ? $desc->getLocal('label') : $desc); ?></option>
+<?php } ?>
+    </optgroup>
+<?php }
+} ?>
+</select>
+<script>
+$(function() {
+  $('#search-add-new-field').on('change', function() {
+    var that=this;
+    $.ajax({
+      url: 'ajax.php/tickets/search/field/'+$(this).val(),
+      type: 'get',
+      dataType: 'json',
+      success: function(json) {
+        if (!json.success)
+          return false;
+        ff_uid = json.ff_uid;
+        $(that).find(':selected').prop('disabled', true);
+        $('#extra-fields').append($(json.html));
+      }
+    });
+  });
+});
+</script>
diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php
index cc14fa18df075e28b0369cb40474791cbd5a5cde..201cb66b0af583203453613a784ad76059bf8ae6 100644
--- a/include/staff/templates/advanced-search.tmpl.php
+++ b/include/staff/templates/advanced-search.tmpl.php
@@ -1,4 +1,4 @@
-<div id="advanced-search">
+<div id="advanced-search" class="advanced-search">
 <h3 class="drag-handle"><?php echo __('Advanced Ticket Search');?></h3>
 <a class="close" href=""><i class="icon-remove-circle"></i></a>
 <hr/>
@@ -6,93 +6,7 @@
 <div class="row">
 <div class="span6">
     <input type="hidden" name="a" value="search">
-<?php
-foreach ($form->errors(true) ?: array() as $message) {
-    ?><div class="error-banner"><?php echo $message;?></div><?php
-}
-
-$info = $search->getSearchFields($form);
-foreach (array_keys($info) as $F) {
-    ?><input type="hidden" name="fields[]" value="<?php echo $F; ?>"/><?php
-}
-$errors = !!$form->errors();
-$inbody = false;
-$first_field = true;
-foreach ($form->getFields() as $name=>$field) {
-    @list($name, $sub) = explode('+', $field->get('name'), 2);
-    if ($sub === 'search') {
-        if (!$first_field) {
-            echo '</div></div>';
-        }
-        echo '<div class="adv-search-field-container">';
-        $inbody = false;
-        $first_field = false;
-    }
-    elseif (!$first_field && !$inbody) {
-        echo sprintf('<div class="adv-search-field-body %s">',
-            !$errors && isset($info[$name]) && $info[$name]['active'] ? 'hidden' : '');
-        $inbody = true;
-    }
-?>
-    <fieldset id="field<?php echo $field->getWidget()->id; ?>" <?php
-        $class = array();
-        if (!$field->isVisible())
-            $class[] = "hidden";
-        if ($sub === 'method')
-            $class[] = "adv-search-method";
-        elseif ($sub === 'search')
-            $class[] = "adv-search-field";
-        elseif ($field->get('__searchval__'))
-            $class[] = "adv-search-val";
-        if ($class)
-            echo 'class="'.implode(' ', $class).'"';
-        ?>>
-        <?php echo $field->render(); ?>
-        <?php if (!$errors && $sub === 'search' && isset($info[$name]) && $info[$name]['active']) { ?>
-            <span style="padding-left: 5px">
-            <a href="#"  data-name="<?php echo Format::htmlchars($name); ?>" onclick="javascript:
-    var $this = $(this),
-        name = $this.data('name'),
-        expanded = $this.data('expanded') || false;
-    $this.closest('.adv-search-field-container').find('.adv-search-field-body').slideDown('fast');
-    $this.find('span.faded').hide();
-    $this.find('i').removeClass('icon-caret-right').addClass('icon-caret-down');
-    return false;
-"><i class="icon-caret-right"></i>
-            <span class="faded"><?php echo $search->describeField($info[$name]); ?></span>
-            </a>
-            </span>
-        <?php } ?>
-        <?php foreach ($field->errors() as $E) {
-            ?><div class="error"><?php echo $E; ?></div><?php
-        } ?>
-    </fieldset>
-    <?php if ($name[0] == ':' && substr($name, -7) == '+search') {
-        list($N,) = explode('+', $name, 2);
-?>
-    <input type="hidden" name="fields[]" value="<?php echo $N; ?>"/>
-    <?php }
-}
-if (!$first_field)
-    echo '</div></div>';
-?>
-<div id="extra-fields"></div>
-<hr/>
-<select id="search-add-new-field" name="new-field" style="max-width: 300px;">
-    <option value="">— <?php echo __('Add Other Field'); ?> —</option>
-<?php
-foreach ($matches as $name => $fields) { ?>
-    <optgroup label="<?php echo $name; ?>">
-<?php
-    foreach ($fields as $id => $desc) { ?>
-        <option value="<?php echo $id; ?>" <?php
-            if (isset($state[$id])) echo 'disabled="disabled"';
-        ?>><?php echo ($desc instanceof FormField ? $desc->getLocal('label') : $desc); ?></option>
-<?php } ?>
-    </optgroup>
-<?php } ?>
-</select>
-
+    <?php include STAFFINC_DIR . 'templates/advanced-search-criteria.tmpl.php'; ?>
 </div>
 <div class="span6" style="border-left:1px solid #888;position:relative;padding-bottom:26px;">
 <div style="margin-bottom: 0.5em;"><b style="font-size: 110%;"><?php echo __('Saved Searches'); ?></b></div>
@@ -208,21 +122,5 @@ $(function() {
       return false;
     });
   }, 200);
-
-  $('#search-add-new-field').on('change', function() {
-    var that=this;
-    $.ajax({
-      url: 'ajax.php/tickets/search/field/'+$(this).val(),
-      type: 'get',
-      dataType: 'json',
-      success: function(json) {
-        if (!json.success)
-          return false;
-        ff_uid = json.ff_uid;
-        $(that).find(':selected').prop('disabled', true);
-        $('#extra-fields').append($(json.html));
-      }
-    });
-  });
 });
 </script>
diff --git a/include/staff/templates/queue-column-condition-prop.tmpl.php b/include/staff/templates/queue-column-condition-prop.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..f29f5947d99b6a57f73df9aa96648b261cb0b0af
--- /dev/null
+++ b/include/staff/templates/queue-column-condition-prop.tmpl.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Calling conventions
+ *
+ * $name - condition field name, like 'thread__lastmessage'
+ * $prop - CSS property name from QueueColumnConditionProperty::$properties
+ * $v - value for the property
+ */
+?>
+<div class="condition-property">
+  <div class="pull-right">
+    <a href="#" onclick="javascript:$(this).closest('.condition-property').remove()"
+      ><i class="icon-trash"></i></a>
+  </div>
+  <div><?php echo mb_convert_case($prop, MB_CASE_TITLE); ?></div>
+<?php
+    $F = QueueColumnConditionProperty::getField($prop);
+    $F->set('name', "prop-{$name}-{$prop}");
+    $F->value = $v;
+    $form = new SimpleForm(array($F), $_POST);
+    echo $F->render();
+    echo $form->getMedia();
+?>
+</div>
diff --git a/include/staff/templates/queue-column-condition.tmpl.php b/include/staff/templates/queue-column-condition.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..d445f4f4d1ac567018b55fc414b876673c5a605f
--- /dev/null
+++ b/include/staff/templates/queue-column-condition.tmpl.php
@@ -0,0 +1,74 @@
+<?php
+// Calling convention:
+//
+// $field - field for the condition (Ticket / Last Update)
+// $properties - currently-configured properties for the condition
+// $condition - <QueueColumnCondition> instance for this condition
+?>
+<div class="condition">
+  <div class="pull-right">
+    <a href="#" onclick="javascript: $(this).closest('.condition').remove();
+      "><i class="icon-trash"></i></a>
+  </div>
+  <?php echo $field->get('label'); ?>
+  <div class="advanced-search">
+<?php
+$name = $field->get('name');
+$parts = SavedSearch::getSearchField($field, $name);
+// Drop the search checkbox field
+unset($parts["{$name}+search"]);
+foreach ($parts as $name=>$F) {
+    if (substr($name, -7) == '+method')
+        // XXX: Hack
+        unset($F->ht['visibility']);
+}
+$form = new SimpleForm($parts);
+foreach ($form->getFields() as $F) { ?>
+    <fieldset id="field<?php echo $F->getWidget()->id;
+        ?>" <?php
+            $class = array();
+            @list($name, $sub) = explode('+', $F->get('name'), 2);
+            if (!$F->isVisible()) $class[] = "hidden";
+            if ($sub === 'method')
+                $class[] = "adv-search-method";
+            elseif ($F->get('__searchval__'))
+                $class[] = "adv-search-val";
+            if ($class)
+                echo 'class="'.implode(' ', $class).'"';
+            ?>>
+        <?php echo $F->render(); ?>
+        <?php foreach ($F->errors() as $E) {
+            ?><div class="error"><?php echo $E; ?></div><?php
+        } ?>
+    </fieldset>
+<?php } ?>
+
+    <div class="properties" style="margin-left: 25px; margin-top: 10px">
+<?php foreach ($condition->getProperties() as $prop=>$v) {
+    include 'queue-column-condition-prop.tmpl.php';
+} ?>
+      <div style="margin-top: 10px">
+        <i class="icon-plus-sign"></i>
+        <select onchange="javascript:
+        var $this = $(this),
+            selected = $this.find(':selected'),
+            container = $this.closest('.properties');
+        $.ajax({
+          url: 'ajax.php/queue/condition/addProperty',
+          data: { prop: selected.val() },
+          dataType: 'html',
+          success: function(html) {
+            $(html).insertBefore(container);
+            selected.prop('disabled', true);
+          }
+        });
+        ">
+          <option>— <?php echo __('Add a property'); ?> —</option>
+<?php foreach (array_keys(QueueColumnConditionProperty::$properties) as $p) {
+    echo sprintf('<option value="%s">%s</option>', $p, mb_convert_case($p, MB_CASE_TITLE));
+} ?>
+        </select>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/include/staff/templates/queue-column.tmpl.php b/include/staff/templates/queue-column.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..a3b4b56dea408d0faecc7cfcc997e064791e0f46
--- /dev/null
+++ b/include/staff/templates/queue-column.tmpl.php
@@ -0,0 +1,123 @@
+<?php
+/**
+ * Calling conventions
+ *
+ * $column - <QueueColumn> instance for this column
+ */
+$colid = $column->getId();
+$data_form = $column->getDataConfigForm($_POST);
+?>
+<ul class="alt tabs">
+  <li class="active"><a href="#<?php echo $colid; ?>-data"><?php echo __('Data'); ?></a></li>
+  <li><a href="#<?php echo $colid; ?>-decorations"><?php echo __('Decorations'); ?></a></li>
+  <li><a href="#<?php echo $colid; ?>-conditions"><?php echo __('Conditions'); ?></a></li>
+</ul>
+
+<div class="tab_content" id="<?php echo $colid; ?>-data">
+<?php
+  print $data_form->asTable();
+?>
+</div>
+
+<div class="hidden tab_content" id="<?php echo $colid; ?>-decorations" style="max-width: 400px">
+  <div class="empty placeholder" style="margin-left: 20px">
+    <em><?php echo __('No decorations for this field'); ?></em>
+  </div>
+  <div style="margin: 20px;">
+    <div class="decoration clear template hidden">
+      <input data-field="input" data-name="decorations[]" value="" type="hidden" />
+      <i data-field="icon"></i>
+      <span data-field="name"></span>
+      <div class="pull-right">
+        <select data-field="position">
+<?php foreach (QueueDecoration::getPositions() as $key=>$desc) {
+          echo sprintf('<option value="%s">%s</option>', $key, Format::htmlchars($desc));
+} ?>
+        </select>
+        <a href="#" data-field="delete" title="<?php echo __('Delete'); ?>"
+            onclick="javascript: 
+            $(this).closest('.decoration').remove();
+            return false;"><i class="icon-trash"></i></a>
+      </div>
+    </div>
+
+    <div style="margin-top: 20px">
+      <i class="icon-plus-sign"></i>
+      <select class="add-decoration">
+        <option>— <?php echo __("Add a decoration"); ?> —</option>
+<?php foreach (CustomQueue::getDecorations('Ticket') as $class) {
+        echo sprintf('<option data-icon="%s" value="%s">%s</option>',
+          $class::$icon, $class, $class::getDescription());
+      } ?>
+      </select>
+    </div>
+
+    <script>
+      $(function() {
+        var addDecoration = function(type, icon, pos) {
+          var template = $('.decoration.template', '#<?php echo $colid; ?>-decorations'),
+              clone = template.clone().show().removeClass('template').insertBefore(template),
+              input = clone.find('[data-field=input]'),
+              name = clone.find('[data-field=name]'),
+              i = clone.find('[data-field=icon]'),
+              position = clone.find('[data-field=position]');
+          input.attr('name', input.data('name'));
+          i.addClass('icon-fixed-width icon-' + icon);
+          name.text(type);
+          if (pos) position.val(pos);
+          template.parent().find('.empty').hide();
+        };
+        $('select.add-decoration', '#<?php echo $colid; ?>-decorations').change(function() {
+          var selected = $(this).find(':selected');
+          addDecoration(selected.text(), selected.data('icon'));
+          selected.prop('disabled', true);
+        });
+        $('#<?php echo $colid; ?>-decorations').click('a[data-field=delete]',
+        function() {
+          var tab = $('#<?php echo $colid; ?>-decorations');
+          if ($('.decoration', tab).length === 0)
+            tab.find('.empty').show();
+        });
+        <?php foreach ($column->getDecorations() as $d) {
+            echo sprintf('addDecoration(%s, %s, %s);',
+                JsonDataEncoder::encode($d::getDescription()),
+                JsonDataEncoder::encode($d::getIcon()),
+                JsonDataEncoder::encode($d->getPosition())
+            );
+        } ?>
+      });
+    </script>
+  </div>
+</div>
+
+<div class="hidden tab_content" id="<?php echo $colid; ?>-conditions">
+  <div style="margin: 0 20px"><?php echo __("Conditions are used to change the view of the data in a row based on some conditions of the data. For instance, a column might be shown bold if some condition is met.");
+  ?></div>
+  <div class="conditions" style="margin: 20px; max-width: 400px">
+<?php foreach ($column->getConditions() as $condition) {
+     include STAFFINC_DIR . 'templates/queue-column-condition.tmpl.php';
+} ?>
+    <div style="margin-top: 20px">
+      <i class="icon-plus-sign"></i>
+      <select id="add-condition" onchange="javascript:
+      var $this = $(this),
+          container = $this.closest('div');
+      $.ajax({
+        url: 'ajax.php/queue/condition/add',
+        data: { field: $(this).find(':selected').val() },
+        dataType: 'html',
+        success: function(html) {
+          $(html).insertBefore(container);
+        }
+      });
+      ">
+        <option>— <?php echo __("Add a condition"); ?> —</option>
+<?php
+      foreach (SavedSearch::getSearchableFields('Ticket') as $path=>$f) {
+          echo sprintf('<option value="%s">%s</option>', $path, Format::htmlchars($f->get('label')));
+      }
+?>
+      </select>
+    </div>
+  </div>
+</div>
diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..32d664d906b4fa0d6076b0dd888275ad8de3af8e
--- /dev/null
+++ b/include/staff/templates/queue-tickets.tmpl.php
@@ -0,0 +1,40 @@
+<?php
+// Calling convention (assumed global scope):
+// $tickets - <QuerySet> with all columns and annotations necessary to
+//      render the full page
+// $count - <int> number of records matching the search / filter part of the
+//      query
+
+$page = ($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
+$pageNav->setURL('tickets.php', $args);
+$tickets = $pageNav->paginate($tickets);
+
+// Identify columns of output
+$columns = $queue->getColumns();
+
+?>
+<table class="list" border="0" cellspacing="1" cellpadding="2" width="940">
+  <thead>
+    <tr>
+<?php
+foreach ($columns as $C) {
+    echo sprintf('<th width="%s">%s</th>', $C->getWidth(),
+        Format::htmlchars($C->getHeading()));
+} ?>
+    </tr>
+  </thead>
+  <tbody>
+<?php
+foreach ($tickets as $T) {
+    echo '<tr>';
+    foreach ($columns as $C) {
+        echo "<td>";
+        echo $C->render($T);
+        echo "</td>";
+    }
+    echo '</tr>';
+}
+?>
+  </tbody>
+</table>
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index c786e28468ab527ec60b88aaf24040d65f1601f9..15881dce53da050a89bc66d7d40cea206d7d5452 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -229,7 +229,7 @@ if (!$view_all_tickets) {
     if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
         $visibility->add(array('dept_id__in' => $depts));
 
-    $tickets->filter(Q::any($visibility));
+    $tickets->filter($visibility);
 }
 
 // TODO :: Apply requested quick filter
diff --git a/scp/ajax.php b/scp/ajax.php
index de27157dc74093cf638db8a5a3e0550384e00857..1351b9fbc58c606076286c55bbc71cd8127be9a9 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -253,6 +253,12 @@ $dispatcher = patterns('',
         url('^/reset-permissions', 'resetPermissions'),
         url('^/change-department', 'changeDepartment'),
         url('^/(?P<id>\d+)/avatar/change', 'setAvatar')
+    )),
+    url('^/queue/', patterns('ajax.search.php:SearchAjaxAPI',
+        url('^(?P<id>\d+/)?preview$', 'previewQueue'),
+        url_get('^addColumn$', 'addColumn'),
+        url_get('^condition/add$', 'addCondition'),
+        url_get('^condition/addProperty$', 'addConditionProperty')
     ))
 );
 
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 86632ba2766ff353f499c2c13b1d4b8b60d68b16..8e5e10b70b92025fa6b9fb27c9fdf92aaad77e42 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -1709,7 +1709,7 @@ time.faq {
     width:100%;
 }
 
-.dialog fieldset {
+fieldset {
     margin:0;
     padding:0 0;
     border:none;
@@ -1771,12 +1771,12 @@ time.faq {
     margin-top: 5px !important;
 }
 
-#advanced-search fieldset {
+.advanced-search fieldset {
   margin-top: 3px;
   position: relative;
 }
-#advanced-search .adv-search-method:before,
-#advanced-search .adv-search-val:before {
+.advanced-search .adv-search-method:before,
+.advanced-search .adv-search-val:before {
   content: "";
   border-left: 2px dotted #ccc;
   border-bottom: 2px dotted #ccc;
@@ -1787,10 +1787,10 @@ time.faq {
   position: absolute;
   left: -16px;
 }
-#advanced-search .adv-search-method {
+.advanced-search .adv-search-method {
   margin-left: 24px;
 }
-#advanced-search .adv-search-val {
+.advanced-search .adv-search-val {
   margin-left: 45px;
 }
 
@@ -2876,7 +2876,9 @@ table.grid.form caption {
 .grid.form .field textarea,
 .grid.form .field select {
   width: 100%;
+  max-width: 100%;
   display: block;
+  box-sizing: border-box;
 }
 .grid.form .field > label {
   display: block;
@@ -2921,6 +2923,49 @@ a.attachment {
     margin-bottom: 0.3em;
 }
 
+#resizable-columns {
+  margin: 10px 0;
+  position: relative;
+}
+#resizable-columns .column-header:hover {
+  cursor: pointer;
+}
+#resizable-columns .column-header {
+  display: inline-block;
+  padding: 5px 20px;
+  background-color: #ddd;
+  margin: 0 1px;
+  position: relative;
+  text-align: center;
+  box-sizing: border-box;
+}
+#resizable-columns .column-header.ui-resizable:not(.active) {
+  opacity: 0.4;
+}
+#resizable-columns .column-header.ui-resizable.active {
+  background-color: #cfe6ff;
+}
+
+.ui-resizable-handle {
+  cursor: pointer;
+  cursor: ew-resize;
+  cursor: col-resize;
+  display: inline-block;
+  vertical-align: bottom;
+  position: absolute;
+  right: 5px;
+  color: #777;
+}
+.decoration + .decoration {
+    margin-top: 10px;
+}
+.advanced-search .condition-property {
+    margin: 7px  0 7px 25px;
+}
+.conditions .condition + .condition {
+    margin-top: 10px;
+}
+
 /* FIXME: Drop this with select2 4.0.1
  * Fixes a rendering issue on Safari
  */
diff --git a/scp/css/spectrum.css b/scp/css/spectrum.css
new file mode 100644
index 0000000000000000000000000000000000000000..ecf6fe482c01ede9de6e83edf4aaa6dbb06ce86c
--- /dev/null
+++ b/scp/css/spectrum.css
@@ -0,0 +1,507 @@
+/***
+Spectrum Colorpicker v1.7.1
+https://github.com/bgrins/spectrum
+Author: Brian Grinstead
+License: MIT
+***/
+
+.sp-container {
+    position:absolute;
+    top:0;
+    left:0;
+    display:inline-block;
+    *display: inline;
+    *zoom: 1;
+    /* https://github.com/bgrins/spectrum/issues/40 */
+    z-index: 9999994;
+    overflow: hidden;
+}
+.sp-container.sp-flat {
+    position: relative;
+}
+
+/* Fix for * { box-sizing: border-box; } */
+.sp-container,
+.sp-container * {
+    -webkit-box-sizing: content-box;
+       -moz-box-sizing: content-box;
+            box-sizing: content-box;
+}
+
+/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
+.sp-top {
+  position:relative;
+  width: 100%;
+  display:inline-block;
+}
+.sp-top-inner {
+   position:absolute;
+   top:0;
+   left:0;
+   bottom:0;
+   right:0;
+}
+.sp-color {
+    position: absolute;
+    top:0;
+    left:0;
+    bottom:0;
+    right:20%;
+}
+.sp-hue {
+    position: absolute;
+    top:0;
+    right:0;
+    bottom:0;
+    left:84%;
+    height: 100%;
+}
+
+.sp-clear-enabled .sp-hue {
+    top:33px;
+    height: 77.5%;
+}
+
+.sp-fill {
+    padding-top: 80%;
+}
+.sp-sat, .sp-val {
+    position: absolute;
+    top:0;
+    left:0;
+    right:0;
+    bottom:0;
+}
+
+.sp-alpha-enabled .sp-top {
+    margin-bottom: 18px;
+}
+.sp-alpha-enabled .sp-alpha {
+    display: block;
+}
+.sp-alpha-handle {
+    position:absolute;
+    top:-4px;
+    bottom: -4px;
+    width: 6px;
+    left: 50%;
+    cursor: pointer;
+    border: 1px solid black;
+    background: white;
+    opacity: .8;
+}
+.sp-alpha {
+    display: none;
+    position: absolute;
+    bottom: -14px;
+    right: 0;
+    left: 0;
+    height: 8px;
+}
+.sp-alpha-inner {
+    border: solid 1px #333;
+}
+
+.sp-clear {
+    display: none;
+}
+
+.sp-clear.sp-clear-display {
+    background-position: center;
+}
+
+.sp-clear-enabled .sp-clear {
+    display: block;
+    position:absolute;
+    top:0px;
+    right:0;
+    bottom:0;
+    left:84%;
+    height: 28px;
+}
+
+/* Don't allow text selection */
+.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button  {
+    -webkit-user-select:none;
+    -moz-user-select: -moz-none;
+    -o-user-select:none;
+    user-select: none;
+}
+
+.sp-container.sp-input-disabled .sp-input-container {
+    display: none;
+}
+.sp-container.sp-buttons-disabled .sp-button-container {
+    display: none;
+}
+.sp-container.sp-palette-buttons-disabled .sp-palette-button-container {
+    display: none;
+}
+.sp-palette-only .sp-picker-container {
+    display: none;
+}
+.sp-palette-disabled .sp-palette-container {
+    display: none;
+}
+
+.sp-initial-disabled .sp-initial {
+    display: none;
+}
+
+
+/* Gradients for hue, saturation and value instead of images.  Not pretty... but it works */
+.sp-sat {
+    background-image: -webkit-gradient(linear,  0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0)));
+    background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0));
+    background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
+    background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
+    background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
+    background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
+    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
+    filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
+}
+.sp-val {
+    background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0)));
+    background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0));
+    background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
+    background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
+    background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
+    background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
+    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
+    filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
+}
+
+.sp-hue {
+    background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+    background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+    background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+    background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
+    background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+    background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+}
+
+/* IE filters do not support multiple color stops.
+   Generate 6 divs, line them up, and do two color gradients for each.
+   Yes, really.
+ */
+.sp-1 {
+    height:17%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
+}
+.sp-2 {
+    height:16%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
+}
+.sp-3 {
+    height:17%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
+}
+.sp-4 {
+    height:17%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
+}
+.sp-5 {
+    height:16%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
+}
+.sp-6 {
+    height:17%;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
+}
+
+.sp-hidden {
+    display: none !important;
+}
+
+/* Clearfix hack */
+.sp-cf:before, .sp-cf:after { content: ""; display: table; }
+.sp-cf:after { clear: both; }
+.sp-cf { *zoom: 1; }
+
+/* Mobile devices, make hue slider bigger so it is easier to slide */
+@media (max-device-width: 480px) {
+    .sp-color { right: 40%; }
+    .sp-hue { left: 63%; }
+    .sp-fill { padding-top: 60%; }
+}
+.sp-dragger {
+   border-radius: 5px;
+   height: 5px;
+   width: 5px;
+   border: 1px solid #fff;
+   background: #000;
+   cursor: pointer;
+   position:absolute;
+   top:0;
+   left: 0;
+}
+.sp-slider {
+    position: absolute;
+    top:0;
+    cursor:pointer;
+    height: 3px;
+    left: -1px;
+    right: -1px;
+    border: 1px solid #000;
+    background: white;
+    opacity: .8;
+}
+
+/*
+Theme authors:
+Here are the basic themeable display options (colors, fonts, global widths).
+See http://bgrins.github.io/spectrum/themes/ for instructions.
+*/
+
+.sp-container {
+    border-radius: 0;
+    background-color: #ECECEC;
+    border: solid 1px #f0c49B;
+    padding: 0;
+}
+.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear {
+    font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+    box-sizing: border-box;
+}
+.sp-top {
+    margin-bottom: 3px;
+}
+.sp-color, .sp-hue, .sp-clear {
+    border: solid 1px #666;
+}
+
+/* Input */
+.sp-input-container {
+    float:right;
+    width: 100px;
+    margin-bottom: 4px;
+}
+.sp-initial-disabled  .sp-input-container {
+    width: 100%;
+}
+.sp-input {
+   font-size: 12px !important;
+   border: 1px inset;
+   padding: 4px 5px;
+   margin: 0;
+   width: 100%;
+   background:transparent;
+   border-radius: 3px;
+   color: #222;
+}
+.sp-input:focus  {
+    border: 1px solid orange;
+}
+.sp-input.sp-validation-error {
+    border: 1px solid red;
+    background: #fdd;
+}
+.sp-picker-container , .sp-palette-container {
+    float:left;
+    position: relative;
+    padding: 10px;
+    padding-bottom: 300px;
+    margin-bottom: -290px;
+}
+.sp-picker-container {
+    width: 172px;
+    border-left: solid 1px #fff;
+}
+
+/* Palettes */
+.sp-palette-container {
+    border-right: solid 1px #ccc;
+}
+
+.sp-palette-only .sp-palette-container {
+    border: 0;
+}
+
+.sp-palette .sp-thumb-el {
+    display: block;
+    position:relative;
+    float:left;
+    width: 24px;
+    height: 15px;
+    margin: 3px;
+    cursor: pointer;
+    border:solid 2px transparent;
+}
+.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
+    border-color: orange;
+}
+.sp-thumb-el {
+    position:relative;
+}
+
+/* Initial */
+.sp-initial {
+    float: left;
+    border: solid 1px #333;
+}
+.sp-initial span {
+    width: 30px;
+    height: 25px;
+    border:none;
+    display:block;
+    float:left;
+    margin:0;
+}
+
+.sp-initial .sp-clear-display {
+    background-position: center;
+}
+
+/* Buttons */
+.sp-palette-button-container,
+.sp-button-container {
+    float: right;
+}
+
+/* Replacer (the little preview div that shows up instead of the <input>) */
+.sp-replacer {
+    margin:0;
+    overflow:hidden;
+    cursor:pointer;
+    padding: 4px;
+    display:inline-block;
+    *zoom: 1;
+    *display: inline;
+    border: solid 1px #91765d;
+    background: #eee;
+    color: #333;
+    vertical-align: middle;
+}
+.sp-replacer:hover, .sp-replacer.sp-active {
+    border-color: #F0C49B;
+    color: #111;
+}
+.sp-replacer.sp-disabled {
+    cursor:default;
+    border-color: silver;
+    color: silver;
+}
+.sp-dd {
+    padding: 2px 0;
+    height: 16px;
+    line-height: 16px;
+    float:left;
+    font-size:10px;
+}
+.sp-preview {
+    position:relative;
+    width:25px;
+    height: 20px;
+    border: solid 1px #222;
+    margin-right: 5px;
+    float:left;
+    z-index: 0;
+}
+
+.sp-palette {
+    *width: 220px;
+    max-width: 220px;
+}
+.sp-palette .sp-thumb-el {
+    width:16px;
+    height: 16px;
+    margin:2px 1px;
+    border: solid 1px #d0d0d0;
+}
+
+.sp-container {
+    padding-bottom:0;
+}
+
+
+/* Buttons: http://hellohappy.org/css3-buttons/ */
+.sp-container button {
+  background-color: #eeeeee;
+  background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
+  background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
+  background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
+  background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
+  background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
+  border: 1px solid #ccc;
+  border-bottom: 1px solid #bbb;
+  border-radius: 3px;
+  color: #333;
+  font-size: 14px;
+  line-height: 1;
+  padding: 5px 4px;
+  text-align: center;
+  text-shadow: 0 1px 0 #eee;
+  vertical-align: middle;
+}
+.sp-container button:hover {
+    background-color: #dddddd;
+    background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
+    background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
+    background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
+    background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
+    background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
+    border: 1px solid #bbb;
+    border-bottom: 1px solid #999;
+    cursor: pointer;
+    text-shadow: 0 1px 0 #ddd;
+}
+.sp-container button:active {
+    border: 1px solid #aaa;
+    border-bottom: 1px solid #888;
+    -webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+    -moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+    -ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+    -o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+    box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
+}
+.sp-cancel {
+    font-size: 11px;
+    color: #d93f3f !important;
+    margin:0;
+    padding:2px;
+    margin-right: 5px;
+    vertical-align: middle;
+    text-decoration:none;
+
+}
+.sp-cancel:hover {
+    color: #d93f3f !important;
+    text-decoration: underline;
+}
+
+
+.sp-palette span:hover, .sp-palette span.sp-thumb-active {
+    border-color: #000;
+}
+
+.sp-preview, .sp-alpha, .sp-thumb-el {
+    position:relative;
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
+}
+.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner {
+    display:block;
+    position:absolute;
+    top:0;left:0;bottom:0;right:0;
+}
+
+.sp-palette .sp-thumb-inner {
+    background-position: 50% 50%;
+    background-repeat: no-repeat;
+}
+
+.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner {
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
+}
+
+.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner {
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
+}
+
+.sp-clear-display {
+    background-repeat:no-repeat;
+    background-position: center;
+    background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==);
+}
diff --git a/scp/js/scp.js b/scp/js/scp.js
index c825c051cc64764435c33debafd976b3c0c53324..82e2cd81cf5c3c117402c661f0d64ca782a3956c 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -945,7 +945,7 @@ $(document).on('click.tab', 'ul.tabs > li > a', function(e) {
         $ul.children('li.active').removeClass('active');
         $(this).closest('li').addClass('active');
         $container.children('.tab_content').hide();
-        $tab.fadeIn('fast');
+        $tab.fadeIn('fast').show();
         return false;
     }
 
@@ -1228,3 +1228,51 @@ window.relativeAdjust = setInterval(function() {
   });
 }, 20000);
 
+// Add 'afterShow' event to jQuery elements,
+// thanks http://stackoverflow.com/a/1225238/1025836
+(function ($) {
+    var _oldShow = $.fn.show;
+
+    $.fn.show = function (/*speed, easing, callback*/) {
+        var argsArray = Array.prototype.slice.call(arguments),
+            duration = argsArray[0],
+            easing,
+            callback,
+            callbackArgIndex;
+
+        // jQuery recursively calls show sometimes; we shouldn't
+        //  handle such situations. Pass it to original show method.
+        if (!this.selector) {
+            _oldShow.apply(this, argsArray);
+            return this;
+        }
+
+        if (argsArray.length === 2) {
+            if ($.isFunction(argsArray[1])) {
+                callback = argsArray[1];
+                callbackArgIndex = 1;
+            } else {
+                easing = argsArray[1];
+            }
+        } else if (argsArray.length === 3) {
+            easing = argsArray[1];
+            callback = argsArray[2];
+            callbackArgIndex = 2;
+        }
+        return $(this).each(function () {
+            var obj = $(this),
+                oldCallback = callback,
+                newCallback = function () {
+                    if ($.isFunction(oldCallback)) {
+                        oldCallback.apply(obj);
+                    }
+                };
+            if (callback) {
+                argsArray[callbackArgIndex] = newCallback;
+            }
+            obj.trigger('beforeShow');
+            _oldShow.apply(obj, argsArray);
+            obj.trigger('afterShow');
+        });
+    };
+})(jQuery);
diff --git a/scp/js/spectrum.js b/scp/js/spectrum.js
new file mode 100644
index 0000000000000000000000000000000000000000..39ffcd7e5c5c9db1942d62e7d4a915c1530cff44
--- /dev/null
+++ b/scp/js/spectrum.js
@@ -0,0 +1,2317 @@
+// Spectrum Colorpicker v1.7.1
+// https://github.com/bgrins/spectrum
+// Author: Brian Grinstead
+// License: MIT
+
+(function (factory) {
+    "use strict";
+
+    if (typeof define === 'function' && define.amd) { // AMD
+        define(['jquery'], factory);
+    }
+    else if (typeof exports == "object" && typeof module == "object") { // CommonJS
+        module.exports = factory;
+    }
+    else { // Browser
+        factory(jQuery);
+    }
+})(function($, undefined) {
+    "use strict";
+
+    var defaultOpts = {
+
+        // Callbacks
+        beforeShow: noop,
+        move: noop,
+        change: noop,
+        show: noop,
+        hide: noop,
+
+        // Options
+        color: false,
+        flat: false,
+        showInput: false,
+        allowEmpty: false,
+        showButtons: true,
+        clickoutFiresChange: true,
+        showInitial: false,
+        showPalette: false,
+        showPaletteOnly: false,
+        hideAfterPaletteSelect: false,
+        togglePaletteOnly: false,
+        showSelectionPalette: true,
+        localStorageKey: false,
+        appendTo: "body",
+        maxSelectionSize: 7,
+        cancelText: "cancel",
+        chooseText: "choose",
+        togglePaletteMoreText: "more",
+        togglePaletteLessText: "less",
+        clearText: "Clear Color Selection",
+        noColorSelectedText: "No Color Selected",
+        preferredFormat: false,
+        className: "", // Deprecated - use containerClassName and replacerClassName instead.
+        containerClassName: "",
+        replacerClassName: "",
+        showAlpha: false,
+        theme: "sp-light",
+        palette: [["#ffffff", "#000000", "#ff0000", "#ff8000", "#ffff00", "#008000", "#0000ff", "#4b0082", "#9400d3"]],
+        selectionPalette: [],
+        disabled: false,
+        offset: null
+    },
+    spectrums = [],
+    IE = !!/msie/i.exec( window.navigator.userAgent ),
+    rgbaSupport = (function() {
+        function contains( str, substr ) {
+            return !!~('' + str).indexOf(substr);
+        }
+
+        var elem = document.createElement('div');
+        var style = elem.style;
+        style.cssText = 'background-color:rgba(0,0,0,.5)';
+        return contains(style.backgroundColor, 'rgba') || contains(style.backgroundColor, 'hsla');
+    })(),
+    replaceInput = [
+        "<div class='sp-replacer'>",
+            "<div class='sp-preview'><div class='sp-preview-inner'></div></div>",
+            "<div class='sp-dd'>&#9660;</div>",
+        "</div>"
+    ].join(''),
+    markup = (function () {
+
+        // IE does not support gradients with multiple stops, so we need to simulate
+        //  that for the rainbow slider with 8 divs that each have a single gradient
+        var gradientFix = "";
+        if (IE) {
+            for (var i = 1; i <= 6; i++) {
+                gradientFix += "<div class='sp-" + i + "'></div>";
+            }
+        }
+
+        return [
+            "<div class='sp-container sp-hidden'>",
+                "<div class='sp-palette-container'>",
+                    "<div class='sp-palette sp-thumb sp-cf'></div>",
+                    "<div class='sp-palette-button-container sp-cf'>",
+                        "<button type='button' class='sp-palette-toggle'></button>",
+                    "</div>",
+                "</div>",
+                "<div class='sp-picker-container'>",
+                    "<div class='sp-top sp-cf'>",
+                        "<div class='sp-fill'></div>",
+                        "<div class='sp-top-inner'>",
+                            "<div class='sp-color'>",
+                                "<div class='sp-sat'>",
+                                    "<div class='sp-val'>",
+                                        "<div class='sp-dragger'></div>",
+                                    "</div>",
+                                "</div>",
+                            "</div>",
+                            "<div class='sp-clear sp-clear-display'>",
+                            "</div>",
+                            "<div class='sp-hue'>",
+                                "<div class='sp-slider'></div>",
+                                gradientFix,
+                            "</div>",
+                        "</div>",
+                        "<div class='sp-alpha'><div class='sp-alpha-inner'><div class='sp-alpha-handle'></div></div></div>",
+                    "</div>",
+                    "<div class='sp-input-container sp-cf'>",
+                        "<input class='sp-input' type='text' spellcheck='false'  />",
+                    "</div>",
+                    "<div class='sp-initial sp-thumb sp-cf'></div>",
+                    "<div class='sp-button-container sp-cf'>",
+                        "<a class='sp-cancel' href='#'></a>",
+                        "<button type='button' class='sp-choose'></button>",
+                    "</div>",
+                "</div>",
+            "</div>"
+        ].join("");
+    })();
+
+    function paletteTemplate (p, color, className, opts) {
+        var html = [];
+        for (var i = 0; i < p.length; i++) {
+            var current = p[i];
+            if(current) {
+                var tiny = tinycolor(current);
+                var c = tiny.toHsl().l < 0.5 ? "sp-thumb-el sp-thumb-dark" : "sp-thumb-el sp-thumb-light";
+                c += (tinycolor.equals(color, current)) ? " sp-thumb-active" : "";
+                var formattedString = tiny.toString(opts.preferredFormat || "rgb");
+                var swatchStyle = rgbaSupport ? ("background-color:" + tiny.toRgbString()) : "filter:" + tiny.toFilter();
+                html.push('<span title="' + formattedString + '" data-color="' + tiny.toRgbString() + '" class="' + c + '"><span class="sp-thumb-inner" style="' + swatchStyle + ';" /></span>');
+            } else {
+                var cls = 'sp-clear-display';
+                html.push($('<div />')
+                    .append($('<span data-color="" style="background-color:transparent;" class="' + cls + '"></span>')
+                        .attr('title', opts.noColorSelectedText)
+                    )
+                    .html()
+                );
+            }
+        }
+        return "<div class='sp-cf " + className + "'>" + html.join('') + "</div>";
+    }
+
+    function hideAll() {
+        for (var i = 0; i < spectrums.length; i++) {
+            if (spectrums[i]) {
+                spectrums[i].hide();
+            }
+        }
+    }
+
+    function instanceOptions(o, callbackContext) {
+        var opts = $.extend({}, defaultOpts, o);
+        opts.callbacks = {
+            'move': bind(opts.move, callbackContext),
+            'change': bind(opts.change, callbackContext),
+            'show': bind(opts.show, callbackContext),
+            'hide': bind(opts.hide, callbackContext),
+            'beforeShow': bind(opts.beforeShow, callbackContext)
+        };
+
+        return opts;
+    }
+
+    function spectrum(element, o) {
+
+        var opts = instanceOptions(o, element),
+            flat = opts.flat,
+            showSelectionPalette = opts.showSelectionPalette,
+            localStorageKey = opts.localStorageKey,
+            theme = opts.theme,
+            callbacks = opts.callbacks,
+            resize = throttle(reflow, 10),
+            visible = false,
+            isDragging = false,
+            dragWidth = 0,
+            dragHeight = 0,
+            dragHelperHeight = 0,
+            slideHeight = 0,
+            slideWidth = 0,
+            alphaWidth = 0,
+            alphaSlideHelperWidth = 0,
+            slideHelperHeight = 0,
+            currentHue = 0,
+            currentSaturation = 0,
+            currentValue = 0,
+            currentAlpha = 1,
+            palette = [],
+            paletteArray = [],
+            paletteLookup = {},
+            selectionPalette = opts.selectionPalette.slice(0),
+            maxSelectionSize = opts.maxSelectionSize,
+            draggingClass = "sp-dragging",
+            shiftMovementDirection = null;
+
+        var doc = element.ownerDocument,
+            body = doc.body,
+            boundElement = $(element),
+            disabled = false,
+            container = $(markup, doc).addClass(theme),
+            pickerContainer = container.find(".sp-picker-container"),
+            dragger = container.find(".sp-color"),
+            dragHelper = container.find(".sp-dragger"),
+            slider = container.find(".sp-hue"),
+            slideHelper = container.find(".sp-slider"),
+            alphaSliderInner = container.find(".sp-alpha-inner"),
+            alphaSlider = container.find(".sp-alpha"),
+            alphaSlideHelper = container.find(".sp-alpha-handle"),
+            textInput = container.find(".sp-input"),
+            paletteContainer = container.find(".sp-palette"),
+            initialColorContainer = container.find(".sp-initial"),
+            cancelButton = container.find(".sp-cancel"),
+            clearButton = container.find(".sp-clear"),
+            chooseButton = container.find(".sp-choose"),
+            toggleButton = container.find(".sp-palette-toggle"),
+            isInput = boundElement.is("input"),
+            isInputTypeColor = isInput && boundElement.attr("type") === "color" && inputTypeColorSupport(),
+            shouldReplace = isInput && !flat,
+            replacer = (shouldReplace) ? $(replaceInput).addClass(theme).addClass(opts.className).addClass(opts.replacerClassName) : $([]),
+            offsetElement = (shouldReplace) ? replacer : boundElement,
+            previewElement = replacer.find(".sp-preview-inner"),
+            initialColor = opts.color || (isInput && boundElement.val()),
+            colorOnShow = false,
+            preferredFormat = opts.preferredFormat,
+            currentPreferredFormat = preferredFormat,
+            clickoutFiresChange = !opts.showButtons || opts.clickoutFiresChange,
+            isEmpty = !initialColor,
+            allowEmpty = opts.allowEmpty && !isInputTypeColor;
+
+        function applyOptions() {
+
+            if (opts.showPaletteOnly) {
+                opts.showPalette = true;
+            }
+
+            toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText);
+
+            if (opts.palette) {
+                palette = opts.palette.slice(0);
+                paletteArray = $.isArray(palette[0]) ? palette : [palette];
+                paletteLookup = {};
+                for (var i = 0; i < paletteArray.length; i++) {
+                    for (var j = 0; j < paletteArray[i].length; j++) {
+                        var rgb = tinycolor(paletteArray[i][j]).toRgbString();
+                        paletteLookup[rgb] = true;
+                    }
+                }
+            }
+
+            container.toggleClass("sp-flat", flat);
+            container.toggleClass("sp-input-disabled", !opts.showInput);
+            container.toggleClass("sp-alpha-enabled", opts.showAlpha);
+            container.toggleClass("sp-clear-enabled", allowEmpty);
+            container.toggleClass("sp-buttons-disabled", !opts.showButtons);
+            container.toggleClass("sp-palette-buttons-disabled", !opts.togglePaletteOnly);
+            container.toggleClass("sp-palette-disabled", !opts.showPalette);
+            container.toggleClass("sp-palette-only", opts.showPaletteOnly);
+            container.toggleClass("sp-initial-disabled", !opts.showInitial);
+            container.addClass(opts.className).addClass(opts.containerClassName);
+
+            reflow();
+        }
+
+        function initialize() {
+
+            if (IE) {
+                container.find("*:not(input)").attr("unselectable", "on");
+            }
+
+            applyOptions();
+
+            if (shouldReplace) {
+                boundElement.after(replacer).hide();
+            }
+
+            if (!allowEmpty) {
+                clearButton.hide();
+            }
+
+            if (flat) {
+                boundElement.after(container).hide();
+            }
+            else {
+
+                var appendTo = opts.appendTo === "parent" ? boundElement.parent() : $(opts.appendTo);
+                if (appendTo.length !== 1) {
+                    appendTo = $("body");
+                }
+
+                appendTo.append(container);
+            }
+
+            updateSelectionPaletteFromStorage();
+
+            offsetElement.bind("click.spectrum touchstart.spectrum", function (e) {
+                if (!disabled) {
+                    toggle();
+                }
+
+                e.stopPropagation();
+
+                if (!$(e.target).is("input")) {
+                    e.preventDefault();
+                }
+            });
+
+            if(boundElement.is(":disabled") || (opts.disabled === true)) {
+                disable();
+            }
+
+            // Prevent clicks from bubbling up to document.  This would cause it to be hidden.
+            container.click(stopPropagation);
+
+            // Handle user typed input
+            textInput.change(setFromTextInput);
+            textInput.bind("paste", function () {
+                setTimeout(setFromTextInput, 1);
+            });
+            textInput.keydown(function (e) { if (e.keyCode == 13) { setFromTextInput(); } });
+
+            cancelButton.text(opts.cancelText);
+            cancelButton.bind("click.spectrum", function (e) {
+                e.stopPropagation();
+                e.preventDefault();
+                revert();
+                hide();
+            });
+
+            clearButton.attr("title", opts.clearText);
+            clearButton.bind("click.spectrum", function (e) {
+                e.stopPropagation();
+                e.preventDefault();
+                isEmpty = true;
+                move();
+
+                if(flat) {
+                    //for the flat style, this is a change event
+                    updateOriginalInput(true);
+                }
+            });
+
+            chooseButton.text(opts.chooseText);
+            chooseButton.bind("click.spectrum", function (e) {
+                e.stopPropagation();
+                e.preventDefault();
+
+                if (IE && textInput.is(":focus")) {
+                    textInput.trigger('change');
+                }
+
+                if (isValid()) {
+                    updateOriginalInput(true);
+                    hide();
+                }
+            });
+
+            toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText);
+            toggleButton.bind("click.spectrum", function (e) {
+                e.stopPropagation();
+                e.preventDefault();
+
+                opts.showPaletteOnly = !opts.showPaletteOnly;
+
+                // To make sure the Picker area is drawn on the right, next to the
+                // Palette area (and not below the palette), first move the Palette
+                // to the left to make space for the picker, plus 5px extra.
+                // The 'applyOptions' function puts the whole container back into place
+                // and takes care of the button-text and the sp-palette-only CSS class.
+                if (!opts.showPaletteOnly && !flat) {
+                    container.css('left', '-=' + (pickerContainer.outerWidth(true) + 5));
+                }
+                applyOptions();
+            });
+
+            draggable(alphaSlider, function (dragX, dragY, e) {
+                currentAlpha = (dragX / alphaWidth);
+                isEmpty = false;
+                if (e.shiftKey) {
+                    currentAlpha = Math.round(currentAlpha * 10) / 10;
+                }
+
+                move();
+            }, dragStart, dragStop);
+
+            draggable(slider, function (dragX, dragY) {
+                currentHue = parseFloat(dragY / slideHeight);
+                isEmpty = false;
+                if (!opts.showAlpha) {
+                    currentAlpha = 1;
+                }
+                move();
+            }, dragStart, dragStop);
+
+            draggable(dragger, function (dragX, dragY, e) {
+
+                // shift+drag should snap the movement to either the x or y axis.
+                if (!e.shiftKey) {
+                    shiftMovementDirection = null;
+                }
+                else if (!shiftMovementDirection) {
+                    var oldDragX = currentSaturation * dragWidth;
+                    var oldDragY = dragHeight - (currentValue * dragHeight);
+                    var furtherFromX = Math.abs(dragX - oldDragX) > Math.abs(dragY - oldDragY);
+
+                    shiftMovementDirection = furtherFromX ? "x" : "y";
+                }
+
+                var setSaturation = !shiftMovementDirection || shiftMovementDirection === "x";
+                var setValue = !shiftMovementDirection || shiftMovementDirection === "y";
+
+                if (setSaturation) {
+                    currentSaturation = parseFloat(dragX / dragWidth);
+                }
+                if (setValue) {
+                    currentValue = parseFloat((dragHeight - dragY) / dragHeight);
+                }
+
+                isEmpty = false;
+                if (!opts.showAlpha) {
+                    currentAlpha = 1;
+                }
+
+                move();
+
+            }, dragStart, dragStop);
+
+            if (!!initialColor) {
+                set(initialColor);
+
+                // In case color was black - update the preview UI and set the format
+                // since the set function will not run (default color is black).
+                updateUI();
+                currentPreferredFormat = preferredFormat || tinycolor(initialColor).format;
+
+                addColorToSelectionPalette(initialColor);
+            }
+            else {
+                updateUI();
+            }
+
+            if (flat) {
+                show();
+            }
+
+            function paletteElementClick(e) {
+                if (e.data && e.data.ignore) {
+                    set($(e.target).closest(".sp-thumb-el").data("color"));
+                    move();
+                }
+                else {
+                    set($(e.target).closest(".sp-thumb-el").data("color"));
+                    move();
+                    updateOriginalInput(true);
+                    if (opts.hideAfterPaletteSelect) {
+                      hide();
+                    }
+                }
+
+                return false;
+            }
+
+            var paletteEvent = IE ? "mousedown.spectrum" : "click.spectrum touchstart.spectrum";
+            paletteContainer.delegate(".sp-thumb-el", paletteEvent, paletteElementClick);
+            initialColorContainer.delegate(".sp-thumb-el:nth-child(1)", paletteEvent, { ignore: true }, paletteElementClick);
+        }
+
+        function updateSelectionPaletteFromStorage() {
+
+            if (localStorageKey && window.localStorage) {
+
+                // Migrate old palettes over to new format.  May want to remove this eventually.
+                try {
+                    var oldPalette = window.localStorage[localStorageKey].split(",#");
+                    if (oldPalette.length > 1) {
+                        delete window.localStorage[localStorageKey];
+                        $.each(oldPalette, function(i, c) {
+                             addColorToSelectionPalette(c);
+                        });
+                    }
+                }
+                catch(e) { }
+
+                try {
+                    selectionPalette = window.localStorage[localStorageKey].split(";");
+                }
+                catch (e) { }
+            }
+        }
+
+        function addColorToSelectionPalette(color) {
+            if (showSelectionPalette) {
+                var rgb = tinycolor(color).toRgbString();
+                if (!paletteLookup[rgb] && $.inArray(rgb, selectionPalette) === -1) {
+                    selectionPalette.push(rgb);
+                    while(selectionPalette.length > maxSelectionSize) {
+                        selectionPalette.shift();
+                    }
+                }
+
+                if (localStorageKey && window.localStorage) {
+                    try {
+                        window.localStorage[localStorageKey] = selectionPalette.join(";");
+                    }
+                    catch(e) { }
+                }
+            }
+        }
+
+        function getUniqueSelectionPalette() {
+            var unique = [];
+            if (opts.showPalette) {
+                for (var i = 0; i < selectionPalette.length; i++) {
+                    var rgb = tinycolor(selectionPalette[i]).toRgbString();
+
+                    if (!paletteLookup[rgb]) {
+                        unique.push(selectionPalette[i]);
+                    }
+                }
+            }
+
+            return unique.reverse().slice(0, opts.maxSelectionSize);
+        }
+
+        function drawPalette() {
+
+            var currentColor = get();
+
+            var html = $.map(paletteArray, function (palette, i) {
+                return paletteTemplate(palette, currentColor, "sp-palette-row sp-palette-row-" + i, opts);
+            });
+
+            updateSelectionPaletteFromStorage();
+
+            if (selectionPalette) {
+                html.push(paletteTemplate(getUniqueSelectionPalette(), currentColor, "sp-palette-row sp-palette-row-selection", opts));
+            }
+
+            paletteContainer.html(html.join(""));
+        }
+
+        function drawInitial() {
+            if (opts.showInitial) {
+                var initial = colorOnShow;
+                var current = get();
+                initialColorContainer.html(paletteTemplate([initial, current], current, "sp-palette-row-initial", opts));
+            }
+        }
+
+        function dragStart() {
+            if (dragHeight <= 0 || dragWidth <= 0 || slideHeight <= 0) {
+                reflow();
+            }
+            isDragging = true;
+            container.addClass(draggingClass);
+            shiftMovementDirection = null;
+            boundElement.trigger('dragstart.spectrum', [ get() ]);
+        }
+
+        function dragStop() {
+            isDragging = false;
+            container.removeClass(draggingClass);
+            boundElement.trigger('dragstop.spectrum', [ get() ]);
+        }
+
+        function setFromTextInput() {
+
+            var value = textInput.val();
+
+            if ((value === null || value === "") && allowEmpty) {
+                set(null);
+                updateOriginalInput(true);
+            }
+            else {
+                var tiny = tinycolor(value);
+                if (tiny.isValid()) {
+                    set(tiny);
+                    updateOriginalInput(true);
+                }
+                else {
+                    textInput.addClass("sp-validation-error");
+                }
+            }
+        }
+
+        function toggle() {
+            if (visible) {
+                hide();
+            }
+            else {
+                show();
+            }
+        }
+
+        function show() {
+            var event = $.Event('beforeShow.spectrum');
+
+            if (visible) {
+                reflow();
+                return;
+            }
+
+            boundElement.trigger(event, [ get() ]);
+
+            if (callbacks.beforeShow(get()) === false || event.isDefaultPrevented()) {
+                return;
+            }
+
+            hideAll();
+            visible = true;
+
+            $(doc).bind("keydown.spectrum", onkeydown);
+            $(doc).bind("click.spectrum", clickout);
+            $(window).bind("resize.spectrum", resize);
+            replacer.addClass("sp-active");
+            container.removeClass("sp-hidden");
+
+            reflow();
+            updateUI();
+
+            colorOnShow = get();
+
+            drawInitial();
+            callbacks.show(colorOnShow);
+            boundElement.trigger('show.spectrum', [ colorOnShow ]);
+        }
+
+        function onkeydown(e) {
+            // Close on ESC
+            if (e.keyCode === 27) {
+                hide();
+            }
+        }
+
+        function clickout(e) {
+            // Return on right click.
+            if (e.button == 2) { return; }
+
+            // If a drag event was happening during the mouseup, don't hide
+            // on click.
+            if (isDragging) { return; }
+
+            if (clickoutFiresChange) {
+                updateOriginalInput(true);
+            }
+            else {
+                revert();
+            }
+            hide();
+        }
+
+        function hide() {
+            // Return if hiding is unnecessary
+            if (!visible || flat) { return; }
+            visible = false;
+
+            $(doc).unbind("keydown.spectrum", onkeydown);
+            $(doc).unbind("click.spectrum", clickout);
+            $(window).unbind("resize.spectrum", resize);
+
+            replacer.removeClass("sp-active");
+            container.addClass("sp-hidden");
+
+            callbacks.hide(get());
+            boundElement.trigger('hide.spectrum', [ get() ]);
+        }
+
+        function revert() {
+            set(colorOnShow, true);
+        }
+
+        function set(color, ignoreFormatChange) {
+            if (tinycolor.equals(color, get())) {
+                // Update UI just in case a validation error needs
+                // to be cleared.
+                updateUI();
+                return;
+            }
+
+            var newColor, newHsv;
+            if (!color && allowEmpty) {
+                isEmpty = true;
+            } else {
+                isEmpty = false;
+                newColor = tinycolor(color);
+                newHsv = newColor.toHsv();
+
+                currentHue = (newHsv.h % 360) / 360;
+                currentSaturation = newHsv.s;
+                currentValue = newHsv.v;
+                currentAlpha = newHsv.a;
+            }
+            updateUI();
+
+            if (newColor && newColor.isValid() && !ignoreFormatChange) {
+                currentPreferredFormat = preferredFormat || newColor.getFormat();
+            }
+        }
+
+        function get(opts) {
+            opts = opts || { };
+
+            if (allowEmpty && isEmpty) {
+                return null;
+            }
+
+            return tinycolor.fromRatio({
+                h: currentHue,
+                s: currentSaturation,
+                v: currentValue,
+                a: Math.round(currentAlpha * 100) / 100
+            }, { format: opts.format || currentPreferredFormat });
+        }
+
+        function isValid() {
+            return !textInput.hasClass("sp-validation-error");
+        }
+
+        function move() {
+            updateUI();
+
+            callbacks.move(get());
+            boundElement.trigger('move.spectrum', [ get() ]);
+        }
+
+        function updateUI() {
+
+            textInput.removeClass("sp-validation-error");
+
+            updateHelperLocations();
+
+            // Update dragger background color (gradients take care of saturation and value).
+            var flatColor = tinycolor.fromRatio({ h: currentHue, s: 1, v: 1 });
+            dragger.css("background-color", flatColor.toHexString());
+
+            // Get a format that alpha will be included in (hex and names ignore alpha)
+            var format = currentPreferredFormat;
+            if (currentAlpha < 1 && !(currentAlpha === 0 && format === "name")) {
+                if (format === "hex" || format === "hex3" || format === "hex6" || format === "name") {
+                    format = "rgb";
+                }
+            }
+
+            var realColor = get({ format: format }),
+                displayColor = '';
+
+             //reset background info for preview element
+            previewElement.removeClass("sp-clear-display");
+            previewElement.css('background-color', 'transparent');
+
+            if (!realColor && allowEmpty) {
+                // Update the replaced elements background with icon indicating no color selection
+                previewElement.addClass("sp-clear-display");
+            }
+            else {
+                var realHex = realColor.toHexString(),
+                    realRgb = realColor.toRgbString();
+
+                // Update the replaced elements background color (with actual selected color)
+                if (rgbaSupport || realColor.alpha === 1) {
+                    previewElement.css("background-color", realRgb);
+                }
+                else {
+                    previewElement.css("background-color", "transparent");
+                    previewElement.css("filter", realColor.toFilter());
+                }
+
+                if (opts.showAlpha) {
+                    var rgb = realColor.toRgb();
+                    rgb.a = 0;
+                    var realAlpha = tinycolor(rgb).toRgbString();
+                    var gradient = "linear-gradient(left, " + realAlpha + ", " + realHex + ")";
+
+                    if (IE) {
+                        alphaSliderInner.css("filter", tinycolor(realAlpha).toFilter({ gradientType: 1 }, realHex));
+                    }
+                    else {
+                        alphaSliderInner.css("background", "-webkit-" + gradient);
+                        alphaSliderInner.css("background", "-moz-" + gradient);
+                        alphaSliderInner.css("background", "-ms-" + gradient);
+                        // Use current syntax gradient on unprefixed property.
+                        alphaSliderInner.css("background",
+                            "linear-gradient(to right, " + realAlpha + ", " + realHex + ")");
+                    }
+                }
+
+                displayColor = realColor.toString(format);
+            }
+
+            // Update the text entry input as it changes happen
+            if (opts.showInput) {
+                textInput.val(displayColor);
+            }
+
+            if (opts.showPalette) {
+                drawPalette();
+            }
+
+            drawInitial();
+        }
+
+        function updateHelperLocations() {
+            var s = currentSaturation;
+            var v = currentValue;
+
+            if(allowEmpty && isEmpty) {
+                //if selected color is empty, hide the helpers
+                alphaSlideHelper.hide();
+                slideHelper.hide();
+                dragHelper.hide();
+            }
+            else {
+                //make sure helpers are visible
+                alphaSlideHelper.show();
+                slideHelper.show();
+                dragHelper.show();
+
+                // Where to show the little circle in that displays your current selected color
+                var dragX = s * dragWidth;
+                var dragY = dragHeight - (v * dragHeight);
+                dragX = Math.max(
+                    -dragHelperHeight,
+                    Math.min(dragWidth - dragHelperHeight, dragX - dragHelperHeight)
+                );
+                dragY = Math.max(
+                    -dragHelperHeight,
+                    Math.min(dragHeight - dragHelperHeight, dragY - dragHelperHeight)
+                );
+                dragHelper.css({
+                    "top": dragY + "px",
+                    "left": dragX + "px"
+                });
+
+                var alphaX = currentAlpha * alphaWidth;
+                alphaSlideHelper.css({
+                    "left": (alphaX - (alphaSlideHelperWidth / 2)) + "px"
+                });
+
+                // Where to show the bar that displays your current selected hue
+                var slideY = (currentHue) * slideHeight;
+                slideHelper.css({
+                    "top": (slideY - slideHelperHeight) + "px"
+                });
+            }
+        }
+
+        function updateOriginalInput(fireCallback) {
+            var color = get(),
+                displayColor = '',
+                hasChanged = !tinycolor.equals(color, colorOnShow);
+
+            if (color) {
+                displayColor = color.toString(currentPreferredFormat);
+                // Update the selection palette with the current color
+                addColorToSelectionPalette(color);
+            }
+
+            if (isInput) {
+                boundElement.val(displayColor);
+            }
+
+            if (fireCallback && hasChanged) {
+                callbacks.change(color);
+                boundElement.trigger('change', [ color ]);
+            }
+        }
+
+        function reflow() {
+            dragWidth = dragger.width();
+            dragHeight = dragger.height();
+            dragHelperHeight = dragHelper.height();
+            slideWidth = slider.width();
+            slideHeight = slider.height();
+            slideHelperHeight = slideHelper.height();
+            alphaWidth = alphaSlider.width();
+            alphaSlideHelperWidth = alphaSlideHelper.width();
+
+            if (!flat) {
+                container.css("position", "absolute");
+                if (opts.offset) {
+                    container.offset(opts.offset);
+                } else {
+                    container.offset(getOffset(container, offsetElement));
+                }
+            }
+
+            updateHelperLocations();
+
+            if (opts.showPalette) {
+                drawPalette();
+            }
+
+            boundElement.trigger('reflow.spectrum');
+        }
+
+        function destroy() {
+            boundElement.show();
+            offsetElement.unbind("click.spectrum touchstart.spectrum");
+            container.remove();
+            replacer.remove();
+            spectrums[spect.id] = null;
+        }
+
+        function option(optionName, optionValue) {
+            if (optionName === undefined) {
+                return $.extend({}, opts);
+            }
+            if (optionValue === undefined) {
+                return opts[optionName];
+            }
+
+            opts[optionName] = optionValue;
+            applyOptions();
+        }
+
+        function enable() {
+            disabled = false;
+            boundElement.attr("disabled", false);
+            offsetElement.removeClass("sp-disabled");
+        }
+
+        function disable() {
+            hide();
+            disabled = true;
+            boundElement.attr("disabled", true);
+            offsetElement.addClass("sp-disabled");
+        }
+
+        function setOffset(coord) {
+            opts.offset = coord;
+            reflow();
+        }
+
+        initialize();
+
+        var spect = {
+            show: show,
+            hide: hide,
+            toggle: toggle,
+            reflow: reflow,
+            option: option,
+            enable: enable,
+            disable: disable,
+            offset: setOffset,
+            set: function (c) {
+                set(c);
+                updateOriginalInput();
+            },
+            get: get,
+            destroy: destroy,
+            container: container
+        };
+
+        spect.id = spectrums.push(spect) - 1;
+
+        return spect;
+    }
+
+    /**
+    * checkOffset - get the offset below/above and left/right element depending on screen position
+    * Thanks https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js
+    */
+    function getOffset(picker, input) {
+        var extraY = 0;
+        var dpWidth = picker.outerWidth();
+        var dpHeight = picker.outerHeight();
+        var inputHeight = input.outerHeight();
+        var doc = picker[0].ownerDocument;
+        var docElem = doc.documentElement;
+        var viewWidth = docElem.clientWidth + $(doc).scrollLeft();
+        var viewHeight = docElem.clientHeight + $(doc).scrollTop();
+        var offset = input.offset();
+        offset.top += inputHeight;
+
+        offset.left -=
+            Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?
+            Math.abs(offset.left + dpWidth - viewWidth) : 0);
+
+        offset.top -=
+            Math.min(offset.top, ((offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
+            Math.abs(dpHeight + inputHeight - extraY) : extraY));
+
+        return offset;
+    }
+
+    /**
+    * noop - do nothing
+    */
+    function noop() {
+
+    }
+
+    /**
+    * stopPropagation - makes the code only doing this a little easier to read in line
+    */
+    function stopPropagation(e) {
+        e.stopPropagation();
+    }
+
+    /**
+    * Create a function bound to a given object
+    * Thanks to underscore.js
+    */
+    function bind(func, obj) {
+        var slice = Array.prototype.slice;
+        var args = slice.call(arguments, 2);
+        return function () {
+            return func.apply(obj, args.concat(slice.call(arguments)));
+        };
+    }
+
+    /**
+    * Lightweight drag helper.  Handles containment within the element, so that
+    * when dragging, the x is within [0,element.width] and y is within [0,element.height]
+    */
+    function draggable(element, onmove, onstart, onstop) {
+        onmove = onmove || function () { };
+        onstart = onstart || function () { };
+        onstop = onstop || function () { };
+        var doc = document;
+        var dragging = false;
+        var offset = {};
+        var maxHeight = 0;
+        var maxWidth = 0;
+        var hasTouch = ('ontouchstart' in window);
+
+        var duringDragEvents = {};
+        duringDragEvents["selectstart"] = prevent;
+        duringDragEvents["dragstart"] = prevent;
+        duringDragEvents["touchmove mousemove"] = move;
+        duringDragEvents["touchend mouseup"] = stop;
+
+        function prevent(e) {
+            if (e.stopPropagation) {
+                e.stopPropagation();
+            }
+            if (e.preventDefault) {
+                e.preventDefault();
+            }
+            e.returnValue = false;
+        }
+
+        function move(e) {
+            if (dragging) {
+                // Mouseup happened outside of window
+                if (IE && doc.documentMode < 9 && !e.button) {
+                    return stop();
+                }
+
+                var t0 = e.originalEvent && e.originalEvent.touches && e.originalEvent.touches[0];
+                var pageX = t0 && t0.pageX || e.pageX;
+                var pageY = t0 && t0.pageY || e.pageY;
+
+                var dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
+                var dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
+
+                if (hasTouch) {
+                    // Stop scrolling in iOS
+                    prevent(e);
+                }
+
+                onmove.apply(element, [dragX, dragY, e]);
+            }
+        }
+
+        function start(e) {
+            var rightclick = (e.which) ? (e.which == 3) : (e.button == 2);
+
+            if (!rightclick && !dragging) {
+                if (onstart.apply(element, arguments) !== false) {
+                    dragging = true;
+                    maxHeight = $(element).height();
+                    maxWidth = $(element).width();
+                    offset = $(element).offset();
+
+                    $(doc).bind(duringDragEvents);
+                    $(doc.body).addClass("sp-dragging");
+
+                    move(e);
+
+                    prevent(e);
+                }
+            }
+        }
+
+        function stop() {
+            if (dragging) {
+                $(doc).unbind(duringDragEvents);
+                $(doc.body).removeClass("sp-dragging");
+
+                // Wait a tick before notifying observers to allow the click event
+                // to fire in Chrome.
+                setTimeout(function() {
+                    onstop.apply(element, arguments);
+                }, 0);
+            }
+            dragging = false;
+        }
+
+        $(element).bind("touchstart mousedown", start);
+    }
+
+    function throttle(func, wait, debounce) {
+        var timeout;
+        return function () {
+            var context = this, args = arguments;
+            var throttler = function () {
+                timeout = null;
+                func.apply(context, args);
+            };
+            if (debounce) clearTimeout(timeout);
+            if (debounce || !timeout) timeout = setTimeout(throttler, wait);
+        };
+    }
+
+    function inputTypeColorSupport() {
+        return $.fn.spectrum.inputTypeColorSupport();
+    }
+
+    /**
+    * Define a jQuery plugin
+    */
+    var dataID = "spectrum.id";
+    $.fn.spectrum = function (opts, extra) {
+
+        if (typeof opts == "string") {
+
+            var returnValue = this;
+            var args = Array.prototype.slice.call( arguments, 1 );
+
+            this.each(function () {
+                var spect = spectrums[$(this).data(dataID)];
+                if (spect) {
+                    var method = spect[opts];
+                    if (!method) {
+                        throw new Error( "Spectrum: no such method: '" + opts + "'" );
+                    }
+
+                    if (opts == "get") {
+                        returnValue = spect.get();
+                    }
+                    else if (opts == "container") {
+                        returnValue = spect.container;
+                    }
+                    else if (opts == "option") {
+                        returnValue = spect.option.apply(spect, args);
+                    }
+                    else if (opts == "destroy") {
+                        spect.destroy();
+                        $(this).removeData(dataID);
+                    }
+                    else {
+                        method.apply(spect, args);
+                    }
+                }
+            });
+
+            return returnValue;
+        }
+
+        // Initializing a new instance of spectrum
+        return this.spectrum("destroy").each(function () {
+            var options = $.extend({}, opts, $(this).data());
+            var spect = spectrum(this, options);
+            $(this).data(dataID, spect.id);
+        });
+    };
+
+    $.fn.spectrum.load = true;
+    $.fn.spectrum.loadOpts = {};
+    $.fn.spectrum.draggable = draggable;
+    $.fn.spectrum.defaults = defaultOpts;
+    $.fn.spectrum.inputTypeColorSupport = function inputTypeColorSupport() {
+        if (typeof inputTypeColorSupport._cachedResult === "undefined") {
+            var colorInput = $("<input type='color'/>")[0]; // if color element is supported, value will default to not null
+            inputTypeColorSupport._cachedResult = colorInput.type === "color" && colorInput.value !== "";
+        }
+        return inputTypeColorSupport._cachedResult;
+    };
+
+    $.spectrum = { };
+    $.spectrum.localization = { };
+    $.spectrum.palettes = { };
+
+    $.fn.spectrum.processNativeColorInputs = function () {
+        var colorInputs = $("input[type=color]");
+        if (colorInputs.length && !inputTypeColorSupport()) {
+            colorInputs.spectrum({
+                preferredFormat: "hex6"
+            });
+        }
+    };
+
+    // TinyColor v1.1.2
+    // https://github.com/bgrins/TinyColor
+    // Brian Grinstead, MIT License
+
+    (function() {
+
+    var trimLeft = /^[\s,#]+/,
+        trimRight = /\s+$/,
+        tinyCounter = 0,
+        math = Math,
+        mathRound = math.round,
+        mathMin = math.min,
+        mathMax = math.max,
+        mathRandom = math.random;
+
+    var tinycolor = function(color, opts) {
+
+        color = (color) ? color : '';
+        opts = opts || { };
+
+        // If input is already a tinycolor, return itself
+        if (color instanceof tinycolor) {
+           return color;
+        }
+        // If we are called as a function, call using new instead
+        if (!(this instanceof tinycolor)) {
+            return new tinycolor(color, opts);
+        }
+
+        var rgb = inputToRGB(color);
+        this._originalInput = color,
+        this._r = rgb.r,
+        this._g = rgb.g,
+        this._b = rgb.b,
+        this._a = rgb.a,
+        this._roundA = mathRound(100*this._a) / 100,
+        this._format = opts.format || rgb.format;
+        this._gradientType = opts.gradientType;
+
+        // Don't let the range of [0,255] come back in [0,1].
+        // Potentially lose a little bit of precision here, but will fix issues where
+        // .5 gets interpreted as half of the total, instead of half of 1
+        // If it was supposed to be 128, this was already taken care of by `inputToRgb`
+        if (this._r < 1) { this._r = mathRound(this._r); }
+        if (this._g < 1) { this._g = mathRound(this._g); }
+        if (this._b < 1) { this._b = mathRound(this._b); }
+
+        this._ok = rgb.ok;
+        this._tc_id = tinyCounter++;
+    };
+
+    tinycolor.prototype = {
+        isDark: function() {
+            return this.getBrightness() < 128;
+        },
+        isLight: function() {
+            return !this.isDark();
+        },
+        isValid: function() {
+            return this._ok;
+        },
+        getOriginalInput: function() {
+          return this._originalInput;
+        },
+        getFormat: function() {
+            return this._format;
+        },
+        getAlpha: function() {
+            return this._a;
+        },
+        getBrightness: function() {
+            var rgb = this.toRgb();
+            return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
+        },
+        setAlpha: function(value) {
+            this._a = boundAlpha(value);
+            this._roundA = mathRound(100*this._a) / 100;
+            return this;
+        },
+        toHsv: function() {
+            var hsv = rgbToHsv(this._r, this._g, this._b);
+            return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a };
+        },
+        toHsvString: function() {
+            var hsv = rgbToHsv(this._r, this._g, this._b);
+            var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
+            return (this._a == 1) ?
+              "hsv("  + h + ", " + s + "%, " + v + "%)" :
+              "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")";
+        },
+        toHsl: function() {
+            var hsl = rgbToHsl(this._r, this._g, this._b);
+            return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a };
+        },
+        toHslString: function() {
+            var hsl = rgbToHsl(this._r, this._g, this._b);
+            var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
+            return (this._a == 1) ?
+              "hsl("  + h + ", " + s + "%, " + l + "%)" :
+              "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")";
+        },
+        toHex: function(allow3Char) {
+            return rgbToHex(this._r, this._g, this._b, allow3Char);
+        },
+        toHexString: function(allow3Char) {
+            return '#' + this.toHex(allow3Char);
+        },
+        toHex8: function() {
+            return rgbaToHex(this._r, this._g, this._b, this._a);
+        },
+        toHex8String: function() {
+            return '#' + this.toHex8();
+        },
+        toRgb: function() {
+            return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a };
+        },
+        toRgbString: function() {
+            return (this._a == 1) ?
+              "rgb("  + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" :
+              "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")";
+        },
+        toPercentageRgb: function() {
+            return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a };
+        },
+        toPercentageRgbString: function() {
+            return (this._a == 1) ?
+              "rgb("  + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" :
+              "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")";
+        },
+        toName: function() {
+            if (this._a === 0) {
+                return "transparent";
+            }
+
+            if (this._a < 1) {
+                return false;
+            }
+
+            return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false;
+        },
+        toFilter: function(secondColor) {
+            var hex8String = '#' + rgbaToHex(this._r, this._g, this._b, this._a);
+            var secondHex8String = hex8String;
+            var gradientType = this._gradientType ? "GradientType = 1, " : "";
+
+            if (secondColor) {
+                var s = tinycolor(secondColor);
+                secondHex8String = s.toHex8String();
+            }
+
+            return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")";
+        },
+        toString: function(format) {
+            var formatSet = !!format;
+            format = format || this._format;
+
+            var formattedString = false;
+            var hasAlpha = this._a < 1 && this._a >= 0;
+            var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "name");
+
+            if (needsAlphaFormat) {
+                // Special case for "transparent", all other non-alpha formats
+                // will return rgba when there is transparency.
+                if (format === "name" && this._a === 0) {
+                    return this.toName();
+                }
+                return this.toRgbString();
+            }
+            if (format === "rgb") {
+                formattedString = this.toRgbString();
+            }
+            if (format === "prgb") {
+                formattedString = this.toPercentageRgbString();
+            }
+            if (format === "hex" || format === "hex6") {
+                formattedString = this.toHexString();
+            }
+            if (format === "hex3") {
+                formattedString = this.toHexString(true);
+            }
+            if (format === "hex8") {
+                formattedString = this.toHex8String();
+            }
+            if (format === "name") {
+                formattedString = this.toName();
+            }
+            if (format === "hsl") {
+                formattedString = this.toHslString();
+            }
+            if (format === "hsv") {
+                formattedString = this.toHsvString();
+            }
+
+            return formattedString || this.toHexString();
+        },
+
+        _applyModification: function(fn, args) {
+            var color = fn.apply(null, [this].concat([].slice.call(args)));
+            this._r = color._r;
+            this._g = color._g;
+            this._b = color._b;
+            this.setAlpha(color._a);
+            return this;
+        },
+        lighten: function() {
+            return this._applyModification(lighten, arguments);
+        },
+        brighten: function() {
+            return this._applyModification(brighten, arguments);
+        },
+        darken: function() {
+            return this._applyModification(darken, arguments);
+        },
+        desaturate: function() {
+            return this._applyModification(desaturate, arguments);
+        },
+        saturate: function() {
+            return this._applyModification(saturate, arguments);
+        },
+        greyscale: function() {
+            return this._applyModification(greyscale, arguments);
+        },
+        spin: function() {
+            return this._applyModification(spin, arguments);
+        },
+
+        _applyCombination: function(fn, args) {
+            return fn.apply(null, [this].concat([].slice.call(args)));
+        },
+        analogous: function() {
+            return this._applyCombination(analogous, arguments);
+        },
+        complement: function() {
+            return this._applyCombination(complement, arguments);
+        },
+        monochromatic: function() {
+            return this._applyCombination(monochromatic, arguments);
+        },
+        splitcomplement: function() {
+            return this._applyCombination(splitcomplement, arguments);
+        },
+        triad: function() {
+            return this._applyCombination(triad, arguments);
+        },
+        tetrad: function() {
+            return this._applyCombination(tetrad, arguments);
+        }
+    };
+
+    // If input is an object, force 1 into "1.0" to handle ratios properly
+    // String input requires "1.0" as input, so 1 will be treated as 1
+    tinycolor.fromRatio = function(color, opts) {
+        if (typeof color == "object") {
+            var newColor = {};
+            for (var i in color) {
+                if (color.hasOwnProperty(i)) {
+                    if (i === "a") {
+                        newColor[i] = color[i];
+                    }
+                    else {
+                        newColor[i] = convertToPercentage(color[i]);
+                    }
+                }
+            }
+            color = newColor;
+        }
+
+        return tinycolor(color, opts);
+    };
+
+    // Given a string or object, convert that input to RGB
+    // Possible string inputs:
+    //
+    //     "red"
+    //     "#f00" or "f00"
+    //     "#ff0000" or "ff0000"
+    //     "#ff000000" or "ff000000"
+    //     "rgb 255 0 0" or "rgb (255, 0, 0)"
+    //     "rgb 1.0 0 0" or "rgb (1, 0, 0)"
+    //     "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
+    //     "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
+    //     "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
+    //     "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
+    //     "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
+    //
+    function inputToRGB(color) {
+
+        var rgb = { r: 0, g: 0, b: 0 };
+        var a = 1;
+        var ok = false;
+        var format = false;
+
+        if (typeof color == "string") {
+            color = stringInputToObject(color);
+        }
+
+        if (typeof color == "object") {
+            if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
+                rgb = rgbToRgb(color.r, color.g, color.b);
+                ok = true;
+                format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
+            }
+            else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
+                color.s = convertToPercentage(color.s);
+                color.v = convertToPercentage(color.v);
+                rgb = hsvToRgb(color.h, color.s, color.v);
+                ok = true;
+                format = "hsv";
+            }
+            else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
+                color.s = convertToPercentage(color.s);
+                color.l = convertToPercentage(color.l);
+                rgb = hslToRgb(color.h, color.s, color.l);
+                ok = true;
+                format = "hsl";
+            }
+
+            if (color.hasOwnProperty("a")) {
+                a = color.a;
+            }
+        }
+
+        a = boundAlpha(a);
+
+        return {
+            ok: ok,
+            format: color.format || format,
+            r: mathMin(255, mathMax(rgb.r, 0)),
+            g: mathMin(255, mathMax(rgb.g, 0)),
+            b: mathMin(255, mathMax(rgb.b, 0)),
+            a: a
+        };
+    }
+
+
+    // Conversion Functions
+    // --------------------
+
+    // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
+    // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
+
+    // `rgbToRgb`
+    // Handle bounds / percentage checking to conform to CSS color spec
+    // <http://www.w3.org/TR/css3-color/>
+    // *Assumes:* r, g, b in [0, 255] or [0, 1]
+    // *Returns:* { r, g, b } in [0, 255]
+    function rgbToRgb(r, g, b){
+        return {
+            r: bound01(r, 255) * 255,
+            g: bound01(g, 255) * 255,
+            b: bound01(b, 255) * 255
+        };
+    }
+
+    // `rgbToHsl`
+    // Converts an RGB color value to HSL.
+    // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
+    // *Returns:* { h, s, l } in [0,1]
+    function rgbToHsl(r, g, b) {
+
+        r = bound01(r, 255);
+        g = bound01(g, 255);
+        b = bound01(b, 255);
+
+        var max = mathMax(r, g, b), min = mathMin(r, g, b);
+        var h, s, l = (max + min) / 2;
+
+        if(max == min) {
+            h = s = 0; // achromatic
+        }
+        else {
+            var d = max - min;
+            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+            switch(max) {
+                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+                case g: h = (b - r) / d + 2; break;
+                case b: h = (r - g) / d + 4; break;
+            }
+
+            h /= 6;
+        }
+
+        return { h: h, s: s, l: l };
+    }
+
+    // `hslToRgb`
+    // Converts an HSL color value to RGB.
+    // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
+    // *Returns:* { r, g, b } in the set [0, 255]
+    function hslToRgb(h, s, l) {
+        var r, g, b;
+
+        h = bound01(h, 360);
+        s = bound01(s, 100);
+        l = bound01(l, 100);
+
+        function hue2rgb(p, q, t) {
+            if(t < 0) t += 1;
+            if(t > 1) t -= 1;
+            if(t < 1/6) return p + (q - p) * 6 * t;
+            if(t < 1/2) return q;
+            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+            return p;
+        }
+
+        if(s === 0) {
+            r = g = b = l; // achromatic
+        }
+        else {
+            var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+            var p = 2 * l - q;
+            r = hue2rgb(p, q, h + 1/3);
+            g = hue2rgb(p, q, h);
+            b = hue2rgb(p, q, h - 1/3);
+        }
+
+        return { r: r * 255, g: g * 255, b: b * 255 };
+    }
+
+    // `rgbToHsv`
+    // Converts an RGB color value to HSV
+    // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
+    // *Returns:* { h, s, v } in [0,1]
+    function rgbToHsv(r, g, b) {
+
+        r = bound01(r, 255);
+        g = bound01(g, 255);
+        b = bound01(b, 255);
+
+        var max = mathMax(r, g, b), min = mathMin(r, g, b);
+        var h, s, v = max;
+
+        var d = max - min;
+        s = max === 0 ? 0 : d / max;
+
+        if(max == min) {
+            h = 0; // achromatic
+        }
+        else {
+            switch(max) {
+                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+                case g: h = (b - r) / d + 2; break;
+                case b: h = (r - g) / d + 4; break;
+            }
+            h /= 6;
+        }
+        return { h: h, s: s, v: v };
+    }
+
+    // `hsvToRgb`
+    // Converts an HSV color value to RGB.
+    // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
+    // *Returns:* { r, g, b } in the set [0, 255]
+     function hsvToRgb(h, s, v) {
+
+        h = bound01(h, 360) * 6;
+        s = bound01(s, 100);
+        v = bound01(v, 100);
+
+        var i = math.floor(h),
+            f = h - i,
+            p = v * (1 - s),
+            q = v * (1 - f * s),
+            t = v * (1 - (1 - f) * s),
+            mod = i % 6,
+            r = [v, q, p, p, t, v][mod],
+            g = [t, v, v, q, p, p][mod],
+            b = [p, p, t, v, v, q][mod];
+
+        return { r: r * 255, g: g * 255, b: b * 255 };
+    }
+
+    // `rgbToHex`
+    // Converts an RGB color to hex
+    // Assumes r, g, and b are contained in the set [0, 255]
+    // Returns a 3 or 6 character hex
+    function rgbToHex(r, g, b, allow3Char) {
+
+        var hex = [
+            pad2(mathRound(r).toString(16)),
+            pad2(mathRound(g).toString(16)),
+            pad2(mathRound(b).toString(16))
+        ];
+
+        // Return a 3 character hex if possible
+        if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
+            return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
+        }
+
+        return hex.join("");
+    }
+        // `rgbaToHex`
+        // Converts an RGBA color plus alpha transparency to hex
+        // Assumes r, g, b and a are contained in the set [0, 255]
+        // Returns an 8 character hex
+        function rgbaToHex(r, g, b, a) {
+
+            var hex = [
+                pad2(convertDecimalToHex(a)),
+                pad2(mathRound(r).toString(16)),
+                pad2(mathRound(g).toString(16)),
+                pad2(mathRound(b).toString(16))
+            ];
+
+            return hex.join("");
+        }
+
+    // `equals`
+    // Can be called with any tinycolor input
+    tinycolor.equals = function (color1, color2) {
+        if (!color1 || !color2) { return false; }
+        return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
+    };
+    tinycolor.random = function() {
+        return tinycolor.fromRatio({
+            r: mathRandom(),
+            g: mathRandom(),
+            b: mathRandom()
+        });
+    };
+
+
+    // Modification Functions
+    // ----------------------
+    // Thanks to less.js for some of the basics here
+    // <https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js>
+
+    function desaturate(color, amount) {
+        amount = (amount === 0) ? 0 : (amount || 10);
+        var hsl = tinycolor(color).toHsl();
+        hsl.s -= amount / 100;
+        hsl.s = clamp01(hsl.s);
+        return tinycolor(hsl);
+    }
+
+    function saturate(color, amount) {
+        amount = (amount === 0) ? 0 : (amount || 10);
+        var hsl = tinycolor(color).toHsl();
+        hsl.s += amount / 100;
+        hsl.s = clamp01(hsl.s);
+        return tinycolor(hsl);
+    }
+
+    function greyscale(color) {
+        return tinycolor(color).desaturate(100);
+    }
+
+    function lighten (color, amount) {
+        amount = (amount === 0) ? 0 : (amount || 10);
+        var hsl = tinycolor(color).toHsl();
+        hsl.l += amount / 100;
+        hsl.l = clamp01(hsl.l);
+        return tinycolor(hsl);
+    }
+
+    function brighten(color, amount) {
+        amount = (amount === 0) ? 0 : (amount || 10);
+        var rgb = tinycolor(color).toRgb();
+        rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100))));
+        rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100))));
+        rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100))));
+        return tinycolor(rgb);
+    }
+
+    function darken (color, amount) {
+        amount = (amount === 0) ? 0 : (amount || 10);
+        var hsl = tinycolor(color).toHsl();
+        hsl.l -= amount / 100;
+        hsl.l = clamp01(hsl.l);
+        return tinycolor(hsl);
+    }
+
+    // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
+    // Values outside of this range will be wrapped into this range.
+    function spin(color, amount) {
+        var hsl = tinycolor(color).toHsl();
+        var hue = (mathRound(hsl.h) + amount) % 360;
+        hsl.h = hue < 0 ? 360 + hue : hue;
+        return tinycolor(hsl);
+    }
+
+    // Combination Functions
+    // ---------------------
+    // Thanks to jQuery xColor for some of the ideas behind these
+    // <https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js>
+
+    function complement(color) {
+        var hsl = tinycolor(color).toHsl();
+        hsl.h = (hsl.h + 180) % 360;
+        return tinycolor(hsl);
+    }
+
+    function triad(color) {
+        var hsl = tinycolor(color).toHsl();
+        var h = hsl.h;
+        return [
+            tinycolor(color),
+            tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
+            tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
+        ];
+    }
+
+    function tetrad(color) {
+        var hsl = tinycolor(color).toHsl();
+        var h = hsl.h;
+        return [
+            tinycolor(color),
+            tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
+            tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
+            tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
+        ];
+    }
+
+    function splitcomplement(color) {
+        var hsl = tinycolor(color).toHsl();
+        var h = hsl.h;
+        return [
+            tinycolor(color),
+            tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
+            tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
+        ];
+    }
+
+    function analogous(color, results, slices) {
+        results = results || 6;
+        slices = slices || 30;
+
+        var hsl = tinycolor(color).toHsl();
+        var part = 360 / slices;
+        var ret = [tinycolor(color)];
+
+        for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
+            hsl.h = (hsl.h + part) % 360;
+            ret.push(tinycolor(hsl));
+        }
+        return ret;
+    }
+
+    function monochromatic(color, results) {
+        results = results || 6;
+        var hsv = tinycolor(color).toHsv();
+        var h = hsv.h, s = hsv.s, v = hsv.v;
+        var ret = [];
+        var modification = 1 / results;
+
+        while (results--) {
+            ret.push(tinycolor({ h: h, s: s, v: v}));
+            v = (v + modification) % 1;
+        }
+
+        return ret;
+    }
+
+    // Utility Functions
+    // ---------------------
+
+    tinycolor.mix = function(color1, color2, amount) {
+        amount = (amount === 0) ? 0 : (amount || 50);
+
+        var rgb1 = tinycolor(color1).toRgb();
+        var rgb2 = tinycolor(color2).toRgb();
+
+        var p = amount / 100;
+        var w = p * 2 - 1;
+        var a = rgb2.a - rgb1.a;
+
+        var w1;
+
+        if (w * a == -1) {
+            w1 = w;
+        } else {
+            w1 = (w + a) / (1 + w * a);
+        }
+
+        w1 = (w1 + 1) / 2;
+
+        var w2 = 1 - w1;
+
+        var rgba = {
+            r: rgb2.r * w1 + rgb1.r * w2,
+            g: rgb2.g * w1 + rgb1.g * w2,
+            b: rgb2.b * w1 + rgb1.b * w2,
+            a: rgb2.a * p  + rgb1.a * (1 - p)
+        };
+
+        return tinycolor(rgba);
+    };
+
+
+    // Readability Functions
+    // ---------------------
+    // <http://www.w3.org/TR/AERT#color-contrast>
+
+    // `readability`
+    // Analyze the 2 colors and returns an object with the following properties:
+    //    `brightness`: difference in brightness between the two colors
+    //    `color`: difference in color/hue between the two colors
+    tinycolor.readability = function(color1, color2) {
+        var c1 = tinycolor(color1);
+        var c2 = tinycolor(color2);
+        var rgb1 = c1.toRgb();
+        var rgb2 = c2.toRgb();
+        var brightnessA = c1.getBrightness();
+        var brightnessB = c2.getBrightness();
+        var colorDiff = (
+            Math.max(rgb1.r, rgb2.r) - Math.min(rgb1.r, rgb2.r) +
+            Math.max(rgb1.g, rgb2.g) - Math.min(rgb1.g, rgb2.g) +
+            Math.max(rgb1.b, rgb2.b) - Math.min(rgb1.b, rgb2.b)
+        );
+
+        return {
+            brightness: Math.abs(brightnessA - brightnessB),
+            color: colorDiff
+        };
+    };
+
+    // `readable`
+    // http://www.w3.org/TR/AERT#color-contrast
+    // Ensure that foreground and background color combinations provide sufficient contrast.
+    // *Example*
+    //    tinycolor.isReadable("#000", "#111") => false
+    tinycolor.isReadable = function(color1, color2) {
+        var readability = tinycolor.readability(color1, color2);
+        return readability.brightness > 125 && readability.color > 500;
+    };
+
+    // `mostReadable`
+    // Given a base color and a list of possible foreground or background
+    // colors for that base, returns the most readable color.
+    // *Example*
+    //    tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
+    tinycolor.mostReadable = function(baseColor, colorList) {
+        var bestColor = null;
+        var bestScore = 0;
+        var bestIsReadable = false;
+        for (var i=0; i < colorList.length; i++) {
+
+            // We normalize both around the "acceptable" breaking point,
+            // but rank brightness constrast higher than hue.
+
+            var readability = tinycolor.readability(baseColor, colorList[i]);
+            var readable = readability.brightness > 125 && readability.color > 500;
+            var score = 3 * (readability.brightness / 125) + (readability.color / 500);
+
+            if ((readable && ! bestIsReadable) ||
+                (readable && bestIsReadable && score > bestScore) ||
+                ((! readable) && (! bestIsReadable) && score > bestScore)) {
+                bestIsReadable = readable;
+                bestScore = score;
+                bestColor = tinycolor(colorList[i]);
+            }
+        }
+        return bestColor;
+    };
+
+
+    // Big List of Colors
+    // ------------------
+    // <http://www.w3.org/TR/css3-color/#svg-color>
+    var names = tinycolor.names = {
+        aliceblue: "f0f8ff",
+        antiquewhite: "faebd7",
+        aqua: "0ff",
+        aquamarine: "7fffd4",
+        azure: "f0ffff",
+        beige: "f5f5dc",
+        bisque: "ffe4c4",
+        black: "000",
+        blanchedalmond: "ffebcd",
+        blue: "00f",
+        blueviolet: "8a2be2",
+        brown: "a52a2a",
+        burlywood: "deb887",
+        burntsienna: "ea7e5d",
+        cadetblue: "5f9ea0",
+        chartreuse: "7fff00",
+        chocolate: "d2691e",
+        coral: "ff7f50",
+        cornflowerblue: "6495ed",
+        cornsilk: "fff8dc",
+        crimson: "dc143c",
+        cyan: "0ff",
+        darkblue: "00008b",
+        darkcyan: "008b8b",
+        darkgoldenrod: "b8860b",
+        darkgray: "a9a9a9",
+        darkgreen: "006400",
+        darkgrey: "a9a9a9",
+        darkkhaki: "bdb76b",
+        darkmagenta: "8b008b",
+        darkolivegreen: "556b2f",
+        darkorange: "ff8c00",
+        darkorchid: "9932cc",
+        darkred: "8b0000",
+        darksalmon: "e9967a",
+        darkseagreen: "8fbc8f",
+        darkslateblue: "483d8b",
+        darkslategray: "2f4f4f",
+        darkslategrey: "2f4f4f",
+        darkturquoise: "00ced1",
+        darkviolet: "9400d3",
+        deeppink: "ff1493",
+        deepskyblue: "00bfff",
+        dimgray: "696969",
+        dimgrey: "696969",
+        dodgerblue: "1e90ff",
+        firebrick: "b22222",
+        floralwhite: "fffaf0",
+        forestgreen: "228b22",
+        fuchsia: "f0f",
+        gainsboro: "dcdcdc",
+        ghostwhite: "f8f8ff",
+        gold: "ffd700",
+        goldenrod: "daa520",
+        gray: "808080",
+        green: "008000",
+        greenyellow: "adff2f",
+        grey: "808080",
+        honeydew: "f0fff0",
+        hotpink: "ff69b4",
+        indianred: "cd5c5c",
+        indigo: "4b0082",
+        ivory: "fffff0",
+        khaki: "f0e68c",
+        lavender: "e6e6fa",
+        lavenderblush: "fff0f5",
+        lawngreen: "7cfc00",
+        lemonchiffon: "fffacd",
+        lightblue: "add8e6",
+        lightcoral: "f08080",
+        lightcyan: "e0ffff",
+        lightgoldenrodyellow: "fafad2",
+        lightgray: "d3d3d3",
+        lightgreen: "90ee90",
+        lightgrey: "d3d3d3",
+        lightpink: "ffb6c1",
+        lightsalmon: "ffa07a",
+        lightseagreen: "20b2aa",
+        lightskyblue: "87cefa",
+        lightslategray: "789",
+        lightslategrey: "789",
+        lightsteelblue: "b0c4de",
+        lightyellow: "ffffe0",
+        lime: "0f0",
+        limegreen: "32cd32",
+        linen: "faf0e6",
+        magenta: "f0f",
+        maroon: "800000",
+        mediumaquamarine: "66cdaa",
+        mediumblue: "0000cd",
+        mediumorchid: "ba55d3",
+        mediumpurple: "9370db",
+        mediumseagreen: "3cb371",
+        mediumslateblue: "7b68ee",
+        mediumspringgreen: "00fa9a",
+        mediumturquoise: "48d1cc",
+        mediumvioletred: "c71585",
+        midnightblue: "191970",
+        mintcream: "f5fffa",
+        mistyrose: "ffe4e1",
+        moccasin: "ffe4b5",
+        navajowhite: "ffdead",
+        navy: "000080",
+        oldlace: "fdf5e6",
+        olive: "808000",
+        olivedrab: "6b8e23",
+        orange: "ffa500",
+        orangered: "ff4500",
+        orchid: "da70d6",
+        palegoldenrod: "eee8aa",
+        palegreen: "98fb98",
+        paleturquoise: "afeeee",
+        palevioletred: "db7093",
+        papayawhip: "ffefd5",
+        peachpuff: "ffdab9",
+        peru: "cd853f",
+        pink: "ffc0cb",
+        plum: "dda0dd",
+        powderblue: "b0e0e6",
+        purple: "800080",
+        rebeccapurple: "663399",
+        red: "f00",
+        rosybrown: "bc8f8f",
+        royalblue: "4169e1",
+        saddlebrown: "8b4513",
+        salmon: "fa8072",
+        sandybrown: "f4a460",
+        seagreen: "2e8b57",
+        seashell: "fff5ee",
+        sienna: "a0522d",
+        silver: "c0c0c0",
+        skyblue: "87ceeb",
+        slateblue: "6a5acd",
+        slategray: "708090",
+        slategrey: "708090",
+        snow: "fffafa",
+        springgreen: "00ff7f",
+        steelblue: "4682b4",
+        tan: "d2b48c",
+        teal: "008080",
+        thistle: "d8bfd8",
+        tomato: "ff6347",
+        turquoise: "40e0d0",
+        violet: "ee82ee",
+        wheat: "f5deb3",
+        white: "fff",
+        whitesmoke: "f5f5f5",
+        yellow: "ff0",
+        yellowgreen: "9acd32"
+    };
+
+    // Make it easy to access colors via `hexNames[hex]`
+    var hexNames = tinycolor.hexNames = flip(names);
+
+
+    // Utilities
+    // ---------
+
+    // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
+    function flip(o) {
+        var flipped = { };
+        for (var i in o) {
+            if (o.hasOwnProperty(i)) {
+                flipped[o[i]] = i;
+            }
+        }
+        return flipped;
+    }
+
+    // Return a valid alpha value [0,1] with all invalid values being set to 1
+    function boundAlpha(a) {
+        a = parseFloat(a);
+
+        if (isNaN(a) || a < 0 || a > 1) {
+            a = 1;
+        }
+
+        return a;
+    }
+
+    // Take input from [0, n] and return it as [0, 1]
+    function bound01(n, max) {
+        if (isOnePointZero(n)) { n = "100%"; }
+
+        var processPercent = isPercentage(n);
+        n = mathMin(max, mathMax(0, parseFloat(n)));
+
+        // Automatically convert percentage into number
+        if (processPercent) {
+            n = parseInt(n * max, 10) / 100;
+        }
+
+        // Handle floating point rounding errors
+        if ((math.abs(n - max) < 0.000001)) {
+            return 1;
+        }
+
+        // Convert into [0, 1] range if it isn't already
+        return (n % max) / parseFloat(max);
+    }
+
+    // Force a number between 0 and 1
+    function clamp01(val) {
+        return mathMin(1, mathMax(0, val));
+    }
+
+    // Parse a base-16 hex value into a base-10 integer
+    function parseIntFromHex(val) {
+        return parseInt(val, 16);
+    }
+
+    // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
+    // <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
+    function isOnePointZero(n) {
+        return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
+    }
+
+    // Check to see if string passed in is a percentage
+    function isPercentage(n) {
+        return typeof n === "string" && n.indexOf('%') != -1;
+    }
+
+    // Force a hex value to have 2 characters
+    function pad2(c) {
+        return c.length == 1 ? '0' + c : '' + c;
+    }
+
+    // Replace a decimal with it's percentage value
+    function convertToPercentage(n) {
+        if (n <= 1) {
+            n = (n * 100) + "%";
+        }
+
+        return n;
+    }
+
+    // Converts a decimal to a hex value
+    function convertDecimalToHex(d) {
+        return Math.round(parseFloat(d) * 255).toString(16);
+    }
+    // Converts a hex value to a decimal
+    function convertHexToDecimal(h) {
+        return (parseIntFromHex(h) / 255);
+    }
+
+    var matchers = (function() {
+
+        // <http://www.w3.org/TR/css3-values/#integers>
+        var CSS_INTEGER = "[-\\+]?\\d+%?";
+
+        // <http://www.w3.org/TR/css3-values/#number-value>
+        var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
+
+        // Allow positive/negative integer/number.  Don't capture the either/or, just the entire outcome.
+        var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
+
+        // Actual matching.
+        // Parentheses and commas are optional, but not required.
+        // Whitespace can take the place of commas or opening paren
+        var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+        var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+
+        return {
+            rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
+            rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
+            hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
+            hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
+            hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
+            hsva: new RegExp("hsva" + PERMISSIVE_MATCH4),
+            hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
+            hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
+            hex8: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
+        };
+    })();
+
+    // `stringInputToObject`
+    // Permissive string parsing.  Take in a number of formats, and output an object
+    // based on detected format.  Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
+    function stringInputToObject(color) {
+
+        color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
+        var named = false;
+        if (names[color]) {
+            color = names[color];
+            named = true;
+        }
+        else if (color == 'transparent') {
+            return { r: 0, g: 0, b: 0, a: 0, format: "name" };
+        }
+
+        // Try to match string input using regular expressions.
+        // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
+        // Just return an object and let the conversion functions handle that.
+        // This way the result will be the same whether the tinycolor is initialized with string or object.
+        var match;
+        if ((match = matchers.rgb.exec(color))) {
+            return { r: match[1], g: match[2], b: match[3] };
+        }
+        if ((match = matchers.rgba.exec(color))) {
+            return { r: match[1], g: match[2], b: match[3], a: match[4] };
+        }
+        if ((match = matchers.hsl.exec(color))) {
+            return { h: match[1], s: match[2], l: match[3] };
+        }
+        if ((match = matchers.hsla.exec(color))) {
+            return { h: match[1], s: match[2], l: match[3], a: match[4] };
+        }
+        if ((match = matchers.hsv.exec(color))) {
+            return { h: match[1], s: match[2], v: match[3] };
+        }
+        if ((match = matchers.hsva.exec(color))) {
+            return { h: match[1], s: match[2], v: match[3], a: match[4] };
+        }
+        if ((match = matchers.hex8.exec(color))) {
+            return {
+                a: convertHexToDecimal(match[1]),
+                r: parseIntFromHex(match[2]),
+                g: parseIntFromHex(match[3]),
+                b: parseIntFromHex(match[4]),
+                format: named ? "name" : "hex8"
+            };
+        }
+        if ((match = matchers.hex6.exec(color))) {
+            return {
+                r: parseIntFromHex(match[1]),
+                g: parseIntFromHex(match[2]),
+                b: parseIntFromHex(match[3]),
+                format: named ? "name" : "hex"
+            };
+        }
+        if ((match = matchers.hex3.exec(color))) {
+            return {
+                r: parseIntFromHex(match[1] + '' + match[1]),
+                g: parseIntFromHex(match[2] + '' + match[2]),
+                b: parseIntFromHex(match[3] + '' + match[3]),
+                format: named ? "name" : "hex"
+            };
+        }
+
+        return false;
+    }
+
+    window.tinycolor = tinycolor;
+    })();
+
+    $(function () {
+        if ($.fn.spectrum.load) {
+            $.fn.spectrum.processNativeColorInputs();
+        }
+    });
+
+});
diff --git a/scp/queues.php b/scp/queues.php
new file mode 100644
index 0000000000000000000000000000000000000000..d348d570503e54c51dd17358dc0421645ceb7e5c
--- /dev/null
+++ b/scp/queues.php
@@ -0,0 +1,26 @@
+<?php
+/*********************************************************************
+    queues.php
+
+    Handles management of custom queues
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2015 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+require('admin.inc.php');
+
+require_once INCLUDE_DIR . 'class.queue.php';
+
+$nav->setTabActive('settings', 'settings.php?t='.urlencode($_GET['t']));
+
+require_once(STAFFINC_DIR.'header.inc.php');
+include_once(STAFFINC_DIR."queue.inc.php");
+include_once(STAFFINC_DIR.'footer.inc.php');