diff --git a/l10n_community/editor.js b/l10n_community/editor.js
index 2467632..379935b 100644
--- a/l10n_community/editor.js
+++ b/l10n_community/editor.js
@@ -70,7 +70,7 @@
 
     // If we did hide items, add a link to show all.
     if ($('#l10n-community-filter-form .filter-widget:hidden')) {
-      $('#l10n-community-filter-form').append('<a href="#" class="reveal-link">' + Drupal.t('Reveal more filters') + '</a>');
+      //$('#l10n-community-filter-form').append('<a href="#" class="reveal-link">' + Drupal.t('Reveal more filters') + '</a>');
       $('#l10n-community-filter-form .reveal-link').click(showAllFilters);
     }
 
diff --git a/l10n_community/l10n_community.css b/l10n_community/l10n_community.css
index 4a1c2bb..695b1ee 100644
--- a/l10n_community/l10n_community.css
+++ b/l10n_community/l10n_community.css
@@ -32,7 +32,7 @@ table.l10n-community-overview {
 /* String search filtering form */
 .l10n-server-filter {
   background-color: #f6f6f2;
-  padding: 0.6em 150px 1.5em 0.6em; /* LTR */
+  padding: 0.6em;
   position: relative;
 }
 .l10n-server-filter .filter-widget {
diff --git a/l10n_community/l10n_community.module b/l10n_community/l10n_community.module
index e444e4a..9d0de3e 100644
--- a/l10n_community/l10n_community.module
+++ b/l10n_community/l10n_community.module
@@ -143,6 +143,13 @@ function l10n_community_menu() {
     'access arguments' => array('access localization community'),
     'type' => MENU_CALLBACK,
   );
+  // Autocomplete callback for releases
+  $items['translate/release-autocomplete'] = array(
+    'title' => 'Release autocomplete',
+    'page callback' => 'l10n_community_releases_autocomplete',
+    'access arguments' => array('access localization community'),
+    'type' => MENU_CALLBACK,
+  );
 
   // Callback to provide details about a certain source string.
   $items['translate/source-details/%'] = array(
@@ -672,6 +679,24 @@ function l10n_community_projects_autocomplete($string = '') {
 }
 
 /**
+ * Retrieve a pipe delimited string of autocomplete suggestions for projects.
+ */
+function l10n_community_releases_autocomplete($project_name = '') {
+  $matches = array();
+  if ($project_name) {
+    $projects = l10n_server_get_projects();
+    $selected_project = l10n_community_project_uri_by_title($project_name);
+    $uri = $projects[$selected_project]->uri;
+    $releases = l10n_server_get_releases($uri);
+    foreach ($releases as $rid => $release) {
+      $matches[$rid] = $release->title;
+    }
+  }
+  print drupal_json_encode($matches);
+  drupal_exit();
+}
+
+/**
  * Companion to autocomplete lookup to return uri by title.
  */
 function l10n_community_project_uri_by_title($title) {
diff --git a/l10n_community/l10n_visualsearch.js b/l10n_community/l10n_visualsearch.js
new file mode 100644
index 0000000..b88712a
--- /dev/null
+++ b/l10n_community/l10n_visualsearch.js
@@ -0,0 +1,89 @@
+(function ($) {
+  Drupal.settings.l10nVisualSearch = Drupal.settings.l10nVisualSearch || {};
+  Drupal.behaviors.l10nVisualSearch = {
+    attach: function (context) {
+
+      Drupal.l10nVisualSearch = VS.init({
+        container: $('.visual_search'),
+        query: Drupal.settings.l10nVisualSearch.query || '',
+        autosearch: false,
+        showFacets: true,
+        remainder: 'contains',
+        callbacks: {
+          search: function () {
+            var parameters = {};
+            _.each(Drupal.l10nVisualSearch.searchQuery.facets(), function (a) {
+              parameters[_.keys(a)[0]] = _.values(a)[0];
+            });
+            window.location.href = Drupal.settings.basePath + Drupal.settings.l10nVisualSearch.path + '?' + $.param(parameters);
+          },
+          facetMatches: function (callback) {
+            var facets = [
+              'project', 'context', 'translation status', 'suggestion status',
+              'author', 'contains', 'string id', 'limit'
+            ];
+            if (Drupal.l10nVisualSearch.searchQuery.has('project')) {
+              facets.splice(1, 0, 'release');
+            }
+            // Filter out already used facets
+            var already = _.map(Drupal.l10nVisualSearch.searchQuery.facets(), function (a) {
+              return _.keys(a)[0];
+            });
+            facets = _.filter(facets, function (value) {
+              return !_.contains(already, value);
+            });
+            callback(facets, {preserveOrder: true});
+          },
+          valueMatches: function (facet, searchTerm, callback) {
+            switch (facet) {
+              case 'project':
+                if (!searchTerm) return;
+                $.getJSON(Drupal.settings.basePath + 'translate/project-autocomplete/' + searchTerm, function (data) {
+                  callback(_.toArray(data));
+                });
+                break;
+              case 'release':
+                Drupal.settings.l10nVisualSearch.releases = Drupal.settings.l10nVisualSearch.releases || {};
+                var project = Drupal.l10nVisualSearch.searchQuery.find('project');
+                if (!Drupal.settings.l10nVisualSearch.releases[project]) {
+                  $.getJSON(Drupal.settings.basePath + 'translate/release-autocomplete/' + project, function (data) {
+                    var obj = _.map(data, function (v, k) {
+                      return {
+                        value: k,
+                        label: v
+                      };
+                    }).reverse();
+                    Drupal.settings.l10nVisualSearch.releases[project] = obj;
+                    callback(obj, {preserveOrder: true});
+                  });
+                }
+                else {
+                  callback(Drupal.settings.l10nVisualSearch.releases[project], {preserveOrder: true});
+                }
+                break;
+              case 'context':
+                callback(Drupal.settings.l10nVisualSearch.contexts);
+                break;
+              case 'translation status':
+                callback(Drupal.settings.l10nVisualSearch.translation_status);
+                break;
+              case 'suggestion status':
+                callback(Drupal.settings.l10nVisualSearch.suggestion_status);
+                break;
+              case 'author':
+                if (!searchTerm) return;
+                $.getJSON(Drupal.settings.basePath + 'user/autocomplete/' + searchTerm, function (data) {
+                  callback(_.toArray(data));
+                });
+                break;
+              case 'limit':
+                callback(Drupal.settings.l10nVisualSearch.limit);
+                break;
+            }
+          }
+        }
+      });
+    }
+  };
+
+})(jQuery);
diff --git a/l10n_community/translate.inc b/l10n_community/translate.inc
index 2c5febe..d3c71e8 100644
--- a/l10n_community/translate.inc
+++ b/l10n_community/translate.inc
@@ -52,6 +52,16 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
     '#prefix' => '<div class="l10n-server-filter clear-block">',
     '#suffix' => '</div>',
   );
+  $form['visualsearch']['#markup'] = '<div class="visual_search"></div>';
+
+  // VisualSearch
+  drupal_add_library('system', 'ui.autocomplete');
+  drupal_add_js(drupal_get_path('module', 'l10n_community') . '/visualsearch/vendor/underscore-1.1.5.js');
+  drupal_add_js(drupal_get_path('module', 'l10n_community') . '/visualsearch/vendor/backbone-0.5.0.js');
+  drupal_add_js(drupal_get_path('module', 'l10n_community') . '/visualsearch/build/visualsearch.js');
+  drupal_add_js(drupal_get_path('module', 'l10n_community') . '/l10n_visualsearch.js');
+  drupal_add_css(drupal_get_path('module', 'l10n_community') . '/visualsearch/build/visualsearch-datauri.css');
+
   $class_is_default_project = empty($form_state['values']['project']) ? 'is-default' : 'is-not-default';
   $form['project'] = array(
     '#title' => t('Project'),
@@ -64,7 +74,9 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
     ),
     '#attributes' => array('class' => array($class_is_default_project)),
     '#theme_wrappers' => array('form_element', 'l10n_community_filter_form_element'),
+    '#access' => FALSE,
   );
+  // @TODO also cache on JS side if < 30 projects
   if (($count = count($projects)) <= 30) {
     // Select widget for 1-30 projects.
     $form['project']['#type'] = 'select';
@@ -80,6 +92,7 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
       '#type' => 'textfield',
       '#autocomplete_path' => 'translate/project-autocomplete',
       '#size' => 20,
+      '#access' => FALSE,
     );
   }
 
@@ -107,6 +120,7 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
     '#suffix' => '</div>',
     '#attributes' => array('class' => array($class_is_default_release)),
     '#theme_wrappers' => array('form_element', 'l10n_community_filter_form_element'),
+    '#access' => FALSE,
   );
 
   if (count($contexts = l10n_community_get_contexts()) > 1) {
@@ -118,7 +132,9 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
       '#default_value' => $form_state['values']['context'],
       '#attributes' => array('class' => array($class_is_default_context)),
       '#theme_wrappers' => array('form_element', 'l10n_community_filter_form_element'),
+      '#access' => FALSE,
     );
+    drupal_add_js(array('l10nVisualSearch' => array('contexts' => array_values($contexts))), 'setting');
   }
 
   $form['status'] = array(
@@ -128,17 +144,24 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
     '#suffix' => '</div>',
     '#display_none' => $form_state['values']['status'] == L10N_STATUS_ALL,
     '#theme_wrappers' => array('form_element', 'l10n_community_filter_form_element'),
+    '#access' => FALSE,
   );
   $form['status']['translation'] = array(
     '#type' => 'select',
     '#options' => $translation_options,
     '#default_value' => $form_state['values']['status'] & (L10N_STATUS_TRANSLATED | L10N_STATUS_UNTRANSLATED | L10N_STATUS_IS_TRANSLATION),
+    '#access' => FALSE,
   );
   $form['status']['suggestion'] = array(
     '#type' => 'select',
     '#options' => $suggestion_options,
     '#default_value' => $form_state['values']['status'] & (L10N_STATUS_HAS_SUGGESTION | L10N_STATUS_NO_SUGGESTION | L10N_STATUS_IS_SUGGESTION),
+    '#access' => FALSE,
   );
+  array_shift($translation_options);
+  array_shift($suggestion_options);
+  drupal_add_js(array('l10nVisualSearch' => array('translation_status' => $translation_options)), 'setting');
+  drupal_add_js(array('l10nVisualSearch' => array('suggestion_status' => $suggestion_options)), 'setting');
 
   $class_is_default_author = empty($form_state['values']['author']) ? 'is-default' : 'is-not-default';
   $form['author'] = array(
@@ -151,6 +174,7 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
     '#attributes' => array('class' => array($class_is_default_author)),
     '#display_none' => empty($form_state['values']['author']),
     '#theme_wrappers' => array('form_element', 'l10n_community_filter_form_element'),
+    '#access' => FALSE,
   );
 
   $class_is_default_search = empty($form_state['values']['search']) ? 'is-default' : 'is-not-default';
@@ -162,6 +186,7 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
     '#attributes' => array('class' => array($class_is_default_search)),
     '#display_none' => empty($form_state['values']['search']),
     '#theme_wrappers' => array('form_element', 'l10n_community_filter_form_element'),
+    '#access' => FALSE,
   );
 
   $class_is_default_sid = empty($form_state['values']['sid']) ? 'is-default' : 'is-not-default';
@@ -173,18 +198,28 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
     '#attributes' => array('class' => array($class_is_default_sid)),
     '#display_none' => empty($form_state['values']['sid']),
     '#theme_wrappers' => array('form_element', 'l10n_community_filter_form_element'),
+    '#access' => FALSE,
   );
 
   $class_is_default_limit = $form_state['values']['limit'] == 10 ? 'is-default' : 'is-not-default';
+  $limits = drupal_map_assoc(array(5, 10, 20, 30, 50));
   $form['limit'] = array(
     '#type' => 'select',
     '#title' => t('Limit'),
-    '#options' => drupal_map_assoc(array(5, 10, 20, 30, 50)),
+    '#options' => $limits,
     '#default_value' => $form_state['values']['limit'],
     '#attributes' => array('class' => array($class_is_default_limit)),
     '#display_none' => $form_state['values']['limit'] == 10,
     '#theme_wrappers' => array('form_element', 'l10n_community_filter_form_element'),
+    '#access' => FALSE,
   );
+  drupal_add_js(array('l10nVisualSearch' => array('limit' => array_values($limits))), 'setting');
+  drupal_add_js(array('l10nVisualSearch' => array('path' => current_path())), 'setting');
+  $query = '';
+  foreach(drupal_get_query_parameters() as $name => $value) {
+    $query .= "$name: \"$value\" ";
+  }
+  drupal_add_js(array('l10nVisualSearch' => array('query' => $query)), 'setting');
 
   $form['buttons'] = array(
     '#prefix' => '<div class="filter-submit">',
@@ -193,8 +228,10 @@ function l10n_community_filter_form($form, &$form_state, $langcode, $filters) {
   $form['buttons']['submit'] = array(
     '#value' => t('Filter'),
     '#type' => 'submit',
+    '#access' => FALSE,
   );
   $form['buttons']['reset'] = array(
+    '#access' => FALSE,
     '#type' => 'markup',
     '#markup' => '<div class="reset-link">' . l(t('Clear all filters'), 'translate/languages/' . $langcode . '/translate', array('attributes' => array('class' => array('filter-exclude')))) . '</div>',
   );
diff --git a/l10n_community/visualsearch/build/visualsearch-datauri.css b/l10n_community/visualsearch/build/visualsearch-datauri.css
new file mode 100644
index 0000000..3e3edd0
--- /dev/null
+++ b/l10n_community/visualsearch/build/visualsearch-datauri.css
@@ -0,0 +1,310 @@
+.VS-search .VS-icon {
+  background-repeat: no-repeat;
+  background-position: center center;
+  vertical-align: middle;
+  width: 16px; height: 16px;
+}
+  .VS-search .VS-icon-cancel {
+    width: 11px; height: 11px;
+    background-position: center 0;
+    background-image: url("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAWCAYAAAAW5GZjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAb9JREFUeNqUUr1qAkEQ3j0khQp6kihaeGgEEa18gTQR0iRY+BaBSMDGwidIEUKqFL6BopgqBAJ5AMFGjUU0d4WHEvwJarvZ77gRIzGYgb1hZr+Z75vZ40IIzqTNZrPj8Xicn0wmmcViEXS73aaqqq+BQODG6/W+A8MBNk3zfDAY3C6Xy0O2ZS6X6zMSiVwHg8FHLjtq7Xb7RQKj7BeTzVCgJ5PJU2U0GhUk7REuMpkMi8fjFggeMeecrVYrFRId0CgTAgDDMFg4HLbA8IjJgHNgGEr0er0fQIphUmZAwdSUADUB4RFDsz3oSMF6CLzZkQqgGebz+Z75dDqNdTqdp13bgDmdTj2VSp0oWHg0Gr2UNH2Z/9o+yMv7K4/HY/C/XhDUfr//jl7QQVT9fp/V63VWqVRYt9tliUSCZbPZg1wux9Lp9PqFeK1Wu9A0DdXz7YM87i0FrVZLs4Fi1wmFQh/NZjOmVKvVgq7rR/QflMtlixGedjwcDlUpMQ9tbzalkAAB2/R297mNW+sT2wUbUnA//V/nYrH4QOBNABUQuFQq3TNMuc82sDVrz41G42yvPeODAwZQ0QzwiJEnzLcAAwBJ6WXlwoBgZAAAAABJRU5ErkJggg==");
+    cursor: pointer;
+  }
+    .VS-search .VS-icon-cancel:hover {
+      background-position: center -11px;
+    }
+  .VS-search .VS-icon-search {
+    width: 12px; height: 12px;
+    background-image: url("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAUZJREFUeNpUUM2qgmAQzS8NiUgLzTIXLZQW1QuI9AY9QPSW9gQ9QiriwpJQEBVrVWT2d7p2L9xZzDdzZs7M+YYqy/J8Ptu2vd/v4zgeDAaqqk4mE47jar9GnU6nzWbjOA5FUa/Xq0Jns9l8Pud5vkpp58cwAOzhcBhFkeu6GNztdg3D+Db5vo9nOp2iiWGYTqdDCMFe4LquI0aVpGmKR9M0lmUbjQY8YiBJklTb4YkoilBzOBzq9TogeMQIJEmqmlAlo9EIyXa7tSyrKAp4xEBkWUb5q2k8Hh+PR8/zwjCEgufz+aESstvtoKnVan2GgY31kBkEAfT1ej1FUZDiNIIgrFYr9H1ug3teLpfH43G/3/FBUJGu1+s8z8FZLpc0mmiabrfbf5fEumazuVgsTNO8Xq+3242qRNT+G0CMz7IMzH6//xZgAA60tj6rqzxpAAAAAElFTkSuQmCC");
+  }
+
+/*------------------------------ RESET + DEFAULT STYLES ---------------------------------*/
+
+/* 
+Eric Meyer's final reset.css
+Source: http://meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/ 
+*/
+.VS-search div, .VS-search span, .VS-search a, .VS-search img, 
+.VS-search ul, .VS-search li, .VS-search form, .VS-search label,
+.VS-interface ul, .VS-interface li, .VS-interface {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  outline: 0;
+  font-weight: inherit;
+  font-style: inherit;
+  font-size: 100%;
+  font-family: inherit;
+  vertical-align: baseline;
+}
+
+.VS-search :focus {
+  outline: 0;
+}
+.VS-search {
+  line-height: 1;
+  color: black;
+}
+.VS-search ol, .VS-search ul {
+  list-style: none;
+}
+
+/* ===================== */
+/* = General and Reset = */
+/* ===================== */
+
+.VS-search {
+  font-family: Arial, sans-serif;
+  color: #373737;
+  font-size: 12px;
+}
+.VS-search input {
+  display: block;
+  border: none;
+  -moz-box-shadow: none;
+  -webkit-box-shadow: none;
+  box-shadow: none;
+  outline: none;
+  margin: 0; padding: 4px;
+  background: transparent;
+  font-size: 16px;
+  line-height: 20px;
+  width: 100%;
+}
+.VS-interface, .VS-search .dialog, .VS-search input {
+  font-family: "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif !important;
+  line-height: 1.1em;
+}
+
+/* ========== */
+/* = Layout = */
+/* ========== */
+
+.VS-search .VS-search-box {
+  cursor: text;
+  position: relative;
+  background: transparent;
+  border: 2px solid #ccc;
+  border-radius: 16px; -webkit-border-radius: 16px; -moz-border-radius: 16px;
+  background-color: #fafafa;
+  -webkit-box-shadow: inset 0px 0px 3px #ccc;
+  -moz-box-shadow: inset 0px 0px 3px #ccc;
+  box-shadow: inset 0px 0px 3px #ccc;
+  min-height: 28px;
+  height: auto;
+}
+  .VS-search .VS-search-box.VS-focus {
+    border-color: #acf;
+    -webkit-box-shadow: inset 0px 0px 3px #acf;
+    -moz-box-shadow: inset 0px 0px 3px #acf;
+    box-shadow: inset 0px 0px 3px #acf;
+  }
+  .VS-search .VS-search-inner {
+    position: relative;
+    margin: 0 20px 0 22px;
+    overflow: hidden;
+  }
+  .VS-search input {
+    width: 100px;
+  }
+  .VS-search input,
+  .VS-search .VS-input-width-tester {
+    padding: 6px 0;
+    float: left;
+    color: #808080;
+    font: 13px/17px Helvetica, Arial;
+  }
+  .VS-search.VS-focus input {
+    color: #606060;
+  }
+  .VS-search .VS-icon-search {
+    position: absolute;
+    left: 9px; top: 8px;
+  }
+  .VS-search .VS-icon-cancel {
+    position: absolute;
+    right: 9px; top: 8px;
+  }
+
+/* ================ */
+/* = Search Facet = */
+/* ================ */
+
+.VS-search .search_facet {
+  float: left;
+  margin: 0;
+  padding: 0 0 0 14px;
+  position: relative;
+  border: 1px solid transparent;
+  height: 20px;
+  margin: 3px -3px 3px 0;
+}
+  .VS-search .search_facet.is_selected {
+    margin-left: -3px;
+    -webkit-border-radius: 16px;
+    -moz-border-radius: 16px;
+    border-radius: 16px;
+    background-color: #d2e6fd;
+    background-image: -moz-linear-gradient(top, #d2e6fd, #b0d1f9); /* FF3.6 */
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#d2e6fd), to(#b0d1f9)); /* Saf4+, Chrome */
+    background-image: linear-gradient(top, #d2e6fd, #b0d1f9);
+    border: 1px solid #6eadf5;
+  }
+  .VS-search .search_facet .category {
+    float: left;
+    text-transform: uppercase;
+    font-weight: bold;
+    font-size: 10px;
+    color: #808080;
+    padding: 8px 0 5px;
+    line-height: 13px;
+    cursor: pointer;
+    padding: 4px 0 0;
+  }
+  .VS-search .search_facet.is_selected .category {
+    margin-left: 3px;
+  }
+  .VS-search .search_facet .search_facet_input_container {
+    float: left;
+  }
+  .VS-search .search_facet input {
+    margin: 0;
+    padding: 0;
+    color: #000;
+    font-size: 13px;
+    line-height: 16px;
+    padding: 5px 0 5px 4px;
+    height: 16px;
+    width: auto;
+    z-index: 100;
+    position: relative;
+    padding-top: 1px;
+    padding-bottom: 2px;
+    padding-right: 3px;
+
+  }
+  .VS-search .search_facet.is_editing input,
+  .VS-search .search_facet.is_selected input {
+    color: #000;
+  }
+  .VS-search .search_facet .search_facet_remove {
+    position: absolute;
+    left: 0;
+    top: 4px;
+  }
+    .VS-search .search_facet.is_selected .search_facet_remove {
+      opacity: 0.4;
+      left: 3px;
+      filter: alpha(opacity=40);
+      background-position: center -11px;
+    }
+    .VS-search .search_facet .search_facet_remove:hover {
+      opacity: 1;
+    }
+  .VS-search .search_facet.is_editing .category,
+  .VS-search .search_facet.is_selected .category {
+    color: #000;
+  }
+  .VS-search .search_facet.search_facet_maybe_delete .category,
+  .VS-search .search_facet.search_facet_maybe_delete input {
+    color: darkred;
+  }
+
+/* ================ */
+/* = Search Input = */
+/* ================ */
+
+.VS-search .search_input {
+  height: 28px;
+  float: left;
+  margin-left: -1px;
+}
+  .VS-search .search_input input {
+    padding: 6px 3px 6px 2px;
+    line-height: 10px;
+    height: 22px;
+    margin-top: -4px;
+    width: 10px;
+    z-index: 100;
+    min-width: 4px;
+    position: relative;
+  }
+  .VS-search .search_input.is_editing input {
+    color: #202020;
+  }
+
+/* ================ */
+/* = Autocomplete = */
+/* ================ */
+
+.VS-interface.ui-autocomplete {
+  position: absolute;
+  border: 1px solid #C0C0C0;
+  border-top: 1px solid #D9D9D9;
+  background-color: #F6F6F6;
+  cursor: pointer;
+  z-index: 10000;
+  padding: 0;
+  margin: 0;
+  width: auto;
+  min-width: 80px;
+  max-width: 220px;
+  max-height: 240px;
+  overflow-y: auto;
+  overflow-x: hidden;
+  font-size: 13px;
+  top: 5px;
+  opacity: 0.97;
+  box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5); -moz-box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5);
+}
+  .VS-interface.ui-autocomplete .ui-autocomplete-category {
+    text-transform: capitalize;
+    font-size: 11px;
+    padding: 4px 4px 4px;
+    border-top: 1px solid #A2A2A2;
+    border-bottom: 1px solid #A2A2A2;
+    background-color: #B7B7B7;
+    text-shadow: 0 -1px 0 #999;
+    font-weight: bold;
+    color: white;
+    cursor: default;
+  }
+  .VS-interface.ui-autocomplete .ui-menu-item {
+      float: none;
+  }
+  .VS-interface.ui-autocomplete .ui-menu-item a {
+    color: #000;
+    outline: none;
+    display: block;
+    padding: 3px 4px 5px;
+    border-radius: none;
+    line-height: 1;
+    background-color: #F8F8F8;
+    background-image: -moz-linear-gradient(top, #F8F8F8, #F3F3F3); /* FF3.6 */
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#F8F8F8), to(#F3F3F3)); /* Saf4+, Chrome */
+    background-image: linear-gradient(top, #F8F8F8, #F3F3F3);
+    border-top: 1px solid #FAFAFA;
+    border-bottom: 1px solid #f0f0f0;
+  }
+  .VS-interface.ui-autocomplete .ui-menu-item a:active {
+    outline: none;
+  }
+  .VS-interface.ui-autocomplete .ui-menu-item .ui-state-hover {
+    background-color: #6483F7;
+    background-image: -moz-linear-gradient(top, #648bF5, #2465f3); /* FF3.6 */
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#648bF5), to(#2465f3)); /* Saf4+, Chrome */
+    background-image: linear-gradient(top, #648bF5, #2465f3);
+    border-top: 1px solid #5b83ec;
+    border-bottom: 1px solid #1459e9;
+    border-left: none;
+    border-right: none;
+    color: white;
+    margin: 0;
+  }
+  .VS-interface.ui-autocomplete .ui-corner-all {
+    border-radius: 0;
+  }
+  .VS-interface.ui-autocomplete li {
+    list-style: none;
+    width: auto;
+  }
diff --git a/l10n_community/visualsearch/build/visualsearch.js b/l10n_community/visualsearch/build/visualsearch.js
new file mode 100644
index 0000000..61ee39a
--- /dev/null
+++ b/l10n_community/visualsearch/build/visualsearch.js
@@ -0,0 +1,1879 @@
+// This is the annotated source code for
+// [VisualSearch.js](http://documentcloud.github.com/visualsearch/),
+// a rich search box for real data.
+// 
+// The annotated source HTML is generated by
+// [Docco](http://jashkenas.github.com/docco/).
+
+/** @license VisualSearch.js 0.3.0
+ *  (c) 2011 Samuel Clay, @samuelclay, DocumentCloud Inc.
+ *  VisualSearch.js may be freely distributed under the MIT license.
+ *  For all details and documentation:
+ *  http://documentcloud.github.com/visualsearch
+ */
+
+(function() {
+
+  var $ = jQuery; // Handle namespaced jQuery
+
+  // Setting up VisualSearch globals. These will eventually be made instance-based.
+  if (!window.VS) window.VS = {};
+  if (!VS.app)    VS.app    = {};
+  if (!VS.ui)     VS.ui     = {};
+  if (!VS.model)  VS.model  = {};
+  if (!VS.utils)  VS.utils  = {};
+
+  // Sets the version for VisualSearch to be used programatically elsewhere.
+  VS.VERSION = '0.3.0';
+
+  VS.VisualSearch = function(options) {
+    var defaults = {
+      container   : '',
+      query       : '',
+      autosearch  : true,
+      unquotable  : [],
+      remainder   : 'text',
+      showFacets  : true,
+      callbacks   : {
+        search          : $.noop,
+        focus           : $.noop,
+        blur            : $.noop,
+        facetMatches    : $.noop,
+        valueMatches    : $.noop
+      }
+    };
+    this.options           = _.extend({}, defaults, options);
+    this.options.callbacks = _.extend({}, defaults.callbacks, options.callbacks);
+    
+    VS.app.hotkeys.initialize();
+    this.searchQuery   = new VS.model.SearchQuery();
+    this.searchBox     = new VS.ui.SearchBox({
+        app: this, 
+        showFacets: this.options.showFacets
+    });
+
+    if (options.container) {
+      var searchBox = this.searchBox.render().el;
+      $(this.options.container).html(searchBox);
+    }
+    this.searchBox.value(this.options.query || '');
+
+    // Disable page caching for browsers that incorrectly cache the visual search inputs.
+    // This is forced the browser to re-render the page when it is retrieved in its history.
+    $(window).bind('unload', function(e) {});
+
+    // Gives the user back a reference to the `searchBox` so they
+    // can use public methods.
+    return this;
+  };
+
+  // Entry-point used to tie all parts of VisualSearch together. It will either attach
+  // itself to `options.container`, or pass back the `searchBox` so it can be rendered
+  // at will.
+  VS.init = function(options) {
+    return new VS.VisualSearch(options);
+  };
+
+})();
+(function() {
+
+var $ = jQuery; // Handle namespaced jQuery
+
+// The search box is responsible for managing the many facet views and input views.
+VS.ui.SearchBox = Backbone.View.extend({
+
+  id  : 'search',
+
+  events : {
+    'click .VS-cancel-search-box' : 'clearSearch',
+    'mousedown .VS-search-box'    : 'maybeFocusSearch',
+    'dblclick .VS-search-box'     : 'highlightSearch',
+    'click .VS-search-box'        : 'maybeTripleClick'
+  },
+
+  // Creating a new SearchBox registers handlers for re-rendering facets when necessary,
+  // as well as handling typing when a facet is selected.
+  initialize : function() {
+    this.app = this.options.app;
+    this.flags = {
+      allSelected : false
+    };
+    this.facetViews = [];
+    this.inputViews = [];
+    _.bindAll(this, 'renderFacets', '_maybeDisableFacets', 'disableFacets',
+              'deselectAllFacets', 'addedFacet', 'removedFacet', 'changedFacet');
+    this.app.searchQuery
+            .bind('reset', this.renderFacets)
+            .bind('add', this.addedFacet)
+            .bind('remove', this.removedFacet)
+            .bind('change', this.changedFacet);
+    $(document).bind('keydown', this._maybeDisableFacets);
+  },
+
+  // Renders the search box, but requires placement on the page through `this.el`.
+  render : function() {
+    $(this.el).append(JST['search_box']({}));
+    $(document.body).setMode('no', 'search');
+
+    return this;
+  },
+
+  // # Querying Facets #
+
+  // Either gets a serialized query string or sets the faceted query from a query string.
+  value : function(query) {
+    if (query == null) return this.serialize();
+    return this.setQuery(query);
+  },
+
+  // Uses the VS.app.searchQuery collection to serialize the current query from the various
+  // facets that are in the search box.
+  serialize : function() {
+    var query           = [];
+    var inputViewsCount = this.inputViews.length;
+
+    this.app.searchQuery.each(_.bind(function(facet, i) {
+      query.push(this.inputViews[i].value());
+      query.push(facet.serialize());
+    }, this));
+
+    if (inputViewsCount) {
+      query.push(this.inputViews[inputViewsCount-1].value());
+    }
+
+    return _.compact(query).join(' ');
+  },
+
+  // Takes a query string and uses the SearchParser to parse and render it. Note that
+  // `VS.app.SearchParser` refreshes the `VS.app.searchQuery` collection, which is bound
+  // here to call `this.renderFacets`.
+  setQuery : function(query) {
+    this.currentQuery = query;
+    VS.app.SearchParser.parse(this.app, query);
+  },
+
+  // Returns the position of a facet/input view. Useful when moving between facets.
+  viewPosition : function(view) {
+    var views    = view.type == 'facet' ? this.facetViews : this.inputViews;
+    var position = _.indexOf(views, view);
+    if (position == -1) position = 0;
+    return position;
+  },
+
+  // Used to launch a search. Hitting enter or clicking the search button.
+  searchEvent : function(e) {
+    var query = this.value();
+    this.focusSearch(e);
+    this.value(query);
+    this.app.options.callbacks.search(query, this.app.searchQuery);
+  },
+
+  // # Rendering Facets #
+
+  // Add a new facet. Facet will be focused and ready to accept a value. Can also
+  // specify position, in the case of adding facets from an inbetween input.
+  addFacet : function(category, initialQuery, position) {
+    category     = VS.utils.inflector.trim(category);
+    initialQuery = VS.utils.inflector.trim(initialQuery || '');
+    if (!category) return;
+    
+    var model = new VS.model.SearchFacet({
+      category : category,
+      value    : initialQuery || '',
+      app      : this.app
+    });
+    this.app.searchQuery.add(model, {at: position});
+  },
+
+  // Renders a newly added facet, and selects it.
+  addedFacet : function (model) {
+    this.renderFacets();
+    var facetView = _.detect(this.facetViews, function(view) {
+      if (view.model == model) return true;
+    });
+
+    _.defer(function() {
+      facetView.enableEdit();
+    });
+  },
+
+  // Changing a facet programmatically re-renders it.
+  changedFacet: function () {
+    this.renderFacets();
+  },
+
+  // When removing a facet, potentially do something. For now, the adjacent
+  // remaining facet is selected, but this is handled by the facet's view,
+  // since its position is unknown by the time the collection triggers this
+  // remove callback.
+  removedFacet : function (facet, query, options) {},
+
+  // Renders each facet as a searchFacet view.
+  renderFacets : function() {
+    this.facetViews = [];
+    this.inputViews = [];
+
+    this.$('.VS-search-inner').empty();
+
+    this.app.searchQuery.each(_.bind(this.renderFacet, this));
+
+    // Add on an n+1 empty search input on the very end.
+    this.renderSearchInput();
+  },
+
+  // Render a single facet, using its category and query value.
+  renderFacet : function(facet, position) {
+    var view = new VS.ui.SearchFacet({
+      app   : this.app,
+      model : facet,
+      order : position
+    });
+
+    // Input first, facet second.
+    this.renderSearchInput();
+    this.facetViews.push(view);
+    this.$('.VS-search-inner').children().eq(position*2).after(view.render().el);
+
+    view.calculateSize();
+    _.defer(_.bind(view.calculateSize, view));
+
+    return view;
+  },
+
+  // Render a single input, used to create and autocomplete facets
+  renderSearchInput : function() {
+    var input = new VS.ui.SearchInput({
+      position: this.inputViews.length, 
+      app: this.app,
+      showFacets: this.options.showFacets
+    });
+    this.$('.VS-search-inner').append(input.render().el);
+    this.inputViews.push(input);
+  },
+
+  // # Modifying Facets #
+
+  // Clears out the search box. Command+A + delete can trigger this, as can a cancel button.
+  //
+  // If a `clearSearch` callback was provided, the callback is invoked and
+  // provided with a function performs the actual removal of the data.  This
+  // allows third-party developers to either clear data asynchronously, or
+  // prior to performing their custom "clear" logic.
+  clearSearch : function(e) {
+    var actualClearSearch = _.bind(function() {
+      this.disableFacets();
+      this.value('');
+      this.flags.allSelected = false;
+      this.searchEvent(e);
+      this.focusSearch(e);
+    }, this);
+
+    if (this.app.options.callbacks.clearSearch) {
+      this.app.options.callbacks.clearSearch(actualClearSearch);
+    } else {
+      actualClearSearch();
+    }
+  },
+
+  // Command+A selects all facets.
+  selectAllFacets : function() {
+    this.flags.allSelected = true;
+
+    $(document).one('click.selectAllFacets', this.deselectAllFacets);
+
+    _.each(this.facetViews, function(facetView, i) {
+      facetView.selectFacet();
+    });
+    _.each(this.inputViews, function(inputView, i) {
+      inputView.selectText();
+    });
+  },
+
+  // Used by facets and input to see if all facets are currently selected.
+  allSelected : function(deselect) {
+    if (deselect) this.flags.allSelected = false;
+    return this.flags.allSelected;
+  },
+
+  // After `selectAllFacets` is engaged, this method is bound to the entire document.
+  // This immediate disables and deselects all facets, but it also checks if the user
+  // has clicked on either a facet or an input, and properly selects the view.
+  deselectAllFacets : function(e) {
+    this.disableFacets();
+
+    if (this.$(e.target).is('.category,input')) {
+      var el   = $(e.target).closest('.search_facet,.search_input');
+      var view = _.detect(this.facetViews.concat(this.inputViews), function(v) {
+        return v.el == el[0];
+      });
+      if (view.type == 'facet') {
+        view.selectFacet();
+      } else if (view.type == 'input') {
+        _.defer(function() {
+          view.enableEdit(true);
+        });
+      }
+    }
+  },
+
+  // Disables all facets except for the passed in view. Used when switching between
+  // facets, so as not to have to keep state of active facets.
+  disableFacets : function(keepView) {
+    _.each(this.inputViews, function(view) {
+      if (view && view != keepView &&
+          (view.modes.editing == 'is' || view.modes.selected == 'is')) {
+        view.disableEdit();
+      }
+    });
+    _.each(this.facetViews, function(view) {
+      if (view && view != keepView &&
+          (view.modes.editing == 'is' || view.modes.selected == 'is')) {
+        view.disableEdit();
+        view.deselectFacet();
+      }
+    });
+
+    this.flags.allSelected = false;
+    this.removeFocus();
+    $(document).unbind('click.selectAllFacets');
+  },
+
+  // Resize all inputs to account for extra keystrokes which may be changing the facet
+  // width incorrectly. This is a safety check to ensure inputs are correctly sized.
+  resizeFacets : function(view) {
+    _.each(this.facetViews, function(facetView, i) {
+      if (!view || facetView == view) {
+        facetView.resize();
+      }
+    });
+  },
+
+  // Handles keydown events on the document. Used to complete the Cmd+A deletion, and
+  // blurring focus.
+  _maybeDisableFacets : function(e) {
+    if (this.flags.allSelected && VS.app.hotkeys.key(e) == 'backspace') {
+      e.preventDefault();
+      this.clearSearch(e);
+      return false;
+    } else if (this.flags.allSelected && VS.app.hotkeys.printable(e)) {
+      this.clearSearch(e);
+    }
+  },
+
+  // # Focusing Facets #
+
+  // Move focus between facets and inputs. Takes a direction as well as many options
+  // for skipping over inputs and only to facets, placement of cursor position in facet
+  // (i.e. at the end), and selecting the text in the input/facet.
+  focusNextFacet : function(currentView, direction, options) {
+    options = options || {};
+    var viewCount    = this.facetViews.length;
+    var viewPosition = options.viewPosition || this.viewPosition(currentView);
+
+    if (!options.skipToFacet) {
+      // Correct for bouncing between matching text and facet arrays.
+      if (currentView.type == 'text'  && direction > 0) direction -= 1;
+      if (currentView.type == 'facet' && direction < 0) direction += 1;
+    } else if (options.skipToFacet && currentView.type == 'text' &&
+               viewCount == viewPosition && direction >= 0) {
+      // Special case of looping around to a facet from the last search input box.
+      viewPosition = 0;
+      direction    = 0;
+    }
+    var view, next = Math.min(viewCount, viewPosition + direction);
+
+    if (currentView.type == 'text') {
+      if (next >= 0 && next < viewCount) {
+        view = this.facetViews[next];
+      } else if (next == viewCount) {
+        view = this.inputViews[this.inputViews.length-1];
+      }
+      if (view && options.selectFacet && view.type == 'facet') {
+        view.selectFacet();
+      } else if (view) {
+        view.enableEdit();
+        view.setCursorAtEnd(direction || options.startAtEnd);
+      }
+    } else if (currentView.type == 'facet') {
+      if (options.skipToFacet) {
+        if (next >= viewCount || next < 0) {
+          view = _.last(this.inputViews);
+          view.enableEdit();
+        } else {
+          view = this.facetViews[next];
+          view.enableEdit();
+          view.setCursorAtEnd(direction || options.startAtEnd);
+        }
+      } else {
+        view = this.inputViews[next];
+        view.enableEdit();
+      }
+    }
+    if (options.selectText) view.selectText();
+    this.resizeFacets();
+  },
+
+  maybeFocusSearch : function(e) {
+    if ($(e.target).is('.VS-search-box') ||
+        $(e.target).is('.VS-search-inner') ||
+        e.type == 'keydown') {
+      this.focusSearch(e);
+    }
+  },
+
+  // Bring focus to last input field.
+  focusSearch : function(e, selectText) {
+    var view = this.inputViews[this.inputViews.length-1];
+    view.enableEdit(selectText);
+    if (!selectText) view.setCursorAtEnd(-1);
+    if (e.type == 'keydown') {
+      view.keydown(e);
+      view.box.trigger('keydown');
+    }
+    _.defer(_.bind(function() {
+      if (!this.$('input:focus').length) {
+        view.enableEdit(selectText);
+      }
+    }, this));
+  },
+
+  // Double-clicking on the search wrapper should select the existing text in
+  // the last search input. Also start the triple-click timer.
+  highlightSearch : function(e) {
+    if ($(e.target).is('.VS-search-box') ||
+        $(e.target).is('.VS-search-inner') ||
+        e.type == 'keydown') {
+      var lastinput = this.inputViews[this.inputViews.length-1];
+      lastinput.startTripleClickTimer();
+      this.focusSearch(e, true);
+    }
+  },
+
+  maybeTripleClick : function(e) {
+    var lastinput = this.inputViews[this.inputViews.length-1];
+    return lastinput.maybeTripleClick(e);
+  },
+
+  // Used to show the user is focused on some input inside the search box.
+  addFocus : function() {
+    this.app.options.callbacks.focus();
+    this.$('.VS-search-box').addClass('VS-focus');
+  },
+
+  // User is no longer focused on anything in the search box.
+  removeFocus : function() {
+    this.app.options.callbacks.blur();
+    var focus = _.any(this.facetViews.concat(this.inputViews), function(view) {
+      return view.isFocused();
+    });
+    if (!focus) this.$('.VS-search-box').removeClass('VS-focus');
+  },
+
+  // Show a menu which adds pre-defined facets to the search box. This is unused for now.
+  showFacetCategoryMenu : function(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    if (this.facetCategoryMenu && this.facetCategoryMenu.modes.open == 'is') {
+      return this.facetCategoryMenu.close();
+    }
+
+    var items = [
+      {title: 'Account', onClick: _.bind(this.addFacet, this, 'account', '')},
+      {title: 'Project', onClick: _.bind(this.addFacet, this, 'project', '')},
+      {title: 'Filter', onClick: _.bind(this.addFacet, this, 'filter', '')},
+      {title: 'Access', onClick: _.bind(this.addFacet, this, 'access', '')}
+    ];
+
+    var menu = this.facetCategoryMenu || (this.facetCategoryMenu = new dc.ui.Menu({
+      items       : items,
+      standalone  : true
+    }));
+
+    this.$('.VS-icon-search').after(menu.render().open().content);
+    return false;
+  }
+
+});
+
+})();
+
+(function() {
+
+var $ = jQuery; // Handle namespaced jQuery
+
+// This is the visual search facet that holds the category and its autocompleted
+// input field.
+VS.ui.SearchFacet = Backbone.View.extend({
+
+  type : 'facet',
+
+  className : 'search_facet',
+
+  events : {
+    'click .category'           : 'selectFacet',
+    'keydown input'             : 'keydown',
+    'mousedown input'           : 'enableEdit',
+    'mouseover .VS-icon-cancel' : 'showDelete',
+    'mouseout .VS-icon-cancel'  : 'hideDelete',
+    'click .VS-icon-cancel'     : 'remove'
+  },
+
+  initialize : function(options) {
+    this.flags = {
+      canClose : false
+    };
+    _.bindAll(this, 'set', 'keydown', 'deselectFacet', 'deferDisableEdit');
+  },
+
+  // Rendering the facet sets up autocompletion, events on blur, and populates
+  // the facet's input with its starting value.
+  render : function() {
+    $(this.el).html(JST['search_facet']({
+      model : this.model
+    }));
+
+    this.setMode('not', 'editing');
+    this.setMode('not', 'selected');
+    this.box = this.$('input');
+    this.box.val(this.model.get('value'));
+    this.box.bind('blur', this.deferDisableEdit);
+    // Handle paste events with `propertychange`
+    this.box.bind('input propertychange', this.keydown);
+    this.setupAutocomplete();
+
+    return this;
+  },
+
+  // This method is used to setup the facet's input to auto-grow.
+  // This is defered in the searchBox so it can be attached to the
+  // DOM to get the correct font-size.
+  calculateSize : function() {
+    this.box.autoGrowInput();
+    this.box.unbind('updated.autogrow');
+    this.box.bind('updated.autogrow', _.bind(this.moveAutocomplete, this));
+  },
+
+  // Forces a recalculation of this facet's input field's value. Called when
+  // the facet is focused, removed, or otherwise modified.
+  resize : function(e) {
+    this.box.trigger('resize.autogrow', e);
+  },
+
+  // Watches the facet's input field to see if it matches the beginnings of
+  // words in `autocompleteValues`, which is different for every category.
+  // If the value, when selected from the autocompletion menu, is different
+  // than what it was, commit the facet and search for it.
+  setupAutocomplete : function() {
+    this.box.autocomplete({
+      source    : _.bind(this.autocompleteValues, this),
+      minLength : 0,
+      delay     : 0,
+      autoFocus : true,
+      position  : {offset : "0 5"},
+      create    : _.bind(function(e, ui) {
+        $(this.el).find('.ui-autocomplete-input').css('z-index','auto');
+      }, this),
+      select    : _.bind(function(e, ui) {
+        e.preventDefault();
+        var originalValue = this.model.get('value');
+        this.set(ui.item.value);
+        if (originalValue != ui.item.value || this.box.val() != ui.item.value) {
+            if (this.options.app.options.autosearch) {
+                this.search(e);
+            }
+        }
+        return false;
+      }, this),
+      open      : _.bind(function(e, ui) {
+        var box = this.box;
+        this.box.autocomplete('widget').find('.ui-menu-item').each(function() {
+          var $value = $(this);
+          if ($value.data('item.autocomplete')['value'] == box.val()) {
+            box.data('autocomplete').menu.activate(new $.Event("mouseover"), $value);
+          }
+        });
+      }, this)
+    });
+
+    this.box.autocomplete('widget').addClass('VS-interface');
+  },
+
+  // As the facet's input field grows, it may move to the next line in the
+  // search box. `autoGrowInput` triggers an `updated` event on the input
+  // field, which is bound to this method to move the autocomplete menu.
+  moveAutocomplete : function() {
+    var autocomplete = this.box.data('autocomplete');
+    if (autocomplete) {
+      autocomplete.menu.element.position({
+        my        : "left top",
+        at        : "left bottom",
+        of        : this.box.data('autocomplete').element,
+        collision : "flip",
+        offset    : "0 5"
+      });
+    }
+  },
+
+  // When a user enters a facet and it is being edited, immediately show
+  // the autocomplete menu and size it to match the contents.
+  searchAutocomplete : function(e) {
+    var autocomplete = this.box.data('autocomplete');
+    if (autocomplete) {
+      var menu = autocomplete.menu.element;
+      autocomplete.search();
+
+      // Resize the menu based on the correctly measured width of what's bigger:
+      // the menu's original size or the menu items' new size.
+      menu.outerWidth(Math.max(
+        menu.width('').outerWidth(),
+        autocomplete.element.outerWidth()
+      ));
+    }
+  },
+
+  // Closes the autocomplete menu. Called on disabling, selecting, deselecting,
+  // and anything else that takes focus out of the facet's input field.
+  closeAutocomplete : function() {
+    var autocomplete = this.box.data('autocomplete');
+    if (autocomplete) autocomplete.close();
+  },
+
+  // Search terms used in the autocomplete menu. These are specific to the facet,
+  // and only match for the facet's category. The values are then matched on the
+  // first letter of any word in matches, and finally sorted according to the
+  // value's own category. You can pass `preserveOrder` as an option in the 
+  // `facetMatches` callback to skip any further ordering done client-side.
+  autocompleteValues : function(req, resp) {
+    var category = this.model.get('category');
+    var value    = this.model.get('value');
+    var searchTerm = req.term;
+
+    this.options.app.options.callbacks.valueMatches(category, searchTerm, function(matches, options) {
+      options = options || {};
+      matches = matches || [];
+      
+      if (searchTerm && value != searchTerm) {
+        if (options.preserveMatches) {
+          return matches;
+        } else {
+          var re = VS.utils.inflector.escapeRegExp(searchTerm || '');
+          var matcher = new RegExp('\\b' + re, 'i');
+          matches = $.grep(matches, function(item) {
+            return matcher.test(item) ||
+                   matcher.test(item.value) ||
+                   matcher.test(item.label);
+        });
+        }
+      }
+      
+      if (options.preserveOrder) {
+        resp(matches);
+      } else {
+        resp(_.sortBy(matches, function(match) {
+          if (match == value || match.value == value) return '';
+          else return match;
+        }));
+      }
+    });
+
+  },
+
+  // Sets the facet's model's value.
+  set : function(value) {
+    if (!value) return;
+    this.model.set({'value': value});
+  },
+
+  // Before the searchBox performs a search, we need to close the
+  // autocomplete menu.
+  search : function(e, direction) {
+    if (!direction) direction = 1;
+    this.closeAutocomplete();
+    this.options.app.searchBox.searchEvent(e);
+    _.defer(_.bind(function() {
+      this.options.app.searchBox.focusNextFacet(this, direction, {viewPosition: this.options.order});
+    }, this));
+  },
+
+  // Begin editing the facet's input. This is called when the user enters
+  // the input either from another facet or directly clicking on it.
+  //
+  // This method tells all other facets and inputs to disable so it can have
+  // the sole focus. It also prepares the autocompletion menu.
+  enableEdit : function() {
+    if (this.modes.editing != 'is') {
+      this.setMode('is', 'editing');
+      this.deselectFacet();
+      if (this.box.val() == '') {
+        this.box.val(this.model.get('value'));
+      }
+    }
+
+    this.flags.canClose = false;
+    this.options.app.searchBox.disableFacets(this);
+    this.options.app.searchBox.addFocus();
+    _.defer(_.bind(function() {
+      this.options.app.searchBox.addFocus();
+    }, this));
+    this.resize();
+    this.searchAutocomplete();
+    this.box.focus();
+  },
+
+  // When the user blurs the input, they may either be going to another input
+  // or off the search box entirely. If they go to another input, this facet
+  // will be instantly disabled, and the canClose flag will be turned back off.
+  //
+  // However, if the user clicks elsewhere on the page, this method starts a timer
+  // that checks if any of the other inputs are selected or are being edited. If
+  // not, then it can finally close itself and its autocomplete menu.
+  deferDisableEdit : function() {
+    this.flags.canClose = true;
+    _.delay(_.bind(function() {
+      if (this.flags.canClose && !this.box.is(':focus') &&
+          this.modes.editing == 'is' && this.modes.selected != 'is') {
+        this.disableEdit();
+      }
+    }, this), 250);
+  },
+
+  // Called either by other facets receiving focus or by the timer in `deferDisableEdit`,
+  // this method will turn off the facet, remove any text selection, and close
+  // the autocomplete menu.
+  disableEdit : function() {
+    var newFacetQuery = VS.utils.inflector.trim(this.box.val());
+    if (newFacetQuery != this.model.get('value')) {
+      this.set(newFacetQuery);
+    }
+    this.flags.canClose = false;
+    this.box.selectRange(0, 0);
+    this.box.blur();
+    this.setMode('not', 'editing');
+    this.closeAutocomplete();
+    this.options.app.searchBox.removeFocus();
+  },
+
+  // Selects the facet, which blurs the facet's input and highlights the facet.
+  // If this is the only facet being selected (and not part of a select all event),
+  // we attach a mouse/keyboard watcher to check if the next action by the user
+  // should delete this facet or just deselect it.
+  selectFacet : function(e) {
+    if (e) e.preventDefault();
+    var allSelected = this.options.app.searchBox.allSelected();
+    if (this.modes.selected == 'is') return;
+
+    if (this.box.is(':focus')) {
+      this.box.setCursorPosition(0);
+      this.box.blur();
+    }
+
+    this.flags.canClose = false;
+    this.closeAutocomplete();
+    this.setMode('is', 'selected');
+    this.setMode('not', 'editing');
+    if (!allSelected || e) {
+      $(document).unbind('keydown.facet', this.keydown);
+      $(document).unbind('click.facet', this.deselectFacet);
+      _.defer(_.bind(function() {
+        $(document).unbind('keydown.facet').bind('keydown.facet', this.keydown);
+        $(document).unbind('click.facet').one('click.facet', this.deselectFacet);
+      }, this));
+      this.options.app.searchBox.disableFacets(this);
+      this.options.app.searchBox.addFocus();
+    }
+    return false;
+  },
+
+  // Turns off highlighting on the facet. Called in a variety of ways, this
+  // only deselects the facet if it is selected, and then cleans up the
+  // keyboard/mouse watchers that were created when the facet was first
+  // selected.
+  deselectFacet : function(e) {
+    if (e) e.preventDefault();
+    if (this.modes.selected == 'is') {
+      this.setMode('not', 'selected');
+      this.closeAutocomplete();
+      this.options.app.searchBox.removeFocus();
+    }
+    $(document).unbind('keydown.facet', this.keydown);
+    $(document).unbind('click.facet', this.deselectFacet);
+    return false;
+  },
+
+  // Is the user currently focused in this facet's input field?
+  isFocused : function() {
+    return this.box.is(':focus');
+  },
+
+  // Hovering over the delete button styles the facet so the user knows that
+  // the delete button will kill the entire facet.
+  showDelete : function() {
+    $(this.el).addClass('search_facet_maybe_delete');
+  },
+
+  // On `mouseout`, the user is no longer hovering on the delete button.
+  hideDelete : function() {
+    $(this.el).removeClass('search_facet_maybe_delete');
+  },
+
+  // When switching between facets, depending on the direction the cursor is
+  // coming from, the cursor in this facet's input field should match the original
+  // direction.
+  setCursorAtEnd : function(direction) {
+    if (direction == -1) {
+      this.box.setCursorPosition(this.box.val().length);
+    } else {
+      this.box.setCursorPosition(0);
+    }
+  },
+
+  // Deletes the facet and sends the cursor over to the nearest input field.
+  remove : function(e) {
+    var committed = this.model.get('value');
+    this.deselectFacet();
+    this.disableEdit();
+    this.options.app.searchQuery.remove(this.model);
+    if (committed) {
+      this.search(e, -1);
+    } else {
+      this.options.app.searchBox.renderFacets();
+      this.options.app.searchBox.focusNextFacet(this, -1, {viewPosition: this.options.order});
+    }
+  },
+
+  // Selects the text in the facet's input field. When the user tabs between
+  // facets, convention is to highlight the entire field.
+  selectText: function() {
+    this.box.selectRange(0, this.box.val().length);
+  },
+
+  // Handles all keyboard inputs when in the facet's input field. This checks
+  // for movement between facets and inputs, entering a new value that needs
+  // to be autocompleted, as well as the removal of this facet.
+  keydown : function(e) {
+    var key = VS.app.hotkeys.key(e);
+
+    if (key == 'enter' && this.box.val()) {
+      this.disableEdit();
+      this.search(e);
+    } else if (key == 'left') {
+      if (this.modes.selected == 'is') {
+        this.deselectFacet();
+        this.options.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
+      } else if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
+        this.selectFacet();
+      }
+    } else if (key == 'right') {
+      if (this.modes.selected == 'is') {
+        e.preventDefault();
+        this.deselectFacet();
+        this.setCursorAtEnd(0);
+        this.enableEdit();
+      } else if (this.box.getCursorPosition() == this.box.val().length) {
+        e.preventDefault();
+        this.disableEdit();
+        this.options.app.searchBox.focusNextFacet(this, 1);
+      }
+    } else if (VS.app.hotkeys.shift && key == 'tab') {
+      e.preventDefault();
+      this.options.app.searchBox.focusNextFacet(this, -1, {
+        startAtEnd  : -1,
+        skipToFacet : true,
+        selectText  : true
+      });
+    } else if (key == 'tab') {
+      e.preventDefault();
+      this.options.app.searchBox.focusNextFacet(this, 1, {
+        skipToFacet : true,
+        selectText  : true
+      });
+    } else if (VS.app.hotkeys.command && (e.which == 97 || e.which == 65)) {
+      e.preventDefault();
+      this.options.app.searchBox.selectAllFacets();
+      return false;
+    } else if (VS.app.hotkeys.printable(e) && this.modes.selected == 'is') {
+      this.options.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
+      this.remove(e);
+    } else if (key == 'backspace') {
+      if (this.modes.selected == 'is') {
+        e.preventDefault();
+        this.remove(e);
+      } else if (this.box.getCursorPosition() == 0 &&
+                 !this.box.getSelection().length) {
+        e.preventDefault();
+        this.selectFacet();
+      }
+    }
+
+    // Handle paste events
+    if (e.which == null) {
+        // this.searchAutocomplete(e);
+        _.defer(_.bind(this.resize, this, e));
+    } else {
+      this.resize(e);
+    }
+  }
+
+});
+
+})();
+
+(function() {
+
+var $ = jQuery; // Handle namespaced jQuery
+
+// This is the visual search input that is responsible for creating new facets.
+// There is one input placed in between all facets.
+VS.ui.SearchInput = Backbone.View.extend({
+
+  type : 'text',
+
+  className : 'search_input',
+
+  events : {
+    'keypress input'  : 'keypress',
+    'keydown input'   : 'keydown',
+    'click input'     : 'maybeTripleClick',
+    'dblclick input'  : 'startTripleClickTimer'
+  },
+
+  initialize : function() {
+    this.app = this.options.app;
+    this.flags = {
+      canClose : false
+    };
+    _.bindAll(this, 'removeFocus', 'addFocus', 'moveAutocomplete', 'deferDisableEdit');
+  },
+
+  // Rendering the input sets up autocomplete, events on focusing and blurring
+  // the input, and the auto-grow of the input.
+  render : function() {
+    $(this.el).html(JST['search_input']({}));
+
+    this.setMode('not', 'editing');
+    this.setMode('not', 'selected');
+    this.box = this.$('input');
+    this.box.autoGrowInput();
+    this.box.bind('updated.autogrow', this.moveAutocomplete);
+    this.box.bind('blur',  this.deferDisableEdit);
+    this.box.bind('focus', this.addFocus);
+    this.setupAutocomplete();
+
+    return this;
+  },
+
+  // Watches the input and presents an autocompleted menu, taking the
+  // remainder of the input field and adding a separate facet for it.
+  //
+  // See `addTextFacetRemainder` for explanation on how the remainder works.
+  setupAutocomplete : function() {
+    this.box.autocomplete({
+      minLength : this.options.showFacets ? 0 : 1,
+      delay     : 50,
+      autoFocus : true,
+      position  : {offset : "0 -1"},
+      source    : _.bind(this.autocompleteValues, this),
+      create    : _.bind(function(e, ui) {
+        $(this.el).find('.ui-autocomplete-input').css('z-index','auto');
+      }, this),
+      select    : _.bind(function(e, ui) {
+        e.preventDefault();
+        e.stopPropagation();
+        //var remainder = this.addTextFacetRemainder(ui.item.value);
+        var position  = this.options.position; // + (remainder ? 1 : 0);
+        this.app.searchBox.addFacet(ui.item instanceof String ? ui.item : ui.item.value, '', position);
+        return false;
+      }, this)
+    });
+
+    // Renders the results grouped by the categories they belong to.
+    this.box.data('autocomplete')._renderMenu = function(ul, items) {
+      var category = '';
+      _.each(items, _.bind(function(item, i) {
+        if (item.category && item.category != category) {
+          ul.append('<li class="ui-autocomplete-category">'+item.category+'</li>');
+          category = item.category;
+        }
+        this._renderItem(ul, item);
+      }, this));
+    };
+
+    this.box.autocomplete('widget').addClass('VS-interface');
+  },
+
+  // Search terms used in the autocomplete menu. The values are matched on the
+  // first letter of any word in matches, and finally sorted according to the
+  // value's own category. You can pass `preserveOrder` as an option in the
+  // `facetMatches` callback to skip any further ordering done client-side.
+  autocompleteValues : function(req, resp) {
+    var searchTerm = req.term;
+    var lastWord   = searchTerm.match(/\w+\*?$/); // Autocomplete only last word.
+    var re         = VS.utils.inflector.escapeRegExp(lastWord && lastWord[0] || '');
+    this.app.options.callbacks.facetMatches(function(prefixes, options) {
+      options = options || {};
+      prefixes = prefixes || [];
+
+      // Only match from the beginning of the word.
+      var matcher    = new RegExp('^' + re, 'i');
+      var matches    = $.grep(prefixes, function(item) {
+        return item && matcher.test(item.label || item);
+      });
+
+      if (options.preserveOrder) {
+        resp(matches);
+      } else {
+        resp(_.sortBy(matches, function(match) {
+          if (match.label) return match.category + '-' + match.label;
+          else             return match;
+        }));
+      }
+    });
+
+  },
+
+  // Closes the autocomplete menu. Called on disabling, selecting, deselecting,
+  // and anything else that takes focus out of the facet's input field.
+  closeAutocomplete : function() {
+    var autocomplete = this.box.data('autocomplete');
+    if (autocomplete) autocomplete.close();
+  },
+
+  // As the input field grows, it may move to the next line in the
+  // search box. `autoGrowInput` triggers an `updated` event on the input
+  // field, which is bound to this method to move the autocomplete menu.
+  moveAutocomplete : function() {
+    var autocomplete = this.box.data('autocomplete');
+    if (autocomplete) {
+      autocomplete.menu.element.position({
+        my        : "left top",
+        at        : "left bottom",
+        of        : this.box.data('autocomplete').element,
+        collision : "none",
+        offset    : '0 -1'
+      });
+    }
+  },
+
+  // When a user enters a facet and it is being edited, immediately show
+  // the autocomplete menu and size it to match the contents.
+  searchAutocomplete : function(e) {
+    var autocomplete = this.box.data('autocomplete');
+    if (autocomplete) {
+      var menu = autocomplete.menu.element;
+      autocomplete.search();
+
+      // Resize the menu based on the correctly measured width of what's bigger:
+      // the menu's original size or the menu items' new size.
+      menu.outerWidth(Math.max(
+        menu.width('').outerWidth(),
+        autocomplete.element.outerWidth()
+      ));
+    }
+  },
+
+  // If a user searches for "word word category", the category would be
+  // matched and autocompleted, and when selected, the "word word" would
+  // also be caught as the remainder and then added in its own facet.
+  addTextFacetRemainder : function(facetValue) {
+    var boxValue = this.box.val();
+    var lastWord = boxValue.match(/\b(\w+)$/);
+    
+    if (!lastWord) {
+      return '';
+    }
+
+    var matcher = new RegExp(lastWord[0], "i");
+    if (facetValue.search(matcher) == 0) {
+      boxValue = boxValue.replace(/\b(\w+)$/, '');
+    }
+    boxValue = boxValue.replace('^\s+|\s+$', '');
+    
+    if (boxValue) {
+      this.app.searchBox.addFacet(this.app.options.remainder, boxValue, this.options.position);
+    }
+    
+    return boxValue;
+  },
+
+  // Directly called to focus the input. This is different from `addFocus`
+  // because this is not called by a focus event. This instead calls a
+  // focus event causing the input to become focused.
+  enableEdit : function(selectText) {
+    this.addFocus();
+    if (selectText) {
+      this.selectText();
+    }
+    this.box.focus();
+  },
+
+  // Event called on user focus on the input. Tells all other input and facets
+  // to give up focus, and starts revving the autocomplete.
+  addFocus : function() {
+    this.flags.canClose = false;
+    if (!this.app.searchBox.allSelected()) {
+      this.app.searchBox.disableFacets(this);
+    }
+    this.app.searchBox.addFocus();
+    this.setMode('is', 'editing');
+    this.setMode('not', 'selected');
+    this.searchAutocomplete();
+  },
+
+  // Directly called to blur the input. This is different from `removeFocus`
+  // because this is not called by a blur event.
+  disableEdit : function() {
+    this.box.blur();
+    this.removeFocus();
+  },
+
+  // Event called when user blur's the input, either through the keyboard tabbing
+  // away or the mouse clicking off. Cleans up
+  removeFocus : function() {
+    this.flags.canClose = false;
+    this.app.searchBox.removeFocus();
+    this.setMode('not', 'editing');
+    this.setMode('not', 'selected');
+    this.closeAutocomplete();
+  },
+
+  // When the user blurs the input, they may either be going to another input
+  // or off the search box entirely. If they go to another input, this facet
+  // will be instantly disabled, and the canClose flag will be turned back off.
+  //
+  // However, if the user clicks elsewhere on the page, this method starts a timer
+  // that checks if any of the other inputs are selected or are being edited. If
+  // not, then it can finally close itself and its autocomplete menu.
+  deferDisableEdit : function() {
+    this.flags.canClose = true;
+    _.delay(_.bind(function() {
+      if (this.flags.canClose &&
+          !this.box.is(':focus') &&
+          this.modes.editing == 'is') {
+        this.disableEdit();
+      }
+    }, this), 250);
+  },
+
+  // Starts a timer that will cause a triple-click, which highlights all facets.
+  startTripleClickTimer : function() {
+    this.tripleClickTimer = setTimeout(_.bind(function() {
+      this.tripleClickTimer = null;
+    }, this), 500);
+  },
+
+  // Event on click that checks if a triple click is in play. The
+  // `tripleClickTimer` is counting down, ready to be engaged and intercept
+  // the click event to force a select all instead.
+  maybeTripleClick : function(e) {
+    if (!!this.tripleClickTimer) {
+      e.preventDefault();
+      this.app.searchBox.selectAllFacets();
+      return false;
+    }
+  },
+
+  // Is the user currently focused in the input field?
+  isFocused : function() {
+    return this.box.is(':focus');
+  },
+
+  // When serializing the facets, the inputs need to also have their values represented,
+  // in case they contain text that is not yet faceted (but will be once the search is
+  // completed).
+  value : function() {
+    return this.box.val();
+  },
+
+  // When switching between facets and inputs, depending on the direction the cursor
+  // is coming from, the cursor in this facet's input field should match the original
+  // direction.
+  setCursorAtEnd : function(direction) {
+    if (direction == -1) {
+      this.box.setCursorPosition(this.box.val().length);
+    } else {
+      this.box.setCursorPosition(0);
+    }
+  },
+
+  // Selects the entire range of text in the input. Useful when tabbing between inputs
+  // and facets.
+  selectText : function() {
+    this.box.selectRange(0, this.box.val().length);
+    if (!this.app.searchBox.allSelected()) {
+      this.box.focus();
+    } else {
+      this.setMode('is', 'selected');
+    }
+  },
+
+  // Before the searchBox performs a search, we need to close the
+  // autocomplete menu.
+  search : function(e, direction) {
+    if (!direction) direction = 0;
+    this.closeAutocomplete();
+    this.app.searchBox.searchEvent(e);
+    _.defer(_.bind(function() {
+      this.app.searchBox.focusNextFacet(this, direction);
+    }, this));
+  },
+
+  // Callback fired on key press in the search box. We search when they hit return.
+  keypress : function(e) {
+    var key = VS.app.hotkeys.key(e);
+
+    if (key == 'enter') {
+      return this.search(e, 100);
+    } else if (VS.app.hotkeys.colon(e)) {
+      this.box.trigger('resize.autogrow', e);
+      var query    = this.box.val();
+      var prefixes = [];
+      if (this.app.options.callbacks.facetMatches) {
+          this.app.options.callbacks.facetMatches(function(p) {
+              prefixes = p;
+          });
+      }
+      var labels   = _.map(prefixes, function(prefix) {
+        if (prefix.label) return prefix.label;
+        else              return prefix;
+      });
+      if (_.contains(labels, query)) {
+        e.preventDefault();
+        var remainder = this.addTextFacetRemainder(query);
+        var position  = this.options.position + (remainder?1:0);
+        this.app.searchBox.addFacet(query, '', position);
+        return false;
+      }
+    } else if (key == 'backspace') {
+      if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
+        e.preventDefault();
+        e.stopPropagation();
+        e.stopImmediatePropagation();
+        this.app.searchBox.resizeFacets();
+        return false;
+      }
+    }
+  },
+
+  // Handles all keyboard inputs when in the input field. This checks
+  // for movement between facets and inputs, entering a new value that needs
+  // to be autocompleted, as well as stepping between facets with backspace.
+  keydown : function(e) {
+    var key = VS.app.hotkeys.key(e);
+
+    if (key == 'left') {
+      if (this.box.getCursorPosition() == 0) {
+        e.preventDefault();
+        this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
+      }
+    } else if (key == 'right') {
+      if (this.box.getCursorPosition() == this.box.val().length) {
+        e.preventDefault();
+        this.app.searchBox.focusNextFacet(this, 1, {selectFacet: true});
+      }
+    } else if (VS.app.hotkeys.shift && key == 'tab') {
+      e.preventDefault();
+      this.app.searchBox.focusNextFacet(this, -1, {selectText: true});
+    } else if (key == 'tab') {
+      e.preventDefault();
+      var value = this.box.val();
+      if (value.length) {
+        var remainder = this.addTextFacetRemainder(value);
+        var position  = this.options.position + (remainder?1:0);
+        this.app.searchBox.addFacet(value, '', position);
+      } else {
+        this.app.searchBox.focusNextFacet(this, 0, {
+          skipToFacet: true,
+          selectText: true
+        });
+      }
+    } else if (VS.app.hotkeys.command &&
+               String.fromCharCode(e.which).toLowerCase() == 'a') {
+      e.preventDefault();
+      this.app.searchBox.selectAllFacets();
+      return false;
+    } else if (key == 'backspace' && !this.app.searchBox.allSelected()) {
+      if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
+        e.preventDefault();
+        this.app.searchBox.focusNextFacet(this, -1, {backspace: true});
+        return false;
+      }
+    } else if (key == 'end') {
+      var view = this.app.searchBox.inputViews[this.app.searchBox.inputViews.length-1];
+      view.setCursorAtEnd(-1);
+    } else if (key == 'home') {
+      var view = this.app.searchBox.inputViews[0];
+      view.setCursorAtEnd(-1);
+    }
+
+    this.box.trigger('resize.autogrow', e);
+  }
+
+});
+
+})();
+
+(function(){
+
+  var $ = jQuery; // Handle namespaced jQuery
+
+  // Makes the view enter a mode. Modes have both a 'mode' and a 'group',
+  // and are mutually exclusive with any other modes in the same group.
+  // Setting will update the view's modes hash, as well as set an HTML class
+  // of *[mode]_[group]* on the view's element. Convenient way to swap styles
+  // and behavior.
+  Backbone.View.prototype.setMode = function(mode, group) {
+    this.modes || (this.modes = {});
+    if (this.modes[group] === mode) return;
+    $(this.el).setMode(mode, group);
+    this.modes[group] = mode;
+  };
+
+})();
+(function() {
+
+var $ = jQuery; // Handle namespaced jQuery
+
+// DocumentCloud workspace hotkeys. To tell if a key is currently being pressed,
+// just ask `VS.app.hotkeys.[key]` on `keypress`, or ask `VS.app.hotkeys.key(e)`
+// on `keydown`.
+//
+// For the most headache-free way to use this utility, check modifier keys,
+// like shift and command, with `VS.app.hotkeys.shift`, and check every other
+// key with `VS.app.hotkeys.key(e) == 'key_name'`.
+VS.app.hotkeys = {
+
+  // Keys that will be mapped to the `hotkeys` namespace.
+  KEYS: {
+    '16':  'shift',
+    '17':  'command',
+    '91':  'command',
+    '93':  'command',
+    '224': 'command',
+    '13':  'enter',
+    '37':  'left',
+    '38':  'upArrow',
+    '39':  'right',
+    '40':  'downArrow',
+    '46':  'delete',
+    '8':   'backspace',
+    '35':  'end',
+    '36':  'home',
+    '9':   'tab',
+    '188': 'comma'
+  },
+
+  // Binds global keydown and keyup events to listen for keys that match `this.KEYS`.
+  initialize : function() {
+    _.bindAll(this, 'down', 'up', 'blur');
+    $(document).bind('keydown', this.down);
+    $(document).bind('keyup', this.up);
+    $(window).bind('blur', this.blur);
+  },
+
+  // On `keydown`, turn on all keys that match.
+  down : function(e) {
+    var key = this.KEYS[e.which];
+    if (key) this[key] = true;
+  },
+
+  // On `keyup`, turn off all keys that match.
+  up : function(e) {
+    var key = this.KEYS[e.which];
+    if (key) this[key] = false;
+  },
+
+  // If an input is blurred, all keys need to be turned off, since they are no longer
+  // able to modify the document.
+  blur : function(e) {
+    for (var key in this.KEYS) this[this.KEYS[key]] = false;
+  },
+
+  // Check a key from an event and return the common english name.
+  key : function(e) {
+    return this.KEYS[e.which];
+  },
+
+  // Colon is special, since the value is different between browsers.
+  colon : function(e) {
+    var charCode = e.which;
+    return charCode && String.fromCharCode(charCode) == ":";
+  },
+
+  // Check a key from an event and match it against any known characters.
+  // The `keyCode` is different depending on the event type: `keydown` vs. `keypress`.
+  //
+  // These were determined by looping through every `keyCode` and `charCode` that
+  // resulted from `keydown` and `keypress` events and counting what was printable.
+  printable : function(e) {
+    var code = e.which;
+    if (e.type == 'keydown') {
+      if (code == 32 ||                      // space
+          (code >= 48 && code <= 90) ||      // 0-1a-z
+          (code >= 96 && code <= 111) ||     // 0-9+-/*.
+          (code >= 186 && code <= 192) ||    // ;=,-./^
+          (code >= 219 && code <= 222)) {    // (\)'
+        return true;
+      }
+    } else {
+      // [space]!"#$%&'()*+,-.0-9:;<=>?@A-Z[\]^_`a-z{|} and unicode characters
+      if ((code >= 32 && code <= 126)  ||
+          (code >= 160 && code <= 500) ||
+          (String.fromCharCode(code) == ":")) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+};
+
+})();
+(function() {
+
+var $ = jQuery; // Handle namespaced jQuery
+
+// Naive English transformations on words. Only used for a few transformations 
+// in VisualSearch.js.
+VS.utils.inflector = {
+
+  // Delegate to the ECMA5 String.prototype.trim function, if available.
+  trim : function(s) {
+    return s.trim ? s.trim() : s.replace(/^\s+|\s+$/g, '');
+  },
+  
+  // Escape strings that are going to be used in a regex. Escapes punctuation
+  // that would be incorrect in a regex.
+  escapeRegExp : function(s) {
+    return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
+  }
+};
+
+})();
+(function() {
+
+var $ = jQuery; // Handle namespaced jQuery
+
+$.fn.extend({
+
+  // Makes the selector enter a mode. Modes have both a 'mode' and a 'group',
+  // and are mutually exclusive with any other modes in the same group.
+  // Setting will update the view's modes hash, as well as set an HTML class
+  // of *[mode]_[group]* on the view's element. Convenient way to swap styles
+  // and behavior.
+  setMode : function(state, group) {
+    group    = group || 'mode';
+    var re   = new RegExp("\\w+_" + group + "(\\s|$)", 'g');
+    var mode = (state === null) ? "" : state + "_" + group;
+    this.each(function() {
+      this.className = (this.className.replace(re, '')+' '+mode)
+                       .replace(/\s\s/g, ' ');
+    });
+    return mode;
+  },
+
+  // When attached to an input element, this will cause the width of the input
+  // to match its contents. This calculates the width of the contents of the input
+  // by measuring a hidden shadow div that should match the styling of the input.
+  autoGrowInput: function() {
+    return this.each(function() {
+      var $input  = $(this);
+      var $tester = $('<div />').css({
+        opacity     : 0,
+        top         : -9999,
+        left        : -9999,
+        position    : 'absolute',
+        whiteSpace  : 'nowrap'
+      }).addClass('VS-input-width-tester').addClass('VS-interface');
+
+      // Watch for input value changes on all of these events. `resize`
+      // event is called explicitly when the input has been changed without
+      // a single keypress.
+      var events = 'keydown.autogrow keypress.autogrow ' +
+                   'resize.autogrow change.autogrow';
+      $input.next('.VS-input-width-tester').remove();
+      $input.after($tester);
+      $input.unbind(events).bind(events, function(e, realEvent) {
+        if (realEvent) e = realEvent;
+        var value = $input.val();
+
+        // Watching for the backspace key is tricky because it may not
+        // actually be deleting the character, but instead the key gets
+        // redirected to move the cursor from facet to facet.
+        if (VS.app.hotkeys.key(e) == 'backspace') {
+          var position = $input.getCursorPosition();
+          if (position > 0) value = value.slice(0, position-1) +
+                                    value.slice(position, value.length);
+        } else if (VS.app.hotkeys.printable(e) &&
+                   !VS.app.hotkeys.command) {
+          value += String.fromCharCode(e.which);
+        }
+        value = value.replace(/&/g, '&amp;')
+                     .replace(/\s/g,'&nbsp;')
+                     .replace(/</g, '&lt;')
+                     .replace(/>/g, '&gt;');
+
+        $tester.html(value);
+
+        $input.width($tester.width() + 3 + parseInt($input.css('min-width')));
+        $input.trigger('updated.autogrow');
+      });
+
+      // Sets the width of the input on initialization.
+      $input.trigger('resize.autogrow');
+    });
+  },
+
+
+  // Cross-browser method used for calculating where the cursor is in an
+  // input field.
+  getCursorPosition: function() {
+    var position = 0;
+    var input    = this.get(0);
+
+    if (document.selection) { // IE
+      input.focus();
+      var sel    = document.selection.createRange();
+      var selLen = document.selection.createRange().text.length;
+      sel.moveStart('character', -input.value.length);
+      position   = sel.text.length - selLen;
+    } else if (input && $(input).is(':visible') &&
+               input.selectionStart != null) { // Firefox/Safari
+      position = input.selectionStart;
+    }
+
+    return position;
+  },
+
+  // A simple proxy for `selectRange` that sets the cursor position in an
+  // input field.
+  setCursorPosition: function(position) {
+    return this.each(function() {
+      return $(this).selectRange(position, position);
+    });
+  },
+
+  // Cross-browser way to select text in an input field.
+  selectRange: function(start, end) {
+    return this.filter(':visible').each(function() {
+      if (this.setSelectionRange) { // FF/Webkit
+        this.focus();
+        if ($(this).is(":visible")) {
+          this.setSelectionRange(start, end);
+        }
+      } else if (this.createTextRange) { // IE
+        var range = this.createTextRange();
+        range.collapse(true);
+        range.moveEnd('character', end);
+        range.moveStart('character', start);
+        if (end - start >= 0) range.select();
+      }
+    });
+  },
+
+  // Returns an object that contains the text selection range values for
+  // an input field.
+  getSelection: function() {
+    var input = this[0];
+
+    if (input.selectionStart != null) { // FF/Webkit
+      var start = input.selectionStart;
+      var end   = input.selectionEnd;
+      return {
+        start   : start,
+        end     : end,
+        length  : end-start,
+        text    : input.value.substr(start, end-start)
+      };
+    } else if (document.selection) { // IE
+      var range = document.selection.createRange();
+      if (range) {
+        var textRange = input.createTextRange();
+        var copyRange = textRange.duplicate();
+        textRange.moveToBookmark(range.getBookmark());
+        copyRange.setEndPoint('EndToStart', textRange);
+        var start = copyRange.text.length;
+        var end   = start + range.text.length;
+        return {
+          start   : start,
+          end     : end,
+          length  : end-start,
+          text    : range.text
+        };
+      }
+    }
+    return {start: 0, end: 0, length: 0};
+  }
+
+});
+
+// Debugging in Internet Explorer. This allows you to use 
+// `console.log(['message', var1, var2, ...])`. Just remove the `false` and
+// add your console.logs. This will automatically stringify objects using
+// `JSON.stringify', so you can read what's going out. Think of this as a
+// *Diet Firebug Lite Zero with Lemon*.
+if ($.browser.msie && false) {
+  window.console = {};
+  var _$ied;
+  window.console.log = function(msg) {
+    if (_.isArray(msg)) {
+      var message = msg[0];
+      var vars = _.map(msg.slice(1), function(arg) {
+        return JSON.stringify(arg);
+      }).join(' - ');
+    }
+    if(!_$ied){
+      _$ied = $('<div><ol></ol></div>').css({
+        'position': 'fixed',
+        'bottom': 10,
+        'left': 10,
+        'zIndex': 20000,
+        'width': $('body').width() - 80,
+        'border': '1px solid #000',
+        'padding': '10px',
+        'backgroundColor': '#fff',
+        'fontFamily': 'arial,helvetica,sans-serif',
+        'fontSize': '11px'
+      });
+      $('body').append(_$ied);
+    }
+    var $message = $('<li>'+message+' - '+vars+'</li>').css({
+      'borderBottom': '1px solid #999999'
+    });
+    _$ied.find('ol').append($message);
+    _.delay(function() {
+      $message.fadeOut(500);
+    }, 5000);
+  };
+
+}
+
+})();
+
+(function() {
+
+var $ = jQuery; // Handle namespaced jQuery
+
+// Used to extract keywords and facets from the free text search.
+var QUOTES_RE   = "('[^']+'|\"[^\"]+\")";
+var FREETEXT_RE = "('[^']+'|\"[^\"]+\"|[^'\"\\s]\\S*)";
+var CATEGORY_RE = FREETEXT_RE +                     ':\\s*';
+VS.app.SearchParser = {
+
+  // Matches `category: "free text"`, with and without quotes.
+  ALL_FIELDS : new RegExp(CATEGORY_RE + FREETEXT_RE, 'g'),
+
+  // Matches a single category without the text. Used to correctly extract facets.
+  CATEGORY   : new RegExp(CATEGORY_RE),
+
+  // Called to parse a query into a collection of `SearchFacet` models.
+  parse : function(instance, query) {
+    var searchFacets = this._extractAllFacets(instance, query);
+    instance.searchQuery.reset(searchFacets);
+    return searchFacets;
+  },
+
+  // Walks the query and extracts facets, categories, and free text.
+  _extractAllFacets : function(instance, query) {
+    var facets = [];
+    var originalQuery = query;
+
+    while (query) {
+      var category, value;
+      originalQuery = query;
+      var field = this._extractNextField(query);
+      if (!field) {
+        category = instance.options.remainder;
+        value    = this._extractSearchText(query);
+        query    = VS.utils.inflector.trim(query.replace(value, ''));
+      } else if (field.indexOf(':') != -1) {
+        category = field.match(this.CATEGORY)[1].replace(/(^['"]|['"]$)/g, '');
+        value    = field.replace(this.CATEGORY, '').replace(/(^['"]|['"]$)/g, '');
+        query    = VS.utils.inflector.trim(query.replace(field, ''));
+      } else if (field.indexOf(':') == -1) {
+        category = instance.options.remainder;
+        value    = field;
+        query    = VS.utils.inflector.trim(query.replace(value, ''));
+      }
+
+      if (category && value) {
+          var searchFacet = new VS.model.SearchFacet({
+            category : category,
+            value    : VS.utils.inflector.trim(value),
+            app      : instance
+          });
+          facets.push(searchFacet);
+      }
+      if (originalQuery == query) break;
+    }
+
+    return facets;
+  },
+
+  // Extracts the first field found, capturing any free text that comes
+  // before the category.
+  _extractNextField : function(query) {
+    var textRe = new RegExp('^\\s*(\\S+)\\s+(?=' + QUOTES_RE + FREETEXT_RE + ')');
+    var textMatch = query.match(textRe);
+    if (textMatch && textMatch.length >= 1) {
+      return textMatch[1];
+    } else {
+      return this._extractFirstField(query);
+    }
+  },
+
+  // If there is no free text before the facet, extract the category and value.
+  _extractFirstField : function(query) {
+    var fields = query.match(this.ALL_FIELDS);
+    return fields && fields.length && fields[0];
+  },
+
+  // If the found match is not a category and facet, extract the trimmed free text.
+  _extractSearchText : function(query) {
+    query = query || '';
+    var text = VS.utils.inflector.trim(query.replace(this.ALL_FIELDS, ''));
+    return text;
+  }
+
+};
+
+})();
+
+(function() {
+
+var $ = jQuery; // Handle namespaced jQuery
+
+// The model that holds individual search facets and their categories.
+// Held in a collection by `VS.app.searchQuery`.
+VS.model.SearchFacet = Backbone.Model.extend({
+
+  // Extract the category and value and serialize it in preparation for
+  // turning the entire searchBox into a search query that can be sent
+  // to the server for parsing and searching.
+  serialize : function() {
+    var category = this.quoteCategory(this.get('category'));
+    var value    = VS.utils.inflector.trim(this.get('value'));
+    var remainder = this.get("app").options.remainder;
+
+    if (!value) return '';
+
+    if (!_.contains(this.get("app").options.unquotable || [], category) && category != remainder) {
+      value = this.quoteValue(value);
+    }
+
+    if (category != remainder) {
+      category = category + ': ';
+    } else {
+      category = "";
+    }
+    return category + value;
+  },
+  
+  // Wrap categories that have spaces or any kind of quote with opposite matching
+  // quotes to preserve the complex category during serialization.
+  quoteCategory : function(category) {
+    var hasDoubleQuote = (/"/).test(category);
+    var hasSingleQuote = (/'/).test(category);
+    var hasSpace       = (/\s/).test(category);
+    
+    if (hasDoubleQuote && !hasSingleQuote) {
+      return "'" + category + "'";
+    } else if (hasSpace || (hasSingleQuote && !hasDoubleQuote)) {
+      return '"' + category + '"';
+    } else {
+      return category;
+    }
+  },
+  
+  // Wrap values that have quotes in opposite matching quotes. If a value has
+  // both single and double quotes, just use the double quotes.
+  quoteValue : function(value) {
+    var hasDoubleQuote = (/"/).test(value);
+    var hasSingleQuote = (/'/).test(value);
+    
+    if (hasDoubleQuote && !hasSingleQuote) {
+      return "'" + value + "'";
+    } else {
+      return '"' + value + '"';
+    }
+  }
+
+});
+
+})();
+(function() {
+
+var $ = jQuery; // Handle namespaced jQuery
+
+// Collection which holds all of the individual facets (category: value).
+// Used for finding and removing specific facets.
+VS.model.SearchQuery = Backbone.Collection.extend({
+
+  // Model holds the category and value of the facet.
+  model : VS.model.SearchFacet,
+
+  // Turns all of the facets into a single serialized string.
+  serialize : function() {
+    return this.map(function(facet){ return facet.serialize(); }).join(' ');
+  },
+  
+  facets : function() {
+    return this.map(function(facet) {
+      var value = {};
+      value[facet.get('category')] = facet.get('value');
+      return value;
+    });
+  },
+
+  // Find a facet by its category. Multiple facets with the same category
+  // is fine, but only the first is returned.
+  find : function(category) {
+    var facet = this.detect(function(facet) {
+      return facet.get('category').toLowerCase() == category.toLowerCase();
+    });
+    return facet && facet.get('value');
+  },
+
+  // Counts the number of times a specific category is in the search query.
+  count : function(category) {
+    return this.select(function(facet) {
+      return facet.get('category').toLowerCase() == category.toLowerCase();
+    }).length;
+  },
+
+  // Returns an array of extracted values from each facet in a category.
+  values : function(category) {
+    var facets = this.select(function(facet) {
+      return facet.get('category').toLowerCase() == category.toLowerCase();
+    });
+    return _.map(facets, function(facet) { return facet.get('value'); });
+  },
+
+  // Checks all facets for matches of either a category or both category and value.
+  has : function(category, value) {
+    return this.any(function(facet) {
+      var categoryMatched = facet.get('category').toLowerCase() == category.toLowerCase();
+      if (!value) return categoryMatched;
+      return categoryMatched && facet.get('value') == value;
+    });
+  },
+
+  // Used to temporarily hide specific categories and serialize the search query.
+  withoutCategory : function() {
+    var categories = _.map(_.toArray(arguments), function(cat) { return cat.toLowerCase(); });
+    return this.map(function(facet) {
+      if (!_.include(categories, facet.get('category').toLowerCase())) { 
+        return facet.serialize();
+      };
+    }).join(' ');
+  }
+
+});
+
+})();
+(function(){
+window.JST = window.JST || {};
+
+window.JST['search_box'] = _.template('<div class="VS-search">\n  <div class="VS-search-box-wrapper VS-search-box">\n    <div class="VS-icon VS-icon-search"></div>\n    <div class="VS-search-inner"></div>\n    <div class="VS-icon VS-icon-cancel VS-cancel-search-box" title="clear search"></div>\n  </div>\n</div>');
+window.JST['search_facet'] = _.template('<% if (model.has(\'category\')) { %>\n  <div class="category"><%= model.get(\'category\') %>:</div>\n<% } %>\n\n<div class="search_facet_input_container">\n  <input type="text" class="search_facet_input VS-interface" value="" />\n</div>\n\n<div class="search_facet_remove VS-icon VS-icon-cancel"></div>');
+window.JST['search_input'] = _.template('<input type="text" />');
+})();
diff --git a/l10n_community/visualsearch/vendor/backbone-0.5.0.js b/l10n_community/visualsearch/vendor/backbone-0.5.0.js
new file mode 100644
index 0000000..5f8ea5c
--- /dev/null
+++ b/l10n_community/visualsearch/vendor/backbone-0.5.0.js
@@ -0,0 +1,1152 @@
+//     Backbone.js 0.5.0-pre
+//     (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
+//     Backbone may be freely distributed under the MIT license.
+//     For all details and documentation:
+//     http://documentcloud.github.com/backbone
+
+(function(){
+
+  // Initial Setup
+  // -------------
+
+  // Save a reference to the global object.
+  var root = this;
+
+  // Save the previous value of the `Backbone` variable.
+  var previousBackbone = root.Backbone;
+
+  // The top-level namespace. All public Backbone classes and modules will
+  // be attached to this. Exported for both CommonJS and the browser.
+  var Backbone;
+  if (typeof exports !== 'undefined') {
+    Backbone = exports;
+  } else {
+    Backbone = root.Backbone = {};
+  }
+
+  // Current version of the library. Keep in sync with `package.json`.
+  Backbone.VERSION = '0.5.0-pre';
+
+  // Require Underscore, if we're on the server, and it's not already present.
+  var _ = root._;
+  if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
+
+  // For Backbone's purposes, jQuery or Zepto owns the `$` variable.
+  var $ = root.jQuery || root.Zepto;
+
+  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
+  // to its previous owner. Returns a reference to this Backbone object.
+  Backbone.noConflict = function() {
+    root.Backbone = previousBackbone;
+    return this;
+  };
+
+  // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will
+  // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
+  // `X-Http-Method-Override` header.
+  Backbone.emulateHTTP = false;
+
+  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
+  // `application/json` requests ... will encode the body as
+  // `application/x-www-form-urlencoded` instead and will send the model in a
+  // form param named `model`.
+  Backbone.emulateJSON = false;
+
+  // Backbone.Events
+  // -----------------
+
+  // A module that can be mixed in to *any object* in order to provide it with
+  // custom events. You may `bind` or `unbind` a callback function to an event;
+  // `trigger`-ing an event fires all callbacks in succession.
+  //
+  //     var object = {};
+  //     _.extend(object, Backbone.Events);
+  //     object.bind('expand', function(){ alert('expanded'); });
+  //     object.trigger('expand');
+  //
+  Backbone.Events = {
+
+    // Bind an event, specified by a string name, `ev`, to a `callback` function.
+    // Passing `"all"` will bind the callback to all events fired.
+    bind : function(ev, callback) {
+      var calls = this._callbacks || (this._callbacks = {});
+      var list  = calls[ev] || (calls[ev] = []);
+      list.push(callback);
+      return this;
+    },
+
+    // Remove one or many callbacks. If `callback` is null, removes all
+    // callbacks for the event. If `ev` is null, removes all bound callbacks
+    // for all events.
+    unbind : function(ev, callback) {
+      var calls;
+      if (!ev) {
+        this._callbacks = {};
+      } else if (calls = this._callbacks) {
+        if (!callback) {
+          calls[ev] = [];
+        } else {
+          var list = calls[ev];
+          if (!list) return this;
+          for (var i = 0, l = list.length; i < l; i++) {
+            if (callback === list[i]) {
+              list[i] = null;
+              break;
+            }
+          }
+        }
+      }
+      return this;
+    },
+
+    // Trigger an event, firing all bound callbacks. Callbacks are passed the
+    // same arguments as `trigger` is, apart from the event name.
+    // Listening for `"all"` passes the true event name as the first argument.
+    trigger : function(eventName) {
+      var list, calls, ev, callback, args;
+      var both = 2;
+      if (!(calls = this._callbacks)) return this;
+      while (both--) {
+        ev = both ? eventName : 'all';
+        if (list = calls[ev]) {
+          for (var i = 0, l = list.length; i < l; i++) {
+            if (!(callback = list[i])) {
+              list.splice(i, 1); i--; l--;
+            } else {
+              args = both ? Array.prototype.slice.call(arguments, 1) : arguments;
+              callback.apply(this, args);
+            }
+          }
+        }
+      }
+      return this;
+    }
+
+  };
+
+  // Backbone.Model
+  // --------------
+
+  // Create a new model, with defined attributes. A client id (`cid`)
+  // is automatically generated and assigned for you.
+  Backbone.Model = function(attributes, options) {
+    var defaults;
+    attributes || (attributes = {});
+    if (defaults = this.defaults) {
+      if (_.isFunction(defaults)) defaults = defaults();
+      attributes = _.extend({}, defaults, attributes);
+    }
+    this.attributes = {};
+    this._escapedAttributes = {};
+    this.cid = _.uniqueId('c');
+    this.set(attributes, {silent : true});
+    this._changed = false;
+    this._previousAttributes = _.clone(this.attributes);
+    if (options && options.collection) this.collection = options.collection;
+    this.initialize.apply(this, arguments);
+  };
+
+  // Attach all inheritable methods to the Model prototype.
+  _.extend(Backbone.Model.prototype, Backbone.Events, {
+
+    // A snapshot of the model's previous attributes, taken immediately
+    // after the last `"change"` event was fired.
+    _previousAttributes : null,
+
+    // Has the item been changed since the last `"change"` event?
+    _changed : false,
+
+    // The default name for the JSON `id` attribute is `"id"`. MongoDB and
+    // CouchDB users may want to set this to `"_id"`.
+    idAttribute : 'id',
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize : function(){},
+
+    // Return a copy of the model's `attributes` object.
+    toJSON : function() {
+      return _.clone(this.attributes);
+    },
+
+    // Get the value of an attribute.
+    get : function(attr) {
+      return this.attributes[attr];
+    },
+
+    // Get the HTML-escaped value of an attribute.
+    escape : function(attr) {
+      var html;
+      if (html = this._escapedAttributes[attr]) return html;
+      var val = this.attributes[attr];
+      return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val);
+    },
+
+    // Returns `true` if the attribute contains a value that is not null
+    // or undefined.
+    has : function(attr) {
+      return this.attributes[attr] != null;
+    },
+
+    // Set a hash of model attributes on the object, firing `"change"` unless you
+    // choose to silence it.
+    set : function(attrs, options) {
+
+      // Extract attributes and options.
+      options || (options = {});
+      if (!attrs) return this;
+      if (attrs.attributes) attrs = attrs.attributes;
+      var now = this.attributes, escaped = this._escapedAttributes;
+
+      // Run validation.
+      if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
+
+      // Check for changes of `id`.
+      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
+
+      // We're about to start triggering change events.
+      var alreadyChanging = this._changing;
+      this._changing = true;
+
+      // Update attributes.
+      for (var attr in attrs) {
+        var val = attrs[attr];
+        if (!_.isEqual(now[attr], val)) {
+          now[attr] = val;
+          delete escaped[attr];
+          this._changed = true;
+          if (!options.silent) this.trigger('change:' + attr, this, val, options);
+        }
+      }
+
+      // Fire the `"change"` event, if the model has been changed.
+      if (!alreadyChanging && !options.silent && this._changed) this.change(options);
+      this._changing = false;
+      return this;
+    },
+
+    // Remove an attribute from the model, firing `"change"` unless you choose
+    // to silence it. `unset` is a noop if the attribute doesn't exist.
+    unset : function(attr, options) {
+      if (!(attr in this.attributes)) return this;
+      options || (options = {});
+      var value = this.attributes[attr];
+
+      // Run validation.
+      var validObj = {};
+      validObj[attr] = void 0;
+      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
+
+      // Remove the attribute.
+      delete this.attributes[attr];
+      delete this._escapedAttributes[attr];
+      if (attr == this.idAttribute) delete this.id;
+      this._changed = true;
+      if (!options.silent) {
+        this.trigger('change:' + attr, this, void 0, options);
+        this.change(options);
+      }
+      return this;
+    },
+
+    // Clear all attributes on the model, firing `"change"` unless you choose
+    // to silence it.
+    clear : function(options) {
+      options || (options = {});
+      var old = this.attributes;
+
+      // Run validation.
+      var validObj = {};
+      for (var attr in old) validObj[attr] = void 0;
+      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
+
+      this.attributes = {};
+      this._escapedAttributes = {};
+      this._changed = true;
+      if (!options.silent) {
+        for (var attr in old) {
+          this.trigger('change:' + attr, this, void 0, options);
+        }
+        this.change(options);
+      }
+      return this;
+    },
+
+    // Fetch the model from the server. If the server's representation of the
+    // model differs from its current attributes, they will be overriden,
+    // triggering a `"change"` event.
+    fetch : function(options) {
+      options || (options = {});
+      var model = this;
+      var success = options.success;
+      options.success = function(resp, status, xhr) {
+        if (!model.set(model.parse(resp, xhr), options)) return false;
+        if (success) success(model, resp);
+      };
+      options.error = wrapError(options.error, model, options);
+      return (this.sync || Backbone.sync).call(this, 'read', this, options);
+    },
+
+    // Set a hash of model attributes, and sync the model to the server.
+    // If the server returns an attributes hash that differs, the model's
+    // state will be `set` again.
+    save : function(attrs, options) {
+      options || (options = {});
+      if (attrs && !this.set(attrs, options)) return false;
+      var model = this;
+      var success = options.success;
+      options.success = function(resp, status, xhr) {
+        if (!model.set(model.parse(resp, xhr), options)) return false;
+        if (success) success(model, resp, xhr);
+      };
+      options.error = wrapError(options.error, model, options);
+      var method = this.isNew() ? 'create' : 'update';
+      return (this.sync || Backbone.sync).call(this, method, this, options);
+    },
+
+    // Destroy this model on the server if it was already persisted. Upon success, the model is removed
+    // from its collection, if it has one.
+    destroy : function(options) {
+      options || (options = {});
+      if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
+      var model = this;
+      var success = options.success;
+      options.success = function(resp) {
+        model.trigger('destroy', model, model.collection, options);
+        if (success) success(model, resp);
+      };
+      options.error = wrapError(options.error, model, options);
+      return (this.sync || Backbone.sync).call(this, 'delete', this, options);
+    },
+
+    // Default URL for the model's representation on the server -- if you're
+    // using Backbone's restful methods, override this to change the endpoint
+    // that will be called.
+    url : function() {
+      var base = getUrl(this.collection) || this.urlRoot || urlError();
+      if (this.isNew()) return base;
+      return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
+    },
+
+    // **parse** converts a response into the hash of attributes to be `set` on
+    // the model. The default implementation is just to pass the response along.
+    parse : function(resp, xhr) {
+      return resp;
+    },
+
+    // Create a new model with identical attributes to this one.
+    clone : function() {
+      return new this.constructor(this);
+    },
+
+    // A model is new if it has never been saved to the server, and lacks an id.
+    isNew : function() {
+      return this.id == null;
+    },
+
+    // Call this method to manually fire a `change` event for this model.
+    // Calling this will cause all objects observing the model to update.
+    change : function(options) {
+      this.trigger('change', this, options);
+      this._previousAttributes = _.clone(this.attributes);
+      this._changed = false;
+    },
+
+    // Determine if the model has changed since the last `"change"` event.
+    // If you specify an attribute name, determine if that attribute has changed.
+    hasChanged : function(attr) {
+      if (attr) return this._previousAttributes[attr] != this.attributes[attr];
+      return this._changed;
+    },
+
+    // Return an object containing all the attributes that have changed, or false
+    // if there are no changed attributes. Useful for determining what parts of a
+    // view need to be updated and/or what attributes need to be persisted to
+    // the server.
+    changedAttributes : function(now) {
+      now || (now = this.attributes);
+      var old = this._previousAttributes;
+      var changed = false;
+      for (var attr in now) {
+        if (!_.isEqual(old[attr], now[attr])) {
+          changed = changed || {};
+          changed[attr] = now[attr];
+        }
+      }
+      return changed;
+    },
+
+    // Get the previous value of an attribute, recorded at the time the last
+    // `"change"` event was fired.
+    previous : function(attr) {
+      if (!attr || !this._previousAttributes) return null;
+      return this._previousAttributes[attr];
+    },
+
+    // Get all of the attributes of the model at the time of the previous
+    // `"change"` event.
+    previousAttributes : function() {
+      return _.clone(this._previousAttributes);
+    },
+
+    // Run validation against a set of incoming attributes, returning `true`
+    // if all is well. If a specific `error` callback has been passed,
+    // call that instead of firing the general `"error"` event.
+    _performValidation : function(attrs, options) {
+      var error = this.validate(attrs);
+      if (error) {
+        if (options.error) {
+          options.error(this, error, options);
+        } else {
+          this.trigger('error', this, error, options);
+        }
+        return false;
+      }
+      return true;
+    }
+
+  });
+
+  // Backbone.Collection
+  // -------------------
+
+  // Provides a standard collection class for our sets of models, ordered
+  // or unordered. If a `comparator` is specified, the Collection will maintain
+  // its models in sort order, as they're added and removed.
+  Backbone.Collection = function(models, options) {
+    options || (options = {});
+    if (options.comparator) this.comparator = options.comparator;
+    _.bindAll(this, '_onModelEvent', '_removeReference');
+    this._reset();
+    if (models) this.reset(models, {silent: true});
+    this.initialize.apply(this, arguments);
+  };
+
+  // Define the Collection's inheritable methods.
+  _.extend(Backbone.Collection.prototype, Backbone.Events, {
+
+    // The default model for a collection is just a **Backbone.Model**.
+    // This should be overridden in most cases.
+    model : Backbone.Model,
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize : function(){},
+
+    // The JSON representation of a Collection is an array of the
+    // models' attributes.
+    toJSON : function() {
+      return this.map(function(model){ return model.toJSON(); });
+    },
+
+    // Add a model, or list of models to the set. Pass **silent** to avoid
+    // firing the `added` event for every new model.
+    add : function(models, options) {
+      if (_.isArray(models)) {
+        for (var i = 0, l = models.length; i < l; i++) {
+          this._add(models[i], options);
+        }
+      } else {
+        this._add(models, options);
+      }
+      return this;
+    },
+
+    // Remove a model, or a list of models from the set. Pass silent to avoid
+    // firing the `removed` event for every model removed.
+    remove : function(models, options) {
+      if (_.isArray(models)) {
+        for (var i = 0, l = models.length; i < l; i++) {
+          this._remove(models[i], options);
+        }
+      } else {
+        this._remove(models, options);
+      }
+      return this;
+    },
+
+    // Get a model from the set by id.
+    get : function(id) {
+      if (id == null) return null;
+      return this._byId[id.id != null ? id.id : id];
+    },
+
+    // Get a model from the set by client id.
+    getByCid : function(cid) {
+      return cid && this._byCid[cid.cid || cid];
+    },
+
+    // Get the model at the given index.
+    at: function(index) {
+      return this.models[index];
+    },
+
+    // Force the collection to re-sort itself. You don't need to call this under normal
+    // circumstances, as the set will maintain sort order as each item is added.
+    sort : function(options) {
+      options || (options = {});
+      if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
+      this.models = this.sortBy(this.comparator);
+      if (!options.silent) this.trigger('reset', this, options);
+      return this;
+    },
+
+    // Pluck an attribute from each model in the collection.
+    pluck : function(attr) {
+      return _.map(this.models, function(model){ return model.get(attr); });
+    },
+
+    // When you have more items than you want to add or remove individually,
+    // you can reset the entire set with a new list of models, without firing
+    // any `added` or `removed` events. Fires `reset` when finished.
+    reset : function(models, options) {
+      models  || (models = []);
+      options || (options = {});
+      this.each(this._removeReference);
+      this._reset();
+      this.add(models, {silent: true});
+      if (!options.silent) this.trigger('reset', this, options);
+      return this;
+    },
+
+    // Fetch the default set of models for this collection, resetting the
+    // collection when they arrive. If `add: true` is passed, appends the
+    // models to the collection instead of resetting.
+    fetch : function(options) {
+      options || (options = {});
+      var collection = this;
+      var success = options.success;
+      options.success = function(resp, status, xhr) {
+        collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
+        if (success) success(collection, resp);
+      };
+      options.error = wrapError(options.error, collection, options);
+      return (this.sync || Backbone.sync).call(this, 'read', this, options);
+    },
+
+    // Create a new instance of a model in this collection. After the model
+    // has been created on the server, it will be added to the collection.
+    // Returns the model, or 'false' if validation on a new model fails.
+    create : function(model, options) {
+      var coll = this;
+      options || (options = {});
+      model = this._prepareModel(model, options);
+      if (!model) return false;
+      var success = options.success;
+      options.success = function(nextModel, resp, xhr) {
+        coll.add(nextModel, options);
+        if (success) success(nextModel, resp, xhr);
+      };
+      model.save(null, options);
+      return model;
+    },
+
+    // **parse** converts a response into a list of models to be added to the
+    // collection. The default implementation is just to pass it through.
+    parse : function(resp, xhr) {
+      return resp;
+    },
+
+    // Proxy to _'s chain. Can't be proxied the same way the rest of the
+    // underscore methods are proxied because it relies on the underscore
+    // constructor.
+    chain: function () {
+      return _(this.models).chain();
+    },
+
+    // Reset all internal state. Called when the collection is refreshed.
+    _reset : function(options) {
+      this.length = 0;
+      this.models = [];
+      this._byId  = {};
+      this._byCid = {};
+    },
+
+    // Prepare a model to be added to this collection
+    _prepareModel: function(model, options) {
+      if (!(model instanceof Backbone.Model)) {
+        var attrs = model;
+        model = new this.model(attrs, {collection: this});
+        if (model.validate && !model._performValidation(attrs, options)) model = false;
+      } else if (!model.collection) {
+        model.collection = this;
+      }
+      return model;
+    },
+
+    // Internal implementation of adding a single model to the set, updating
+    // hash indexes for `id` and `cid` lookups.
+    // Returns the model, or 'false' if validation on a new model fails.
+    _add : function(model, options) {
+      options || (options = {});
+      model = this._prepareModel(model, options);
+      if (!model) return false;
+      var already = this.getByCid(model) || this.get(model);
+      if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
+      this._byId[model.id] = model;
+      this._byCid[model.cid] = model;
+      var index = options.at != null ? options.at :
+                  this.comparator ? this.sortedIndex(model, this.comparator) :
+                  this.length;
+      this.models.splice(index, 0, model);
+      model.bind('all', this._onModelEvent);
+      this.length++;
+      if (!options.silent) model.trigger('add', model, this, options);
+      return model;
+    },
+
+    // Internal implementation of removing a single model from the set, updating
+    // hash indexes for `id` and `cid` lookups.
+    _remove : function(model, options) {
+      options || (options = {});
+      model = this.getByCid(model) || this.get(model);
+      if (!model) return null;
+      delete this._byId[model.id];
+      delete this._byCid[model.cid];
+      this.models.splice(this.indexOf(model), 1);
+      this.length--;
+      if (!options.silent) model.trigger('remove', model, this, options);
+      this._removeReference(model);
+      return model;
+    },
+
+    // Internal method to remove a model's ties to a collection.
+    _removeReference : function(model) {
+      if (this == model.collection) {
+        delete model.collection;
+      }
+      model.unbind('all', this._onModelEvent);
+    },
+
+    // Internal method called every time a model in the set fires an event.
+    // Sets need to update their indexes when models change ids. All other
+    // events simply proxy through. "add" and "remove" events that originate
+    // in other collections are ignored.
+    _onModelEvent : function(ev, model, collection, options) {
+      if ((ev == 'add' || ev == 'remove') && collection != this) return;
+      if (ev == 'destroy') {
+        this._remove(model, options);
+      }
+      if (model && ev === 'change:' + model.idAttribute) {
+        delete this._byId[model.previous(model.idAttribute)];
+        this._byId[model.id] = model;
+      }
+      this.trigger.apply(this, arguments);
+    }
+
+  });
+
+  // Underscore methods that we want to implement on the Collection.
+  var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
+    'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
+    'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
+    'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty'];
+
+  // Mix in each Underscore method as a proxy to `Collection#models`.
+  _.each(methods, function(method) {
+    Backbone.Collection.prototype[method] = function() {
+      return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
+    };
+  });
+
+  // Backbone.Router
+  // -------------------
+
+  // Routers map faux-URLs to actions, and fire events when routes are
+  // matched. Creating a new one sets its `routes` hash, if not set statically.
+  Backbone.Router = function(options) {
+    options || (options = {});
+    if (options.routes) this.routes = options.routes;
+    this._bindRoutes();
+    this.initialize.apply(this, arguments);
+  };
+
+  // Cached regular expressions for matching named param parts and splatted
+  // parts of route strings.
+  var namedParam    = /:([\w\d]+)/g;
+  var splatParam    = /\*([\w\d]+)/g;
+  var escapeRegExp  = /[-[\]{}()+?.,\\^$|#\s]/g;
+
+  // Set up all inheritable **Backbone.Router** properties and methods.
+  _.extend(Backbone.Router.prototype, Backbone.Events, {
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize : function(){},
+
+    // Manually bind a single named route to a callback. For example:
+    //
+    //     this.route('search/:query/p:num', 'search', function(query, num) {
+    //       ...
+    //     });
+    //
+    route : function(route, name, callback) {
+      Backbone.history || (Backbone.history = new Backbone.History);
+      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
+      Backbone.history.route(route, _.bind(function(fragment) {
+        var args = this._extractParameters(route, fragment);
+        callback.apply(this, args);
+        this.trigger.apply(this, ['route:' + name].concat(args));
+      }, this));
+    },
+
+    // Simple proxy to `Backbone.history` to save a fragment into the history,
+    // without triggering routes.
+    saveLocation : function(fragment) {
+      Backbone.history.saveLocation(fragment);
+    },
+
+    // Simple proxy to `Backbone.history` to both save a fragment into the
+    // history and to then load the route at that fragment.
+    setLocation : function(fragment) {
+      Backbone.history.saveLocation(fragment);
+      Backbone.history.loadUrl(fragment);
+    },
+
+    // Bind all defined routes to `Backbone.history`. We have to reverse the
+    // order of the routes here to support behavior where the most general
+    // routes can be defined at the bottom of the route map.
+    _bindRoutes : function() {
+      if (!this.routes) return;
+      var routes = [];
+      for (var route in this.routes) {
+        routes.unshift([route, this.routes[route]]);
+      }
+      for (var i = 0, l = routes.length; i < l; i++) {
+        this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
+      }
+    },
+
+    // Convert a route string into a regular expression, suitable for matching
+    // against the current location hash.
+    _routeToRegExp : function(route) {
+      route = route.replace(escapeRegExp, "\\$&")
+                   .replace(namedParam, "([^\/]*)")
+                   .replace(splatParam, "(.*?)");
+      return new RegExp('^' + route + '$');
+    },
+
+    // Given a route, and a URL fragment that it matches, return the array of
+    // extracted parameters.
+    _extractParameters : function(route, fragment) {
+      return route.exec(fragment).slice(1);
+    }
+
+  });
+
+  // Backbone.History
+  // ----------------
+
+  // Handles cross-browser history management, based on URL fragments. If the
+  // browser does not support `onhashchange`, falls back to polling.
+  Backbone.History = function() {
+    this.handlers = [];
+    _.bindAll(this, 'checkUrl');
+  };
+
+  // Cached regex for cleaning hashes.
+  var hashStrip = /^#*!?/;
+
+  // Cached regex for detecting MSIE.
+  var isExplorer = /msie [\w.]+/;
+
+  // Has the history handling already been started?
+  var historyStarted = false;
+
+  // Set up all inheritable **Backbone.History** properties and methods.
+  _.extend(Backbone.History.prototype, {
+
+    // The default interval to poll for hash changes, if necessary, is
+    // twenty times a second.
+    interval: 50,
+
+    // Get the cross-browser normalized URL fragment, either from the URL,
+    // the hash, or the override.
+    getFragment : function(fragment, forcePushState) {
+      if (fragment == null) {
+        if (this._hasPushState || forcePushState) {
+          fragment = window.location.pathname;
+          var search = window.location.search;
+          if (search) fragment += search;
+          if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);
+        } else {
+          fragment = window.location.hash;
+        }
+      }
+      return fragment.replace(hashStrip, '');
+    },
+
+    // Start the hash change handling, returning `true` if the current URL matches
+    // an existing route, and `false` otherwise.
+    start : function(options) {
+
+      // Figure out the initial configuration. Do we need an iframe?
+      // Is pushState desired ... is it available?
+      if (historyStarted) throw new Error("Backbone.history has already been started");
+      this.options          = _.extend({}, {root: '/'}, this.options, options);
+      this._wantsPushState  = !!this.options.pushState;
+      this._hasPushState    = !!(this.options.pushState && window.history && window.history.pushState);
+      var fragment          = this.getFragment();
+      var docMode           = document.documentMode;
+      var oldIE             = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
+      if (oldIE) {
+        this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
+        this.saveLocation(fragment);
+      }
+
+      // Depending on whether we're using pushState or hashes, and whether
+      // 'onhashchange' is supported, determine how we check the URL state.
+      if (this._hasPushState) {
+        $(window).bind('popstate', this.checkUrl);
+      } else if ('onhashchange' in window && !oldIE) {
+        $(window).bind('hashchange', this.checkUrl);
+      } else {
+        setInterval(this.checkUrl, this.interval);
+      }
+
+      // Determine if we need to change the base url, for a pushState link
+      // opened by a non-pushState browser.
+      this.fragment = fragment;
+      historyStarted = true;
+      var started = this.loadUrl() || this.loadUrl(window.location.hash);
+      if (this._wantsPushState && !this._hasPushState && window.location.pathname != this.options.root) {
+        this.fragment = this.getFragment(null, true);
+        window.location = this.options.root + '#' + this.fragment;
+      } else {
+        return started;
+      }
+    },
+
+    // Add a route to be tested when the fragment changes. Routes added later may
+    // override previous routes.
+    route : function(route, callback) {
+      this.handlers.unshift({route : route, callback : callback});
+    },
+
+    // Checks the current URL to see if it has changed, and if it has,
+    // calls `loadUrl`, normalizing across the hidden iframe.
+    checkUrl : function(e) {
+      var current = this.getFragment();
+      if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
+      if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
+      if (this.iframe) this.saveLocation(current);
+      this.loadUrl() || this.loadUrl(window.location.hash);
+    },
+
+    // Attempt to load the current URL fragment. If a route succeeds with a
+    // match, returns `true`. If no defined routes matches the fragment,
+    // returns `false`.
+    loadUrl : function(fragmentOverride) {
+      var fragment = this.fragment = this.getFragment(fragmentOverride);
+      var matched = _.any(this.handlers, function(handler) {
+        if (handler.route.test(fragment)) {
+          handler.callback(fragment);
+          return true;
+        }
+      });
+      return matched;
+    },
+
+    // Save a fragment into the hash history. You are responsible for properly
+    // URL-encoding the fragment in advance. This does not trigger
+    // a `hashchange` event.
+    saveLocation : function(fragment) {
+      fragment = (fragment || '').replace(hashStrip, '');
+      if (this.fragment == fragment || this.fragment == decodeURIComponent(fragment)) return;
+      if (this._hasPushState) {
+        var loc = window.location;
+        if (fragment.indexOf(this.options.root) != 0) fragment = this.options.root + fragment;
+        this.fragment = fragment;
+        window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + fragment);
+      } else {
+        window.location.hash = this.fragment = fragment;
+        if (this.iframe && (fragment != this.getFragment(this.iframe.location.hash))) {
+          this.iframe.document.open().close();
+          this.iframe.location.hash = fragment;
+        }
+      }
+    }
+
+  });
+
+  // Backbone.View
+  // -------------
+
+  // Creating a Backbone.View creates its initial element outside of the DOM,
+  // if an existing element is not provided...
+  Backbone.View = function(options) {
+    this.cid = _.uniqueId('view');
+    this._configure(options || {});
+    this._ensureElement();
+    this.delegateEvents();
+    this.initialize.apply(this, arguments);
+  };
+
+  // Element lookup, scoped to DOM elements within the current view.
+  // This should be prefered to global lookups, if you're dealing with
+  // a specific view.
+  var selectorDelegate = function(selector) {
+    return $(selector, this.el);
+  };
+
+  // Cached regex to split keys for `delegate`.
+  var eventSplitter = /^(\S+)\s*(.*)$/;
+
+  // List of view options to be merged as properties.
+  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
+
+  // Set up all inheritable **Backbone.View** properties and methods.
+  _.extend(Backbone.View.prototype, Backbone.Events, {
+
+    // The default `tagName` of a View's element is `"div"`.
+    tagName : 'div',
+
+    // Attach the `selectorDelegate` function as the `$` property.
+    $       : selectorDelegate,
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize : function(){},
+
+    // **render** is the core function that your view should override, in order
+    // to populate its element (`this.el`), with the appropriate HTML. The
+    // convention is for **render** to always return `this`.
+    render : function() {
+      return this;
+    },
+
+    // Remove this view from the DOM. Note that the view isn't present in the
+    // DOM by default, so calling this method may be a no-op.
+    remove : function() {
+      $(this.el).remove();
+      return this;
+    },
+
+    // For small amounts of DOM Elements, where a full-blown template isn't
+    // needed, use **make** to manufacture elements, one at a time.
+    //
+    //     var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
+    //
+    make : function(tagName, attributes, content) {
+      var el = document.createElement(tagName);
+      if (attributes) $(el).attr(attributes);
+      if (content) $(el).html(content);
+      return el;
+    },
+
+    // Set callbacks, where `this.callbacks` is a hash of
+    //
+    // *{"event selector": "callback"}*
+    //
+    //     {
+    //       'mousedown .title':  'edit',
+    //       'click .button':     'save'
+    //     }
+    //
+    // pairs. Callbacks will be bound to the view, with `this` set properly.
+    // Uses event delegation for efficiency.
+    // Omitting the selector binds the event to `this.el`.
+    // This only works for delegate-able events: not `focus`, `blur`, and
+    // not `change`, `submit`, and `reset` in Internet Explorer.
+    delegateEvents : function(events) {
+      if (!(events || (events = this.events))) return;
+      $(this.el).unbind('.delegateEvents' + this.cid);
+      for (var key in events) {
+        var method = this[events[key]];
+        if (!method) throw new Error('Event "' + events[key] + '" does not exist');
+        var match = key.match(eventSplitter);
+        var eventName = match[1], selector = match[2];
+        method = _.bind(method, this);
+        eventName += '.delegateEvents' + this.cid;
+        if (selector === '') {
+          $(this.el).bind(eventName, method);
+        } else {
+          $(this.el).delegate(selector, eventName, method);
+        }
+      }
+    },
+
+    // Performs the initial configuration of a View with a set of options.
+    // Keys with special meaning *(model, collection, id, className)*, are
+    // attached directly to the view.
+    _configure : function(options) {
+      if (this.options) options = _.extend({}, this.options, options);
+      for (var i = 0, l = viewOptions.length; i < l; i++) {
+        var attr = viewOptions[i];
+        if (options[attr]) this[attr] = options[attr];
+      }
+      this.options = options;
+    },
+
+    // Ensure that the View has a DOM element to render into.
+    // If `this.el` is a string, pass it through `$()`, take the first
+    // matching element, and re-assign it to `el`. Otherwise, create
+    // an element from the `id`, `className` and `tagName` proeprties.
+    _ensureElement : function() {
+      if (!this.el) {
+        var attrs = this.attributes || {};
+        if (this.id) attrs.id = this.id;
+        if (this.className) attrs['class'] = this.className;
+        this.el = this.make(this.tagName, attrs);
+      } else if (_.isString(this.el)) {
+        this.el = $(this.el).get(0);
+      }
+    }
+
+  });
+
+  // The self-propagating extend function that Backbone classes use.
+  var extend = function (protoProps, classProps) {
+    var child = inherits(this, protoProps, classProps);
+    child.extend = this.extend;
+    return child;
+  };
+
+  // Set up inheritance for the model, collection, and view.
+  Backbone.Model.extend = Backbone.Collection.extend =
+    Backbone.Router.extend = Backbone.View.extend = extend;
+
+  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
+  var methodMap = {
+    'create': 'POST',
+    'update': 'PUT',
+    'delete': 'DELETE',
+    'read'  : 'GET'
+  };
+
+  // Backbone.sync
+  // -------------
+
+  // Override this function to change the manner in which Backbone persists
+  // models to the server. You will be passed the type of request, and the
+  // model in question. By default, uses makes a RESTful Ajax request
+  // to the model's `url()`. Some possible customizations could be:
+  //
+  // * Use `setTimeout` to batch rapid-fire updates into a single request.
+  // * Send up the models as XML instead of JSON.
+  // * Persist models via WebSockets instead of Ajax.
+  //
+  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
+  // as `POST`, with a `_method` parameter containing the true HTTP method,
+  // as well as all requests with the body as `application/x-www-form-urlencoded` instead of
+  // `application/json` with the model in a param named `model`.
+  // Useful when interfacing with server-side languages like **PHP** that make
+  // it difficult to read the body of `PUT` requests.
+  Backbone.sync = function(method, model, options) {
+    var type = methodMap[method];
+
+    // Default JSON-request options.
+    var params = _.extend({
+      type:         type,
+      dataType:     'json',
+      processData:  false
+    }, options);
+
+    // Ensure that we have a URL.
+    if (!params.url) {
+      params.url = getUrl(model) || urlError();
+    }
+
+    // Ensure that we have the appropriate request data.
+    if (!params.data && model && (method == 'create' || method == 'update')) {
+      params.contentType = 'application/json';
+      params.data = JSON.stringify(model.toJSON());
+    }
+
+    // For older servers, emulate JSON by encoding the request into an HTML-form.
+    if (Backbone.emulateJSON) {
+      params.contentType = 'application/x-www-form-urlencoded';
+      params.processData = true;
+      params.data        = params.data ? {model : params.data} : {};
+    }
+
+    // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
+    // And an `X-HTTP-Method-Override` header.
+    if (Backbone.emulateHTTP) {
+      if (type === 'PUT' || type === 'DELETE') {
+        if (Backbone.emulateJSON) params.data._method = type;
+        params.type = 'POST';
+        params.beforeSend = function(xhr) {
+          xhr.setRequestHeader('X-HTTP-Method-Override', type);
+        };
+      }
+    }
+
+    // Make the request.
+    return $.ajax(params);
+  };
+
+  // Helpers
+  // -------
+
+  // Shared empty constructor function to aid in prototype-chain creation.
+  var ctor = function(){};
+
+  // Helper function to correctly set up the prototype chain, for subclasses.
+  // Similar to `goog.inherits`, but uses a hash of prototype properties and
+  // class properties to be extended.
+  var inherits = function(parent, protoProps, staticProps) {
+    var child;
+
+    // The constructor function for the new subclass is either defined by you
+    // (the "constructor" property in your `extend` definition), or defaulted
+    // by us to simply call `super()`.
+    if (protoProps && protoProps.hasOwnProperty('constructor')) {
+      child = protoProps.constructor;
+    } else {
+      child = function(){ return parent.apply(this, arguments); };
+    }
+
+    // Inherit class (static) properties from parent.
+    _.extend(child, parent);
+
+    // Set the prototype chain to inherit from `parent`, without calling
+    // `parent`'s constructor function.
+    ctor.prototype = parent.prototype;
+    child.prototype = new ctor();
+
+    // Add prototype properties (instance properties) to the subclass,
+    // if supplied.
+    if (protoProps) _.extend(child.prototype, protoProps);
+
+    // Add static properties to the constructor function, if supplied.
+    if (staticProps) _.extend(child, staticProps);
+
+    // Correctly set child's `prototype.constructor`.
+    child.prototype.constructor = child;
+
+    // Set a convenience property in case the parent's prototype is needed later.
+    child.__super__ = parent.prototype;
+
+    return child;
+  };
+
+  // Helper function to get a URL from a Model or Collection as a property
+  // or as a function.
+  var getUrl = function(object) {
+    if (!(object && object.url)) return null;
+    return _.isFunction(object.url) ? object.url() : object.url;
+  };
+
+  // Throw an error when a URL is needed, and none is supplied.
+  var urlError = function() {
+    throw new Error('A "url" property or function must be specified');
+  };
+
+  // Wrap an optional error callback with a fallback error event.
+  var wrapError = function(onError, model, options) {
+    return function(resp) {
+      if (onError) {
+        onError(model, resp, options);
+      } else {
+        model.trigger('error', model, resp, options);
+      }
+    };
+  };
+
+  // Helper function to escape a string for HTML rendering.
+  var escapeHTML = function(string) {
+    return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27').replace(/\//g,'&#x2F;');
+  };
+
+}).call(this);
diff --git a/l10n_community/visualsearch/vendor/underscore-1.1.5.js b/l10n_community/visualsearch/vendor/underscore-1.1.5.js
new file mode 100644
index 0000000..00f6c26
--- /dev/null
+++ b/l10n_community/visualsearch/vendor/underscore-1.1.5.js
@@ -0,0 +1,796 @@
+//     Underscore.js 1.1.5
+//     (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
+//     Underscore is freely distributable under the MIT license.
+//     Portions of Underscore are inspired or borrowed from Prototype,
+//     Oliver Steele's Functional, and John Resig's Micro-Templating.
+//     For all details and documentation:
+//     http://documentcloud.github.com/underscore
+
+(function() {
+
+  // Baseline setup
+  // --------------
+
+  // Establish the root object, `window` in the browser, or `global` on the server.
+  var root = this;
+
+  // Save the previous value of the `_` variable.
+  var previousUnderscore = root._;
+
+  // Establish the object that gets returned to break out of a loop iteration.
+  var breaker = {};
+
+  // Save bytes in the minified (but not gzipped) version:
+  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+  // Create quick reference variables for speed access to core prototypes.
+  var slice            = ArrayProto.slice,
+      unshift          = ArrayProto.unshift,
+      toString         = ObjProto.toString,
+      hasOwnProperty   = ObjProto.hasOwnProperty;
+
+  // All **ECMAScript 5** native function implementations that we hope to use
+  // are declared here.
+  var
+    nativeForEach      = ArrayProto.forEach,
+    nativeMap          = ArrayProto.map,
+    nativeReduce       = ArrayProto.reduce,
+    nativeReduceRight  = ArrayProto.reduceRight,
+    nativeFilter       = ArrayProto.filter,
+    nativeEvery        = ArrayProto.every,
+    nativeSome         = ArrayProto.some,
+    nativeIndexOf      = ArrayProto.indexOf,
+    nativeLastIndexOf  = ArrayProto.lastIndexOf,
+    nativeIsArray      = Array.isArray,
+    nativeKeys         = Object.keys,
+    nativeBind         = FuncProto.bind;
+
+  // Create a safe reference to the Underscore object for use below.
+  var _ = function(obj) { return new wrapper(obj); };
+
+  // Export the Underscore object for **CommonJS**, with backwards-compatibility
+  // for the old `require()` API. If we're not in CommonJS, add `_` to the
+  // global object.
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = _;
+    _._ = _;
+  } else {
+    root._ = _;
+  }
+
+  // Current version.
+  _.VERSION = '1.1.5';
+
+  // Collection Functions
+  // --------------------
+
+  // The cornerstone, an `each` implementation, aka `forEach`.
+  // Handles objects implementing `forEach`, arrays, and raw objects.
+  // Delegates to **ECMAScript 5**'s native `forEach` if available.
+  var each = _.each = _.forEach = function(obj, iterator, context) {
+    if (obj == null) return;
+    if (nativeForEach && obj.forEach === nativeForEach) {
+      obj.forEach(iterator, context);
+    } else if (_.isNumber(obj.length)) {
+      for (var i = 0, l = obj.length; i < l; i++) {
+        if (iterator.call(context, obj[i], i, obj) === breaker) return;
+      }
+    } else {
+      for (var key in obj) {
+        if (hasOwnProperty.call(obj, key)) {
+          if (iterator.call(context, obj[key], key, obj) === breaker) return;
+        }
+      }
+    }
+  };
+
+  // Return the results of applying the iterator to each element.
+  // Delegates to **ECMAScript 5**'s native `map` if available.
+  _.map = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
+    each(obj, function(value, index, list) {
+      results[results.length] = iterator.call(context, value, index, list);
+    });
+    return results;
+  };
+
+  // **Reduce** builds up a single result from a list of values, aka `inject`,
+  // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
+  _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
+    var initial = memo !== void 0;
+    if (obj == null) obj = [];
+    if (nativeReduce && obj.reduce === nativeReduce) {
+      if (context) iterator = _.bind(iterator, context);
+      return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
+    }
+    each(obj, function(value, index, list) {
+      if (!initial && index === 0) {
+        memo = value;
+        initial = true;
+      } else {
+        memo = iterator.call(context, memo, value, index, list);
+      }
+    });
+    if (!initial) throw new TypeError("Reduce of empty array with no initial value");
+    return memo;
+  };
+
+  // The right-associative version of reduce, also known as `foldr`.
+  // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
+  _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
+    if (obj == null) obj = [];
+    if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
+      if (context) iterator = _.bind(iterator, context);
+      return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
+    }
+    var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
+    return _.reduce(reversed, iterator, memo, context);
+  };
+
+  // Return the first value which passes a truth test. Aliased as `detect`.
+  _.find = _.detect = function(obj, iterator, context) {
+    var result;
+    any(obj, function(value, index, list) {
+      if (iterator.call(context, value, index, list)) {
+        result = value;
+        return true;
+      }
+    });
+    return result;
+  };
+
+  // Return all the elements that pass a truth test.
+  // Delegates to **ECMAScript 5**'s native `filter` if available.
+  // Aliased as `select`.
+  _.filter = _.select = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
+    each(obj, function(value, index, list) {
+      if (iterator.call(context, value, index, list)) results[results.length] = value;
+    });
+    return results;
+  };
+
+  // Return all the elements for which a truth test fails.
+  _.reject = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    each(obj, function(value, index, list) {
+      if (!iterator.call(context, value, index, list)) results[results.length] = value;
+    });
+    return results;
+  };
+
+  // Determine whether all of the elements match a truth test.
+  // Delegates to **ECMAScript 5**'s native `every` if available.
+  // Aliased as `all`.
+  _.every = _.all = function(obj, iterator, context) {
+    iterator = iterator || _.identity;
+    var result = true;
+    if (obj == null) return result;
+    if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
+    each(obj, function(value, index, list) {
+      if (!(result = result && iterator.call(context, value, index, list))) return breaker;
+    });
+    return result;
+  };
+
+  // Determine if at least one element in the object matches a truth test.
+  // Delegates to **ECMAScript 5**'s native `some` if available.
+  // Aliased as `any`.
+  var any = _.some = _.any = function(obj, iterator, context) {
+    iterator = iterator || _.identity;
+    var result = false;
+    if (obj == null) return result;
+    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
+    each(obj, function(value, index, list) {
+      if (result = iterator.call(context, value, index, list)) return breaker;
+    });
+    return result;
+  };
+
+  // Determine if a given value is included in the array or object using `===`.
+  // Aliased as `contains`.
+  _.include = _.contains = function(obj, target) {
+    var found = false;
+    if (obj == null) return found;
+    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
+    any(obj, function(value) {
+      if (found = value === target) return true;
+    });
+    return found;
+  };
+
+  // Invoke a method (with arguments) on every item in a collection.
+  _.invoke = function(obj, method) {
+    var args = slice.call(arguments, 2);
+    return _.map(obj, function(value) {
+      return (method ? value[method] : value).apply(value, args);
+    });
+  };
+
+  // Convenience version of a common use case of `map`: fetching a property.
+  _.pluck = function(obj, key) {
+    return _.map(obj, function(value){ return value[key]; });
+  };
+
+  // Return the maximum element or (element-based computation).
+  _.max = function(obj, iterator, context) {
+    if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
+    var result = {computed : -Infinity};
+    each(obj, function(value, index, list) {
+      var computed = iterator ? iterator.call(context, value, index, list) : value;
+      computed >= result.computed && (result = {value : value, computed : computed});
+    });
+    return result.value;
+  };
+
+  // Return the minimum element (or element-based computation).
+  _.min = function(obj, iterator, context) {
+    if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
+    var result = {computed : Infinity};
+    each(obj, function(value, index, list) {
+      var computed = iterator ? iterator.call(context, value, index, list) : value;
+      computed < result.computed && (result = {value : value, computed : computed});
+    });
+    return result.value;
+  };
+
+  // Sort the object's values by a criterion produced by an iterator.
+  _.sortBy = function(obj, iterator, context) {
+    return _.pluck(_.map(obj, function(value, index, list) {
+      return {
+        value : value,
+        criteria : iterator.call(context, value, index, list)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }), 'value');
+  };
+
+  // Use a comparator function to figure out at what index an object should
+  // be inserted so as to maintain order. Uses binary search.
+  _.sortedIndex = function(array, obj, iterator) {
+    iterator = iterator || _.identity;
+    var low = 0, high = array.length;
+    while (low < high) {
+      var mid = (low + high) >> 1;
+      iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
+    }
+    return low;
+  };
+
+  // Safely convert anything iterable into a real, live array.
+  _.toArray = function(iterable) {
+    if (!iterable)                return [];
+    if (iterable.toArray)         return iterable.toArray();
+    if (_.isArray(iterable))      return iterable;
+    if (_.isArguments(iterable))  return slice.call(iterable);
+    return _.values(iterable);
+  };
+
+  // Return the number of elements in an object.
+  _.size = function(obj) {
+    return _.toArray(obj).length;
+  };
+
+  // Array Functions
+  // ---------------
+
+  // Get the first element of an array. Passing **n** will return the first N
+  // values in the array. Aliased as `head`. The **guard** check allows it to work
+  // with `_.map`.
+  _.first = _.head = function(array, n, guard) {
+    return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
+  };
+
+  // Returns everything but the first entry of the array. Aliased as `tail`.
+  // Especially useful on the arguments object. Passing an **index** will return
+  // the rest of the values in the array from that index onward. The **guard**
+  // check allows it to work with `_.map`.
+  _.rest = _.tail = function(array, index, guard) {
+    return slice.call(array, (index == null) || guard ? 1 : index);
+  };
+
+  // Get the last element of an array.
+  _.last = function(array) {
+    return array[array.length - 1];
+  };
+
+  // Trim out all falsy values from an array.
+  _.compact = function(array) {
+    return _.filter(array, function(value){ return !!value; });
+  };
+
+  // Return a completely flattened version of an array.
+  _.flatten = function(array) {
+    return _.reduce(array, function(memo, value) {
+      if (_.isArray(value)) return memo.concat(_.flatten(value));
+      memo[memo.length] = value;
+      return memo;
+    }, []);
+  };
+
+  // Return a version of the array that does not contain the specified value(s).
+  _.without = function(array) {
+    var values = slice.call(arguments, 1);
+    return _.filter(array, function(value){ return !_.include(values, value); });
+  };
+
+  // Produce a duplicate-free version of the array. If the array has already
+  // been sorted, you have the option of using a faster algorithm.
+  // Aliased as `unique`.
+  _.uniq = _.unique = function(array, isSorted) {
+    return _.reduce(array, function(memo, el, i) {
+      if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
+      return memo;
+    }, []);
+  };
+
+  // Produce an array that contains every item shared between all the
+  // passed-in arrays.
+  _.intersect = function(array) {
+    var rest = slice.call(arguments, 1);
+    return _.filter(_.uniq(array), function(item) {
+      return _.every(rest, function(other) {
+        return _.indexOf(other, item) >= 0;
+      });
+    });
+  };
+
+  // Zip together multiple lists into a single array -- elements that share
+  // an index go together.
+  _.zip = function() {
+    var args = slice.call(arguments);
+    var length = _.max(_.pluck(args, 'length'));
+    var results = new Array(length);
+    for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
+    return results;
+  };
+
+  // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
+  // we need this function. Return the position of the first occurrence of an
+  // item in an array, or -1 if the item is not included in the array.
+  // Delegates to **ECMAScript 5**'s native `indexOf` if available.
+  // If the array is large and already in sort order, pass `true`
+  // for **isSorted** to use binary search.
+  _.indexOf = function(array, item, isSorted) {
+    if (array == null) return -1;
+    var i, l;
+    if (isSorted) {
+      i = _.sortedIndex(array, item);
+      return array[i] === item ? i : -1;
+    }
+    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
+    for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
+    return -1;
+  };
+
+
+  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
+  _.lastIndexOf = function(array, item) {
+    if (array == null) return -1;
+    if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
+    var i = array.length;
+    while (i--) if (array[i] === item) return i;
+    return -1;
+  };
+
+  // Generate an integer Array containing an arithmetic progression. A port of
+  // the native Python `range()` function. See
+  // [the Python documentation](http://docs.python.org/library/functions.html#range).
+  _.range = function(start, stop, step) {
+    if (arguments.length <= 1) {
+      stop = start || 0;
+      start = 0;
+    }
+    step = arguments[2] || 1;
+
+    var len = Math.max(Math.ceil((stop - start) / step), 0);
+    var idx = 0;
+    var range = new Array(len);
+
+    while(idx < len) {
+      range[idx++] = start;
+      start += step;
+    }
+
+    return range;
+  };
+
+  // Function (ahem) Functions
+  // ------------------
+
+  // Create a function bound to a given object (assigning `this`, and arguments,
+  // optionally). Binding with arguments is also known as `curry`.
+  // Delegates to **ECMAScript 5**'s native `Function.bind` if available.
+  // We check for `func.bind` first, to fail fast when `func` is undefined.
+  _.bind = function(func, obj) {
+    if (func.bind === nativeBind && nativeBind) return func.bind.apply(func, slice.call(arguments, 1));
+    var args = slice.call(arguments, 2);
+    return function() {
+      return func.apply(obj, args.concat(slice.call(arguments)));
+    };
+  };
+
+  // Bind all of an object's methods to that object. Useful for ensuring that
+  // all callbacks defined on an object belong to it.
+  _.bindAll = function(obj) {
+    var funcs = slice.call(arguments, 1);
+    if (funcs.length == 0) funcs = _.functions(obj);
+    each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
+    return obj;
+  };
+
+  // Memoize an expensive function by storing its results.
+  _.memoize = function(func, hasher) {
+    var memo = {};
+    hasher = hasher || _.identity;
+    return function() {
+      var key = hasher.apply(this, arguments);
+      return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
+    };
+  };
+
+  // Delays a function for the given number of milliseconds, and then calls
+  // it with the arguments supplied.
+  _.delay = function(func, wait) {
+    var args = slice.call(arguments, 2);
+    return setTimeout(function(){ return func.apply(func, args); }, wait);
+  };
+
+  // Defers a function, scheduling it to run after the current call stack has
+  // cleared.
+  _.defer = function(func) {
+    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
+  };
+
+  // Internal function used to implement `_.throttle` and `_.debounce`.
+  var limit = function(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);
+    };
+  };
+
+  // Returns a function, that, when invoked, will only be triggered at most once
+  // during a given window of time.
+  _.throttle = function(func, wait) {
+    return limit(func, wait, false);
+  };
+
+  // Returns a function, that, as long as it continues to be invoked, will not
+  // be triggered. The function will be called after it stops being called for
+  // N milliseconds.
+  _.debounce = function(func, wait) {
+    return limit(func, wait, true);
+  };
+
+  // Returns a function that will be executed at most one time, no matter how
+  // often you call it. Useful for lazy initialization.
+  _.once = function(func) {
+    var ran = false, memo;
+    return function() {
+      if (ran) return memo;
+      ran = true;
+      return memo = func.apply(this, arguments);
+    };
+  };
+
+  // Returns the first function passed as an argument to the second,
+  // allowing you to adjust arguments, run code before and after, and
+  // conditionally execute the original function.
+  _.wrap = function(func, wrapper) {
+    return function() {
+      var args = [func].concat(slice.call(arguments));
+      return wrapper.apply(this, args);
+    };
+  };
+
+  // Returns a function that is the composition of a list of functions, each
+  // consuming the return value of the function that follows.
+  _.compose = function() {
+    var funcs = slice.call(arguments);
+    return function() {
+      var args = slice.call(arguments);
+      for (var i=funcs.length-1; i >= 0; i--) {
+        args = [funcs[i].apply(this, args)];
+      }
+      return args[0];
+    };
+  };
+
+  // Object Functions
+  // ----------------
+
+  // Retrieve the names of an object's properties.
+  // Delegates to **ECMAScript 5**'s native `Object.keys`
+  _.keys = nativeKeys || function(obj) {
+    if (obj !== Object(obj)) throw new TypeError('Invalid object');
+    var keys = [];
+    for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
+    return keys;
+  };
+
+  // Retrieve the values of an object's properties.
+  _.values = function(obj) {
+    return _.map(obj, _.identity);
+  };
+
+  // Return a sorted list of the function names available on the object.
+  // Aliased as `methods`
+  _.functions = _.methods = function(obj) {
+    return _.filter(_.keys(obj), function(key){ return _.isFunction(obj[key]); }).sort();
+  };
+
+  // Extend a given object with all the properties in passed-in object(s).
+  _.extend = function(obj) {
+    each(slice.call(arguments, 1), function(source) {
+      for (var prop in source) obj[prop] = source[prop];
+    });
+    return obj;
+  };
+
+  // Fill in a given object with default properties.
+  _.defaults = function(obj) {
+    each(slice.call(arguments, 1), function(source) {
+      for (var prop in source) if (obj[prop] == null) obj[prop] = source[prop];
+    });
+    return obj;
+  };
+
+  // Create a (shallow-cloned) duplicate of an object.
+  _.clone = function(obj) {
+    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+  };
+
+  // Invokes interceptor with the obj, and then returns obj.
+  // The primary purpose of this method is to "tap into" a method chain, in
+  // order to perform operations on intermediate results within the chain.
+  _.tap = function(obj, interceptor) {
+    interceptor(obj);
+    return obj;
+  };
+
+  // Perform a deep comparison to check if two objects are equal.
+  _.isEqual = function(a, b) {
+    // Check object identity.
+    if (a === b) return true;
+    // Different types?
+    var atype = typeof(a), btype = typeof(b);
+    if (atype != btype) return false;
+    // Basic equality test (watch out for coercions).
+    if (a == b) return true;
+    // One is falsy and the other truthy.
+    if ((!a && b) || (a && !b)) return false;
+    // Unwrap any wrapped objects.
+    if (a._chain) a = a._wrapped;
+    if (b._chain) b = b._wrapped;
+    // One of them implements an isEqual()?
+    if (a.isEqual) return a.isEqual(b);
+    // Check dates' integer values.
+    if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
+    // Both are NaN?
+    if (_.isNaN(a) && _.isNaN(b)) return false;
+    // Compare regular expressions.
+    if (_.isRegExp(a) && _.isRegExp(b))
+      return a.source     === b.source &&
+             a.global     === b.global &&
+             a.ignoreCase === b.ignoreCase &&
+             a.multiline  === b.multiline;
+    // If a is not an object by this point, we can't handle it.
+    if (atype !== 'object') return false;
+    // Check for different array lengths before comparing contents.
+    if (a.length && (a.length !== b.length)) return false;
+    // Nothing else worked, deep compare the contents.
+    var aKeys = _.keys(a), bKeys = _.keys(b);
+    // Different object sizes?
+    if (aKeys.length != bKeys.length) return false;
+    // Recursive comparison of contents.
+    for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
+    return true;
+  };
+
+  // Is a given array or object empty?
+  _.isEmpty = function(obj) {
+    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
+    for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
+    return true;
+  };
+
+  // Is a given value a DOM element?
+  _.isElement = function(obj) {
+    return !!(obj && obj.nodeType == 1);
+  };
+
+  // Is a given value an array?
+  // Delegates to ECMA5's native Array.isArray
+  _.isArray = nativeIsArray || function(obj) {
+    return toString.call(obj) === '[object Array]';
+  };
+
+  // Is a given variable an arguments object?
+  _.isArguments = function(obj) {
+    return !!(obj && hasOwnProperty.call(obj, 'callee'));
+  };
+
+  // Is a given value a function?
+  _.isFunction = function(obj) {
+    return !!(obj && obj.constructor && obj.call && obj.apply);
+  };
+
+  // Is a given value a string?
+  _.isString = function(obj) {
+    return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
+  };
+
+  // Is a given value a number?
+  _.isNumber = function(obj) {
+    return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
+  };
+
+  // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
+  // that does not equal itself.
+  _.isNaN = function(obj) {
+    return obj !== obj;
+  };
+
+  // Is a given value a boolean?
+  _.isBoolean = function(obj) {
+    return obj === true || obj === false;
+  };
+
+  // Is a given value a date?
+  _.isDate = function(obj) {
+    return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
+  };
+
+  // Is the given value a regular expression?
+  _.isRegExp = function(obj) {
+    return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
+  };
+
+  // Is a given value equal to null?
+  _.isNull = function(obj) {
+    return obj === null;
+  };
+
+  // Is a given variable undefined?
+  _.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  // Utility Functions
+  // -----------------
+
+  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+  // previous owner. Returns a reference to the Underscore object.
+  _.noConflict = function() {
+    root._ = previousUnderscore;
+    return this;
+  };
+
+  // Keep the identity function around for default iterators.
+  _.identity = function(value) {
+    return value;
+  };
+
+  // Run a function **n** times.
+  _.times = function (n, iterator, context) {
+    for (var i = 0; i < n; i++) iterator.call(context, i);
+  };
+
+  // Add your own custom functions to the Underscore object, ensuring that
+  // they're correctly added to the OOP wrapper as well.
+  _.mixin = function(obj) {
+    each(_.functions(obj), function(name){
+      addToWrapper(name, _[name] = obj[name]);
+    });
+  };
+
+  // Generate a unique integer id (unique within the entire client session).
+  // Useful for temporary DOM ids.
+  var idCounter = 0;
+  _.uniqueId = function(prefix) {
+    var id = idCounter++;
+    return prefix ? prefix + id : id;
+  };
+
+  // By default, Underscore uses ERB-style template delimiters, change the
+  // following template settings to use alternative delimiters.
+  _.templateSettings = {
+    evaluate    : /<%([\s\S]+?)%>/g,
+    interpolate : /<%=([\s\S]+?)%>/g
+  };
+
+  // JavaScript micro-templating, similar to John Resig's implementation.
+  // Underscore templating handles arbitrary delimiters, preserves whitespace,
+  // and correctly escapes quotes within interpolated code.
+  _.template = function(str, data) {
+    var c  = _.templateSettings;
+    var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
+      'with(obj||{}){__p.push(\'' +
+      str.replace(/\\/g, '\\\\')
+         .replace(/'/g, "\\'")
+         .replace(c.interpolate, function(match, code) {
+           return "'," + code.replace(/\\'/g, "'") + ",'";
+         })
+         .replace(c.evaluate || null, function(match, code) {
+           return "');" + code.replace(/\\'/g, "'")
+                              .replace(/[\r\n\t]/g, ' ') + "__p.push('";
+         })
+         .replace(/\r/g, '\\r')
+         .replace(/\n/g, '\\n')
+         .replace(/\t/g, '\\t')
+         + "');}return __p.join('');";
+    var func = new Function('obj', tmpl);
+    return data ? func(data) : func;
+  };
+
+  // The OOP Wrapper
+  // ---------------
+
+  // If Underscore is called as a function, it returns a wrapped object that
+  // can be used OO-style. This wrapper holds altered versions of all the
+  // underscore functions. Wrapped objects may be chained.
+  var wrapper = function(obj) { this._wrapped = obj; };
+
+  // Expose `wrapper.prototype` as `_.prototype`
+  _.prototype = wrapper.prototype;
+
+  // Helper function to continue chaining intermediate results.
+  var result = function(obj, chain) {
+    return chain ? _(obj).chain() : obj;
+  };
+
+  // A method to easily add functions to the OOP wrapper.
+  var addToWrapper = function(name, func) {
+    wrapper.prototype[name] = function() {
+      var args = slice.call(arguments);
+      unshift.call(args, this._wrapped);
+      return result(func.apply(_, args), this._chain);
+    };
+  };
+
+  // Add all of the Underscore functions to the wrapper object.
+  _.mixin(_);
+
+  // Add all mutator Array functions to the wrapper.
+  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+    var method = ArrayProto[name];
+    wrapper.prototype[name] = function() {
+      method.apply(this._wrapped, arguments);
+      return result(this._wrapped, this._chain);
+    };
+  });
+
+  // Add all accessor Array functions to the wrapper.
+  each(['concat', 'join', 'slice'], function(name) {
+    var method = ArrayProto[name];
+    wrapper.prototype[name] = function() {
+      return result(method.apply(this._wrapped, arguments), this._chain);
+    };
+  });
+
+  // Start chaining a wrapped Underscore object.
+  wrapper.prototype.chain = function() {
+    this._chain = true;
+    return this;
+  };
+
+  // Extracts the result from a wrapped and chained object.
+  wrapper.prototype.value = function() {
+    return this._wrapped;
+  };
+
+})();
