diff --git a/tailbone/static/themes/falafel/css/layout.css b/tailbone/static/themes/falafel/css/layout.css
new file mode 100644
index 00000000..5134141a
--- /dev/null
+++ b/tailbone/static/themes/falafel/css/layout.css
@@ -0,0 +1,99 @@
+
+/******************************
+ * main layout
+ ******************************/
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+.content-wrapper {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+
+/******************************
+ * header
+ ******************************/
+
+header .level {
+ /* height: 60px; */
+ line-height: 60px;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+header .level #header-logo {
+ display: inline-block;
+}
+
+header .level .global-title,
+header .level-left .global-title {
+ font-size: 2em;
+ font-weight: bold;
+}
+
+header .level #current-context,
+header .level-left #current-context {
+ font-size: 2em;
+ font-weight: bold;
+}
+
+header .level #current-context span,
+header .level-left #current-context span {
+ margin-right: 10px;
+}
+
+header .level .theme-picker {
+ display: inline-flex;
+}
+
+#content-title h1 {
+ font-size: 2em;
+}
+
+/******************************
+ * content
+ ******************************/
+
+#page-body {
+ padding: 0.4em;
+}
+
+/******************************
+ * context menu
+ ******************************/
+
+#context-menu {
+ text-align: right;
+ white-space: nowrap;
+}
+
+/******************************
+ * "object helper" panel
+ ******************************/
+
+.object-helper {
+ border: 1px solid black;
+ margin: 1em;
+ padding: 1em;
+ width: 20em;
+}
+
+.object-helper-content {
+ margin-top: 1em;
+}
+
+/******************************
+ * feedback
+ ******************************/
+
+.feedback-dialog .red {
+ color: red;
+ font-weight: bold;
+}
diff --git a/tailbone/static/themes/falafel/js/tailbone.feedback.js b/tailbone/static/themes/falafel/js/tailbone.feedback.js
new file mode 100644
index 00000000..d6da902b
--- /dev/null
+++ b/tailbone/static/themes/falafel/js/tailbone.feedback.js
@@ -0,0 +1,59 @@
+
+const FeedbackForm = {
+ props: ['user_name', 'referrer'],
+ template: '#feedback-template',
+ methods: {
+ sendFeedback() {
+
+ var textarea = $('.feedback-dialog textarea');
+ var msg = $.trim(textarea.val());
+ if (! msg) {
+ alert("Please enter a message.");
+ textarea.select();
+ textarea.focus();
+ return;
+ }
+
+ // disable_button(dialog_button(event));
+
+ var form = $('.feedback-dialog').parents('form');
+ // TODO: this was copied from default template, but surely we could
+ // just serialize() the form instead?
+ var data = {
+ _csrf: form.find('input[name="_csrf"]').val(),
+ referrer: location.href,
+ user: form.find('input[name="user"]').val(),
+ user_name: form.find('input[name="user_name"]').val(),
+ message: msg
+ };
+
+ var that = this;
+ $.ajax(form.attr('action'), {
+ method: 'POST',
+ data: data,
+ success: function(data) {
+ that.$emit('close');
+ alert("Message successfully sent.\n\nThank you for your feedback.");
+ }
+ });
+
+ }
+ }
+}
+
+new Vue({
+ el: '#feedback-app',
+ methods: {
+ showFeedback() {
+ this.$modal.open({
+ parent: this,
+ canCancel: ['escape', 'x'],
+ component: FeedbackForm,
+ hasModalCard: true,
+ props: {
+ referrer: location.href
+ }
+ });
+ }
+ }
+});
diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako
new file mode 100644
index 00000000..77473d4e
--- /dev/null
+++ b/tailbone/templates/themes/falafel/base.mako
@@ -0,0 +1,329 @@
+## -*- coding: utf-8; -*-
+<%namespace file="/grids/nav.mako" import="grid_index_nav" />
+<%namespace file="/feedback_dialog.mako" import="feedback_dialog" />
+<%namespace name="base_meta" file="/base_meta.mako" />
+
+
+
+
+ ${base_meta.global_title()} » ${capture(self.title)|n}
+ ${base_meta.favicon()}
+ ${self.header_core()}
+
+ % if background_color:
+
+ % endif
+
+ % if not request.rattail_config.production():
+
+ % endif
+
+ ${self.head_tags()}
+
+
+
+
+
+
+
+
+
+
+ ## Page Title
+
+
+ % if capture(self.content_title):
+
+ % if show_prev_next is not Undefined and show_prev_next:
+
+ % if prev_url:
+ ${h.link_to("« Older", prev_url, class_='button autodisable')}
+ % else:
+ ${h.link_to("« Older", '#', class_='button', disabled='disabled')}
+ % endif
+ % if next_url:
+ ${h.link_to("Newer »", next_url, class_='button autodisable')}
+ % else:
+ ${h.link_to("Newer »", '#', class_='button', disabled='disabled')}
+ % endif
+
+ % endif
+
+
${self.content_title()}
+ % endif
+
+
+
+
+
+ ## Page Body
+
+
+ % if request.session.peek_flash('error'):
+ % for error in request.session.pop_flash('error'):
+
+
+ ${error}
+
+ % endfor
+ % endif
+
+ % if request.session.peek_flash():
+ % for msg in request.session.pop_flash():
+
+
+ ${msg}
+
+ % endfor
+ % endif
+
+ ${self.body()}
+
+
+ ## Footer
+
+
+
+
+ ${h.javascript_link(request.static_url('tailbone:static/themes/falafel/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
+
+
+
+
+<%def name="title()">%def>
+
+<%def name="content_title()">
+ ${self.title()}
+%def>
+
+<%def name="header_core()">
+
+ ${self.core_javascript()}
+ ${self.extra_javascript()}
+ ${self.core_styles()}
+ ${self.extra_styles()}
+
+ ## TODO: should this be elsewhere / more customizable?
+ % if dform is not Undefined:
+ <% resources = dform.get_widget_resources() %>
+ % for path in resources['js']:
+ ${h.javascript_link(request.static_url(path))}
+ % endfor
+ % for path in resources['css']:
+ ${h.stylesheet_link(request.static_url(path))}
+ % endfor
+ % endif
+%def>
+
+<%def name="core_javascript()">
+ ${self.jquery()}
+
+ ## Vue.js
+ ${h.javascript_link('https://unpkg.com/vue')}
+
+ ## Buefy 0.7.3
+ ${h.javascript_link('https://unpkg.com/buefy@0.7.3/dist/buefy.min.js')}
+
+ ## ${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.loadmask.min.js'))}
+ ## ${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.ui.timepicker.js'))}
+
+ ## ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.js') + '?ver={}'.format(tailbone.__version__))}
+ ## ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
+ ## ${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js') + '?ver={}'.format(tailbone.__version__))}
+%def>
+
+<%def name="jquery()">
+
+ ## jQuery 1.12.4
+ ${h.javascript_link('https://code.jquery.com/jquery-1.12.4.min.js')}
+
+ ## jQuery 1.11.4
+ ## ${h.javascript_link('https://code.jquery.com/ui/{}/jquery-ui.min.js'.format(request.rattail_config.get('tailbone', 'jquery_ui.version', default='1.11.4')))}
+
+%def>
+
+<%def name="extra_javascript()">%def>
+
+<%def name="core_styles()">
+
+ ## Bulma 0.7.4
+ ${h.stylesheet_link('https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css')}
+
+ ## Buefy 0.7.3
+ ${h.stylesheet_link('https://unpkg.com/buefy@0.7.3/dist/buefy.min.css')}
+
+## ${self.jquery_theme()}
+## ${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.loadmask.css'))}
+## ${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.ui.timepicker.css'))}
+## ${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.ui.tailbone.css') + '?ver={}'.format(tailbone.__version__))}
+
+ ${h.stylesheet_link(request.static_url('tailbone:static/themes/bobcat/css/base.css') + '?ver={}'.format(tailbone.__version__))}
+ ${h.stylesheet_link(request.static_url('tailbone:static/themes/falafel/css/layout.css') + '?ver={}'.format(tailbone.__version__))}
+ ${h.stylesheet_link(request.static_url('tailbone:static/css/grids.css') + '?ver={}'.format(tailbone.__version__))}
+## ${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css') + '?ver={}'.format(tailbone.__version__))}
+ ${h.stylesheet_link(request.static_url('tailbone:static/themes/bobcat/css/forms.css') + '?ver={}'.format(tailbone.__version__))}
+ ${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))}
+%def>
+
+<%def name="jquery_theme()">
+ ${h.stylesheet_link('https://code.jquery.com/ui/1.11.4/themes/dark-hive/jquery-ui.css')}
+%def>
+
+<%def name="extra_styles()">%def>
+
+<%def name="head_tags()">%def>
+
+<%def name="wtfield(form, name, **kwargs)">
+
+%def>
diff --git a/tailbone/templates/themes/falafel/feedback_dialog.mako b/tailbone/templates/themes/falafel/feedback_dialog.mako
new file mode 100644
index 00000000..874ab952
--- /dev/null
+++ b/tailbone/templates/themes/falafel/feedback_dialog.mako
@@ -0,0 +1,69 @@
+## -*- coding: utf-8; -*-
+
+<%def name="feedback_dialog()">
+
+ ${h.form(url('feedback'))}
+ ${h.csrf_token(request)}
+ ${h.hidden('user', value=request.user.uuid if request.user else None)}
+
+
+
+
+
+
+ Questions, suggestions, comments, complaints, etc.
+ regarding this website are
+ welcome and may be submitted below.
+
+
+
+ % if request.user:
+
+
+ % else:
+
+
+ % endif
+
+ % if request.user:
+
+
+ % endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${h.end_form()}
+
+%def>