diff --git a/src/data/UsStates.js b/src/data/UsStates.js
new file mode 100644
index 0000000..4221bf6
--- /dev/null
+++ b/src/data/UsStates.js
@@ -0,0 +1,101 @@
+/**
+ * List of US states and codes
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of liza.
+ *
+ * liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+
+/**
+ * List of states and their codes
+ * @var {Object}
+ */
+var states = {
+ 'AL': 'Alabama',
+ 'AK': 'Alaska',
+ 'AZ': 'Arizona',
+ 'AR': 'Arkansas',
+ 'CA': 'California',
+ 'CO': 'Colorado',
+ 'CT': 'Connecticut',
+ 'DE': 'Delaware',
+ 'FL': 'Florida',
+ 'GA': 'Georgia',
+ 'HI': 'Hawaii',
+ 'ID': 'Idaho',
+ 'IL': 'Illinois',
+ 'IN': 'Indiana',
+ 'IA': 'Iowa',
+ 'KS': 'Kansas',
+ 'KY': 'Kentucky',
+ 'LA': 'Louisiana',
+ 'ME': 'Maine',
+ 'MD': 'Maryland',
+ 'MA': 'Massachusetts',
+ 'MI': 'Michigan',
+ 'MN': 'Minnesota',
+ 'MS': 'Mississippi',
+ 'MO': 'Missouri',
+ 'MT': 'Montana',
+ 'NE': 'Nebraska',
+ 'NV': 'Nevada',
+ 'NH': 'New Hampshire',
+ 'NJ': 'New Jersey',
+ 'NM': 'New Mexico',
+ 'NY': 'New York',
+ 'NC': 'North Carolina',
+ 'ND': 'North Dakota',
+ 'OH': 'Ohio',
+ 'OK': 'Oklahoma',
+ 'OR': 'Oregon',
+ 'PA': 'Pennsylvania',
+ 'RI': 'Rhode Island',
+ 'SC': 'South Carolina',
+ 'SD': 'South Dakota',
+ 'TN': 'Tennessee',
+ 'TX': 'Texas',
+ 'UT': 'Utah',
+ 'VT': 'Vermont',
+ 'VA': 'Virginia',
+ 'WA': 'Washington',
+ 'WV': 'West Virginia',
+ 'WI': 'Wisconsin',
+ 'WY': 'Wyoming',
+
+ // nothing
+ '' : '',
+ 0: ''
+};
+
+
+/**
+ * Gets the name of the state associated with the given code
+ *
+ * @param {string} code state abbr
+ *
+ * @return {string} name of state
+ */
+exports.getName = function( code )
+{
+ var val = states[ code ];
+
+ // if the value was not found, return the code we were given (which likely
+ // makes no sense at all, since it's not a valid state)
+ return ( val === undefined )
+ ? code
+ : val;
+}
diff --git a/src/sort/MultiSort.js b/src/sort/MultiSort.js
index 37c7d35..943a981 100644
--- a/src/sort/MultiSort.js
+++ b/src/sort/MultiSort.js
@@ -22,7 +22,6 @@
* - References to "quote" should be replaced with generic terminology
* representing a document.
* - Dependencies need to be liberated:
- * - ElementStyler;
* - BucketDataValidator.
* - Global references (e.g. jQuery) must be removed.
* - Checkbox-specific logic must be extracted.
diff --git a/src/ui/ElementStyler.js b/src/ui/ElementStyler.js
new file mode 100644
index 0000000..10b20b5
--- /dev/null
+++ b/src/ui/ElementStyler.js
@@ -0,0 +1,1314 @@
+/**
+ * Archaic DOM element styler
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of liza.
+ *
+ * liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * @needsLove
+ * - Everything! This class exists from when the framework was barely
+ * more than a few prototypes and has rotted ever since with little else
+ * but workarounds.
+ * @end needsLove
+ */
+
+var State = require( '../data/UsStates' ),
+ Class = require( 'easejs' ).Class;
+
+
+/**
+ * Styles DOM elements
+ *
+ * This class styles DOM elements with Dojo's Dijits (widgets).
+ *
+ * @return void
+ */
+module.exports = Class( 'ElementStyler',
+{
+ /**
+ * Stores element types as an associative array
+ * @type {Object.}
+ */
+ elementTypes: {},
+
+ 'private _answerRefs': {},
+
+ /**
+ * Stores help text for elements
+ * @type {Object.}
+ */
+ elementHelp: {},
+
+ /**
+ * Whether to show internal questions
+ * @type {boolean}
+ */
+ _showInternal: false,
+
+ /**
+ * Stores defaults for questions
+ * @type {Object}
+ */
+ 'private _defaults': {},
+
+ /**
+ * Stores defaults for display only
+ * @type {Object}
+ */
+ 'private _displayDefaults': {},
+
+ /**
+ * Contains data for select
+ * @type {Object.}
+ */
+ 'private _selectData': {},
+
+ /**
+ * Selector context
+ * @type {jQuery}
+ */
+ 'private _$context': null,
+
+
+ _answerStyles: {
+ 'currency': function( value )
+ {
+ var pre = '$';
+
+ if ( !value )
+ {
+ value = 0;
+ }
+ else if ( value < 0 )
+ {
+ value *= -1;
+ pre = '-$';
+ }
+
+ var formatter = this._getAnswerStyler( 'number' );
+
+ return pre + formatter.call( this, ( +value ).toFixed( 2 ) );
+ },
+
+ 'dollars': function( value )
+ {
+ var formatter = this._getAnswerStyler( 'currency' );
+
+ return formatter.call( this, ( +value ).toFixed( 0 ) );
+ },
+
+ 'float': function( value )
+ {
+ if ( !value )
+ {
+ value = 0;
+ }
+
+ var formatter = this._getAnswerStyler( 'number' );
+
+ return formatter.call( this, ( +value ).toFixed( 2 ) );
+ },
+
+ 'limit': function( value, _, default_val )
+ {
+ value = ( value + '' ).replace( ',', '' );
+
+
+ // if no value was given, be sure that we use the proper default
+ // value
+ if ( ( value === '' ) && default_val )
+ {
+ return default_val;
+ }
+
+ // if the value is simply a string, return it
+ if ( /^[a-z_ -]+$/i.test( value ) )
+ {
+ return value;
+ }
+
+ // split on multiple limits (this will work fine if there's only
+ // one)
+ var data = ( ''+( value ) ).split( '/' ),
+ i = data.length;
+
+ // simple comma addition (for thousands); XXX: this mess is an
+ // abomination
+ while ( i-- )
+ {
+ data[ i ] = data[ i ]
+ .replace( /^.*(?:[0-9]{3}|[1-9]0{2})$/, function( number )
+ {
+ var len = number.length,
+ ret = '';
+
+ // insert thousands separators into their proper
+ // positions
+ for ( var i = 0; i < len; i++ )
+ {
+ ret += number.substr( i, 1 );
+
+ if ( ( ( len - i ) % 3 ) === 1 )
+ {
+ ret += ',';
+ }
+ }
+
+ // could handle this in the above loop, but this is more
+ // clear
+ return ret.replace( /,$/, '' );
+ } )
+ .replace( /^,?([0-9]{2,})$/, '$1,000' )
+ .replace( /^,/, '' );
+ }
+
+ // re-join multiple limits
+ return data.join( '/' );
+ },
+
+ 'multilimit': function( values, _, default_val )
+ {
+ var limit = this._getAnswerStyler( 'limit' );
+
+ // if we're not an array, fall back to the normal limit styler
+ if ( typeof values !== 'object' )
+ {
+ return limit.apply( this, arguments );
+ }
+
+ var formatted = [],
+ same = true;
+
+ for ( var i in values )
+ {
+ formatted.push(
+ limit.call( this, values[ i ], _, default_val )
+ );
+
+ if ( i > 0 )
+ {
+ same = ( same && formatted[ i ] === formatted[ i - 1 ] );
+ }
+ }
+
+ return ( same )
+ ? formatted[ 0 ]
+ : formatted.join( '; ' );
+ },
+
+ 'multitext': function( values, _, default_val )
+ {
+ // for now
+ this._getAnswerStyler( 'multilimit' ).apply(
+ this, arguments
+ );
+ },
+
+ 'deductible': function( value, _, default_val )
+ {
+ // if no value was given, be sure that we use the proper default
+ // value
+ if ( ( value === '' ) && default_val )
+ {
+ return default_val;
+ }
+
+ return value + ' Deductible';
+ },
+
+ /*
+ * display as accepted, rejected or default if available
+ */
+ 'acceptReject': function( value, _, default_val )
+ {
+ // use the default if no value
+ if ( ( value === '' ) && default_val )
+ {
+ return default_val;
+ }
+
+ var ret = value;
+
+ if ( +value === 0 )
+ {
+ ret = 'Rejected';
+ }
+ else if ( +value === 1 )
+ {
+ ret = 'Accepted';
+ }
+
+ return ret;
+ },
+
+
+ 'includeExclude': function( value, _, default_val )
+ {
+ // use the default if no value
+ if ( ( value === '' ) && default_val )
+ {
+ return default_val;
+ }
+
+ return ( +value === 0 )
+ ? 'Excluded'
+ : 'Included';
+ },
+
+
+ /*
+ * display as a limit, rejected or default if available
+ */
+ 'limitReject': function( value, _, default_val )
+ {
+ // use the default if no value
+ if ( ( value === '' ) && default_val )
+ {
+ return default_val;
+ }
+
+ var limit = this._getAnswerStyler( 'number' );
+
+ if ( +value === 0 )
+ {
+ ret = 'Rejected';
+ }
+ else if ( +value === 1 )
+ {
+ ret = 'Accepted';
+ }
+ else
+ {
+ ret = limit.apply( this, arguments );
+ }
+
+ return ret;
+ },
+
+
+ /**
+ * format thousands
+ */
+ 'number': function( value )
+ {
+ var str = value.toString().split( '.' );
+ var len = str[0].length,
+ ret = '';
+
+ for ( var i = 0; i < len; i++ )
+ {
+ ret += str[0].charAt( i );
+
+ if ( ( ( len - i ) % 3 ) === 1 )
+ {
+ ret += ',';
+ }
+ }
+
+ str[0] = ret.replace( /,$/, '' );
+
+ return str.join( '.' )
+ },
+
+ 'state': function( value )
+ {
+ return State.getName( value );
+ },
+
+ /**
+ * Styles a no-yes answer
+ *
+ * A non-zero value is considered to be "Yes". An empty string (unless a
+ * default value is given) and "0" are considered to be "No". Default
+ * value is only returned if an empty string is provided.
+ *
+ * @param {string} value value to style
+ * @param {*} _ ignored
+ * @param {string_ default_val default value
+ *
+ * @return {string} styled answer
+ */
+ 'noyes': function( value, _, default_val )
+ {
+ // if a default value is provided, we will interpret an empty string
+ // as no value and return the default
+ if ( ( value === '' ) && default_val )
+ {
+ return default_val;
+ }
+
+ return ( value && ( value !== '0' ) )
+ ? 'Yes'
+ : 'No';
+ },
+
+ 'select': function( value, ref_id )
+ {
+ var val = this._selectData[ref_id][value];
+
+ // return the string associated with the given value
+ // (the text for the option), or the given value if it does not
+ // exist
+ return ( val === undefined )
+ ? value
+ : val;
+ },
+
+ 'manualDate': function( value )
+ {
+ if( value.replace )
+ {
+ return value.replace(
+ /^([0-9]{4})-([0-9]+)-([0-9]+)$/,
+ '$2/$3/$1'
+ );
+ }
+
+ return null;
+ },
+
+ 'date': function( value )
+ {
+ var data = value.split( '-' );
+
+ // m/d/y
+ return data[1] + '/' + data[2] + '/' + data[0];
+ },
+
+ 'dateTime': function( value )
+ {
+ var ret_val = new Date( ( +value ) * 1000 );
+
+ // do not attempt to format if invalid date
+ if ( isNaN( ret_val.getDate() ) || value === '' )
+ {
+ return '';
+ }
+
+ return ( ret_val.getMonth() + 1 ) + '/'
+ + ret_val.getDate() + '/'
+ + ret_val.getFullYear();
+ },
+
+ percent: function( value )
+ {
+ return value + '%';
+ },
+
+ waitable: function( value )
+ {
+ return value.replace(
+ /Please wait.../,
+ '
$&
'
+ );
+ }
+ },
+
+
+ __construct: function()
+ {
+ this._$context = jQuery;
+ },
+
+
+ /**
+ * Returns the function to be used for the widget jQuery selector
+ *
+ * @return Function selector function
+ */
+ getWidgetSelector: function()
+ {
+ return function( node, i, data )
+ {
+ // name of the widget we're searching for
+ var name = data[3],
+ $node = $( node );
+
+ // if it's not a widget, bail
+ if ( !( $node.hasClass( 'widget' ) ) )
+ {
+ return false;
+ }
+
+ // if no name was given, then don't check for it (they just want to
+ // know if this is a widget)
+ if ( !( name ) )
+ {
+ return true;
+ }
+
+ // quick name check on self
+ if ( $node.attr( 'name' ) === ( name + '[]' ) )
+ {
+ return true;
+ }
+
+ // attempt to locate the element with the name we're looking for
+ var $named = $node.find( '[name="' + name + '[]"]' );
+ if ( $named.length === 0 )
+ {
+ return false;
+ }
+
+ return true;
+ }
+ },
+
+
+ getWidgetIdSelector: function()
+ {
+ return function( node, i, data )
+ {
+ // name of the widget we're searching for
+ var id = data[3],
+ $node = $( node );
+
+ if ( !( $node.hasClass( 'widget' ) ) )
+ {
+ return false;
+ }
+
+ // if no name was given, then don't check for it (they just want to
+ // know if this is a widget)
+ if ( !( name ) )
+ {
+ return true;
+ }
+
+ // quick name check on self
+ if ( $node.attr( 'id' ) === id )
+ {
+ return true;
+ }
+
+ // attempt to locate the element with the name we're looking for
+ var $named = $node.find( '[id="' + id + '"]' );
+ if ( $named.length === 0 )
+ {
+ return false;
+ }
+
+ return true;
+ }
+ },
+
+
+ /**
+ * Applies the style to all DOM elements that are descendants of $content
+ *
+ * This method operates off of a very basic concept. It takes an array of
+ * data containing a jQuery selector and applies the associated attributes
+ * to the elements. These attributes are non-standard - that is, they are
+ * not valid HTML attributes. Dojo then parses out these attributes and
+ * generates the dijit HTML, replacing the existing element.
+ *
+ * @param jQuery $content parent element containing elements to style
+ *
+ * @return ElementStyler self to support method chaining
+ */
+ apply: function( $content, parse )
+ {
+ parse = ( parse === undefined ) ? true : !!parse;
+
+ // if we're internal, show internal questions
+ if ( this._showInternal )
+ {
+ $content.find( '.hidden.i' ).removeClass( 'hidden' );
+ }
+
+ return this;
+ },
+
+
+ /**
+ * Called after the content is appended to the DOM for the first time
+ *
+ * This is used to do final processing for display. In this case, performing
+ * the actual styling based off of the previously set attributes.
+ *
+ * @return ElementStyler self to allow for method chaining
+ */
+ postAppend: function( $content )
+ {
+ return this;
+ },
+
+
+ /**
+ * Retrieves the id associated with the given element
+ *
+ * The problem is that Dijits do not always use a single element. They'll
+ * often have multiple elements in order to achieve a certain effect. The id
+ * may be on a different element than the one that contains the correct name
+ * attribute.
+ *
+ * This method will attempt to find the id by checking first the given
+ * element, then its siblings, followed finally by its children. Having to
+ * check for siblings is slower than accessing directly, and having to go so
+ * far as to check the children is the slowest. It is uncommon to have to
+ * check the children, so that check is performed last.
+ *
+ * @param jQuery $element element to get id of
+ *
+ * @return String element id or undefined
+ */
+ getIdFromElement: function( $element )
+ {
+ // check to see if the given element has the id we're looking for
+ // first, otherwise the siblings/children most likely contain the id
+ // we're looking for
+ return $element.attr( 'widgetid' )
+ || $element.attr( 'id' )
+ || $element.siblings().filter( '[id]' ).attr( 'id' )
+ || $element.children().filter( '[id]' ).attr( 'id' );
+ },
+
+
+ /**
+ * Gets the name from a given element
+ *
+ * This method is needed because the name attribute may exist on a different
+ * element than the one provided.
+ *
+ * @return String|undefined
+ */
+ getNameFromElement: function( $element )
+ {
+ if ( !( $element instanceof jQuery ) )
+ {
+ // assume it's an id
+ $element = $( '#' + $element );
+ }
+
+ return $element.attr( 'name' )
+ || $element.siblings().filter( '[name]' ).attr( 'name' )
+ || $element.children().filter( '[name]' ).attr( 'name' )
+ || '';
+ },
+
+
+ /**
+ * Returns the element with the name attribute
+ *
+ * This allows referencing the element that should be posted
+ *
+ * @return jQuery named element
+ */
+ getNameElement: function( $element )
+ {
+ // if the passed element has a name attribute, then no searching is
+ // needed
+ if ( $element.attr( 'name' ) )
+ {
+ return $element;
+ }
+
+ // attempt to find from siblings and children
+ return $element.siblings().filter( '[name]' )
+ || $element.children().filter( '[name]' )
+ },
+
+
+ 'public setOptions': function( name, index, options, val, $context )
+ {
+ var $element = this.getElementByName( name, index, null, $context );
+
+ // if the provided question is not a select, then we cannot add options
+ // to it---use the first index instead
+ if ( this._getElementType( name ) !== 'select' )
+ {
+ $element.val( ( options[ 0 ] || { value: '' } ).value );
+ return;
+ }
+
+ // store the old value
+ val = val || $element.val();
+
+ $element.html('');
+
+ var answer_data = this._selectData[ name ] = {};
+ for ( var item in options )
+ {
+ var opt = options[ item ],
+ opt_value = opt.value === undefined || opt.value === null ? '' : opt.value;
+
+ answer_data[ opt_value ] = opt.label;
+
+ $element.append(
+ $( '