diff --git a/.gitignore b/.gitignore
index acb26dd9060b7b39e188ec706608d4c3440afc3d..dd5c9cf18c7d6ef94ffd4f571f05c20c78f9992a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,6 +47,7 @@
 !/lib/plugins/popularity
 !/lib/plugins/revert
 !/lib/plugins/safefnrecode
+!/lib/plugins/styling
 !/lib/plugins/testing
 !/lib/plugins/usermanager
 !/lib/plugins/action.php
diff --git a/inc/html.php b/inc/html.php
index 1ccc056def5171cdc2cf36b53e64c3404dd433fa..0914a1762de1ba12bc0bff21d0c424f20b3c60da 100644
--- a/inc/html.php
+++ b/inc/html.php
@@ -2080,6 +2080,13 @@ function html_admin(){
                     $menu['config']['prompt'].'</a></div></li>');
         }
         unset($menu['config']);
+
+        if($menu['styling']){
+            ptln('  <li class="admin_styling"><div class="li">'.
+                '<a href="'.wl($ID, array('do' => 'admin','page' => 'styling')).'">'.
+                $menu['styling']['prompt'].'</a></div></li>');
+        }
+        unset($menu['styling']);
     }
     ptln('</ul>');
 
diff --git a/inc/template.php b/inc/template.php
index c7dea6b46d73aa42537289a63a536f0eb6a1aeab..f918d1a047019690c2f909dedb7f268db2243517 100644
--- a/inc/template.php
+++ b/inc/template.php
@@ -297,6 +297,7 @@ function tpl_metaheaders($alt = true) {
     // prepare seed for js and css
     $tseed   = $updateVersion;
     $depends = getConfigFiles('main');
+    $depends[] = DOKU_CONF."tpl/".$conf['template']."/style.ini";
     foreach($depends as $f) $tseed .= @filemtime($f);
     $tseed   = md5($tseed);
 
diff --git a/lib/exe/css.php b/lib/exe/css.php
index e0bc683123c73a319ad1c9e210b4051e620fbb2e..3b8a524bc76f088ce9de762eaf3a84d2482f9484 100644
--- a/lib/exe/css.php
+++ b/lib/exe/css.php
@@ -45,10 +45,10 @@ function css_out(){
     if(!$tpl) $tpl = $conf['template'];
 
     // The generated script depends on some dynamic options
-    $cache = new cache('styles'.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'].DOKU_BASE.$tpl.$type,'.css');
+    $cache = new cache('styles'.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'].$INPUT->int('preview').DOKU_BASE.$tpl.$type,'.css');
 
     // load styl.ini
-    $styleini = css_styleini($tpl);
+    $styleini = css_styleini($tpl, $INPUT->bool('preview'));
 
     // if old 'default' userstyle setting exists, make it 'screen' userstyle for backwards compatibility
     if (isset($config_cascade['userstyle']['default'])) {
@@ -62,6 +62,7 @@ function css_out(){
     $cache_files[] = $tplinc.'style.local.ini'; // @deprecated
     $cache_files[] = DOKU_CONF."tpl/$tpl/style.ini";
     $cache_files[] = __FILE__;
+    if($INPUT->bool('preview')) $cache_files[] = $conf['cachedir'].'/preview.ini';
 
     // Array of needed files and their web locations, the latter ones
     // are needed to fix relative paths in the stylesheets
@@ -261,9 +262,12 @@ function css_applystyle($css, $replacements) {
  * @author Andreas Gohr <andi@splitbrain.org>
  *
  * @param string $tpl the used template
+ * @param bool   $preview load preview replacements
  * @return array with keys 'stylesheets' and 'replacements'
  */
-function css_styleini($tpl) {
+function css_styleini($tpl, $preview=false) {
+    global $conf;
+
     $stylesheets = array(); // mode, file => base
     $replacements = array(); // placeholder => value
 
@@ -320,6 +324,19 @@ function css_styleini($tpl) {
         }
     }
 
+    // allow replacement overwrites in preview mode
+    if($preview) {
+        $webbase = DOKU_BASE;
+        $ini     = $conf['cachedir'].'/preview.ini';
+        if(file_exists($ini)) {
+            $data = parse_ini_file($ini, true);
+            // replacements
+            if(is_array($data['replacements'])) {
+                $replacements = array_merge($replacements, css_fixreplacementurls($data['replacements'], $webbase));
+            }
+        }
+    }
+
     return array(
         'stylesheets' => $stylesheets,
         'replacements' => $replacements
diff --git a/lib/images/admin/README b/lib/images/admin/README
index 90bab95784b001e0fe0c72c66521e84bdf2ae710..53e7d839ae988c9dc3d6a3d7c0f43a3482df3718 100644
--- a/lib/images/admin/README
+++ b/lib/images/admin/README
@@ -1,2 +1,4 @@
 These icons were taken from the nuvoX KDE icon theme and are GPL licensed
 See http://www.kde-look.org/content/show.php/nuvoX?content=38467
+
+styling.png from https://openclipart.org/detail/25595/brush Public Domain
diff --git a/lib/images/admin/styling.png b/lib/images/admin/styling.png
new file mode 100644
index 0000000000000000000000000000000000000000..859c8c9ef4f465333cae13ea7a33c50af4ee3d8a
Binary files /dev/null and b/lib/images/admin/styling.png differ
diff --git a/lib/plugins/extension/helper/extension.php b/lib/plugins/extension/helper/extension.php
index 719249fbefb0a4f1d44d5d0f229fbb5a468c95d4..c2384080540e69873ff201ee43f5645f4d28cef3 100644
--- a/lib/plugins/extension/helper/extension.php
+++ b/lib/plugins/extension/helper/extension.php
@@ -108,7 +108,7 @@ class helper_plugin_extension_extension extends DokuWiki_Plugin {
         return in_array($this->id,
                         array(
                             'authad', 'authldap', 'authmysql', 'authpgsql', 'authplain', 'acl', 'info', 'extension',
-                            'revert', 'popularity', 'config', 'safefnrecode', 'testing', 'template:dokuwiki'
+                            'revert', 'popularity', 'config', 'safefnrecode', 'styling', 'testing', 'template:dokuwiki'
                         )
         );
     }
diff --git a/lib/plugins/styling/.travis.yml b/lib/plugins/styling/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..75ee0b152cfdd19356b7add7f6e8231a215f7677
--- /dev/null
+++ b/lib/plugins/styling/.travis.yml
@@ -0,0 +1,13 @@
+# Config file for travis-ci.org
+
+language: php
+php:
+  - "5.5"
+  - "5.4"
+  - "5.3"
+env:
+  - DOKUWIKI=master
+  - DOKUWIKI=stable
+before_install: wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh
+install: sh travis.sh
+script: cd _test && phpunit --stderr --group plugin_styling
diff --git a/lib/plugins/styling/README b/lib/plugins/styling/README
new file mode 100644
index 0000000000000000000000000000000000000000..a1a5e890cfa237335a96f0d07c03801592db17ef
--- /dev/null
+++ b/lib/plugins/styling/README
@@ -0,0 +1,27 @@
+styling Plugin for DokuWiki
+
+Allows to edit style.ini replacements
+
+All documentation for this plugin can be found at
+https://www.dokuwiki.org/plugin:styling
+
+If you install this plugin manually, make sure it is installed in
+lib/plugins/styling/ - if the folder is called different it
+will not work!
+
+Please refer to http://www.dokuwiki.org/plugins for additional info
+on how to install plugins in DokuWiki.
+
+----
+Copyright (C) Andreas Gohr <andi@splitbrain.org>
+
+This program 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; version 2 of the License
+
+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.
+
+See the COPYING file in your DokuWiki folder for details
diff --git a/lib/plugins/styling/_test/general.test.php b/lib/plugins/styling/_test/general.test.php
new file mode 100644
index 0000000000000000000000000000000000000000..1337f6f759e77f1d88b6a21165c2b3c60cb50030
--- /dev/null
+++ b/lib/plugins/styling/_test/general.test.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * General tests for the styling plugin
+ *
+ * @group plugin_styling
+ * @group plugins
+ */
+class general_plugin_styling_test extends DokuWikiTest {
+
+    /**
+     * Simple test to make sure the plugin.info.txt is in correct format
+     */
+    public function test_plugininfo() {
+        $file = __DIR__.'/../plugin.info.txt';
+        $this->assertFileExists($file);
+
+        $info = confToHash($file);
+
+        $this->assertArrayHasKey('base', $info);
+        $this->assertArrayHasKey('author', $info);
+        $this->assertArrayHasKey('email', $info);
+        $this->assertArrayHasKey('date', $info);
+        $this->assertArrayHasKey('name', $info);
+        $this->assertArrayHasKey('desc', $info);
+        $this->assertArrayHasKey('url', $info);
+
+        $this->assertEquals('styling', $info['base']);
+        $this->assertRegExp('/^https?:\/\//', $info['url']);
+        $this->assertTrue(mail_isvalid($info['email']));
+        $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
+        $this->assertTrue(false !== strtotime($info['date']));
+    }
+}
diff --git a/lib/plugins/styling/action.php b/lib/plugins/styling/action.php
new file mode 100644
index 0000000000000000000000000000000000000000..896e14bef8ed60d137ea28591760688bfe30d236
--- /dev/null
+++ b/lib/plugins/styling/action.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * DokuWiki Plugin styling (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author  Andreas Gohr <andi@splitbrain.org>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Class action_plugin_styling
+ *
+ * This handles all the save actions and loading the interface
+ *
+ * All this usually would be done within an admin plugin, but we want to have this available outside
+ * the admin interface using our floating dialog.
+ */
+class action_plugin_styling extends DokuWiki_Action_Plugin {
+
+    /**
+     * Registers a callback functions
+     *
+     * @param Doku_Event_Handler $controller DokuWiki's event controller object
+     * @return void
+     */
+    public function register(Doku_Event_Handler $controller) {
+        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'handle_header');
+    }
+
+    /**
+     * Adds the preview parameter to the stylesheet loading in non-js mode
+     *
+     * @param Doku_Event $event  event object by reference
+     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
+     *                           handler was registered]
+     * @return void
+     */
+    public function handle_header(Doku_Event &$event, $param) {
+        global $ACT;
+        global $INPUT;
+        if($ACT != 'admin' || $INPUT->str('page') != 'styling') return;
+        if(!auth_isadmin()) return;
+
+        // set preview
+        $len = count($event->data['link']);
+        for($i = 0; $i < $len; $i++) {
+            if(
+                $event->data['link'][$i]['rel'] == 'stylesheet' &&
+                strpos($event->data['link'][$i]['href'], 'lib/exe/css.php') !== false
+            ) {
+                $event->data['link'][$i]['href'] .= '&preview=1&tseed='.time();
+            }
+        }
+    }
+
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/lib/plugins/styling/admin.php b/lib/plugins/styling/admin.php
new file mode 100644
index 0000000000000000000000000000000000000000..c747c3130e1c1d4cc609f3c5d855db1eda451d7b
--- /dev/null
+++ b/lib/plugins/styling/admin.php
@@ -0,0 +1,211 @@
+<?php
+/**
+ * DokuWiki Plugin styling (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author  Andreas Gohr <andi@splitbrain.org>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+class admin_plugin_styling extends DokuWiki_Admin_Plugin {
+
+    public $ispopup = false;
+
+    /**
+     * @return int sort number in admin menu
+     */
+    public function getMenuSort() {
+        return 1000;
+    }
+
+    /**
+     * @return bool true if only access for superuser, false is for superusers and moderators
+     */
+    public function forAdminOnly() {
+        return true;
+    }
+
+    /**
+     * handle the different actions (also called from ajax)
+     */
+    public function handle() {
+        global $INPUT;
+        $run = $INPUT->extract('run')->str('run');
+        if(!$run) return;
+        $run = "run_$run";
+        $this->$run();
+    }
+
+    /**
+     * Render HTML output, e.g. helpful text and a form
+     */
+    public function html() {
+        $class = 'nopopup';
+        if($this->ispopup) $class = 'ispopup page';
+
+        echo '<div id="plugin__styling" class="'.$class.'">';
+        ptln('<h1>'.$this->getLang('menu').'</h1>');
+        $this->form();
+        echo '</div>';
+    }
+
+    /**
+     * Create the actual editing form
+     */
+    public function form() {
+        global $conf;
+        global $ID;
+        define('SIMPLE_TEST', 1); // hack, ideally certain functions should be moved out of css.php
+        require_once(DOKU_INC.'lib/exe/css.php');
+        $styleini     = css_styleini($conf['template'], true);
+        $replacements = $styleini['replacements'];
+
+        if($this->ispopup) {
+            $target = DOKU_BASE.'lib/plugins/styling/popup.php';
+        } else {
+            $target = wl($ID, array('do' => 'admin', 'page' => 'styling'));
+        }
+
+        if(empty($replacements)) {
+            echo '<p class="error">'.$this->getLang('error').'</p>';
+        } else {
+            echo $this->locale_xhtml('intro');
+
+            echo '<form class="styling" method="post" action="'.$target.'">';
+
+            echo '<table><tbody>';
+            foreach($replacements as $key => $value) {
+                $name = tpl_getLang($key);
+                if(empty($name)) $name = $this->getLang($key);
+                if(empty($name)) $name = $key;
+
+                echo '<tr>';
+                echo '<td><label for="tpl__'.hsc($key).'">'.$name.'</label></td>';
+                echo '<td><input type="text" name="tpl['.hsc($key).']" id="tpl__'.hsc($key).'" value="'.hsc($value).'" '.$this->colorClass($key).' dir="ltr" /></td>';
+                echo '</tr>';
+            }
+            echo '</tbody></table>';
+
+            echo '<p>';
+            echo '<button type="submit" name="run[preview]" class="btn_preview primary">'.$this->getLang('btn_preview').'</button> ';
+            echo '<button type="submit" name="run[reset]">'.$this->getLang('btn_reset').'</button>'; #FIXME only if preview.ini exists
+            echo '</p>';
+
+            echo '<p>';
+            echo '<button type="submit" name="run[save]" class="primary">'.$this->getLang('btn_save').'</button>';
+            echo '</p>';
+
+            echo '<p>';
+            echo '<button type="submit" name="run[revert]">'.$this->getLang('btn_revert').'</button>'; #FIXME only if local.ini exists
+            echo '</p>';
+
+            echo '</form>';
+
+            echo tpl_locale_xhtml('style');
+
+        }
+    }
+
+    /**
+     * set the color class attribute
+     */
+    protected function colorClass($key) {
+        static $colors = array(
+            'text',
+            'background',
+            'text_alt',
+            'background_alt',
+            'text_neu',
+            'background_neu',
+            'border',
+            'highlight',
+            'background_site',
+            'link',
+            'existing',
+            'missing',
+        );
+
+        if(preg_match('/colou?r/', $key) || in_array(trim($key,'_'), $colors)) {
+            return 'class="color"';
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * saves the preview.ini (alos called from ajax directly)
+     */
+    public function run_preview() {
+        global $conf;
+        $ini = $conf['cachedir'].'/preview.ini';
+        io_saveFile($ini, $this->makeini());
+    }
+
+    /**
+     * deletes the preview.ini
+     */
+    protected function run_reset() {
+        global $conf;
+        $ini = $conf['cachedir'].'/preview.ini';
+        io_saveFile($ini, '');
+    }
+
+    /**
+     * deletes the local style.ini replacements
+     */
+    protected function run_revert() {
+        $this->replaceini('');
+        $this->run_reset();
+    }
+
+    /**
+     * save the local style.ini replacements
+     */
+    protected function run_save() {
+        $this->replaceini($this->makeini());
+        $this->run_reset();
+    }
+
+    /**
+     * create the replacement part of a style.ini from submitted data
+     *
+     * @return string
+     */
+    protected function makeini() {
+        global $INPUT;
+
+        $ini = "[replacements]\n";
+        $ini .= ";These overwrites have been generated from the Template styling Admin interface\n";
+        $ini .= ";Any values in this section will be overwritten by that tool again\n";
+        foreach($INPUT->arr('tpl') as $key => $val) {
+            $ini .= $key.' = "'.addslashes($val).'"'."\n";
+        }
+
+        return $ini;
+    }
+
+    /**
+     * replaces the replacement parts in the local ini
+     *
+     * @param string $new the new ini contents
+     */
+    protected function replaceini($new) {
+        global $conf;
+        $ini = DOKU_CONF."tpl/".$conf['template']."/style.ini";
+        if(file_exists($ini)) {
+            $old = io_readFile($ini);
+            $old = preg_replace('/\[replacements\]\n.*?(\n\[.*]|$)/s', '\\1', $old);
+            $old = trim($old);
+        } else {
+            $old = '';
+        }
+
+        io_makeFileDir($ini);
+        io_saveFile($ini, "$old\n\n$new");
+    }
+
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/lib/plugins/styling/iris.js b/lib/plugins/styling/iris.js
new file mode 100644
index 0000000000000000000000000000000000000000..4eda5022e3577ec57b448dc0e97e8ab2be6186d4
--- /dev/null
+++ b/lib/plugins/styling/iris.js
@@ -0,0 +1,1488 @@
+/*! Iris Color Picker - v1.0.7 - 2014-11-28
+* https://github.com/Automattic/Iris
+* Copyright (c) 2014 Matt Wiebe; Licensed GPLv2 */
+(function( $, undef ){
+	var _html, nonGradientIE, gradientType, vendorPrefixes, _css, Iris, UA, isIE, IEVersion;
+
+	_html = '<div class="iris-picker"><div class="iris-picker-inner"><div class="iris-square"><a class="iris-square-value" href="#"><span class="iris-square-handle ui-slider-handle"></span></a><div class="iris-square-inner iris-square-horiz"></div><div class="iris-square-inner iris-square-vert"></div></div><div class="iris-slider iris-strip"><div class="iris-slider-offset"></div></div></div></div>';
+	_css = '.iris-picker{display:block;position:relative}.iris-picker,.iris-picker *{-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input+.iris-picker{margin-top:4px}.iris-error{background-color:#ffafaf}.iris-border{border-radius:3px;border:1px solid #aaa;width:200px;background-color:#fff}.iris-picker-inner{position:absolute;top:0;right:0;left:0;bottom:0}.iris-border .iris-picker-inner{top:10px;right:10px;left:10px;bottom:10px}.iris-picker .iris-square-inner{position:absolute;left:0;right:0;top:0;bottom:0}.iris-picker .iris-square,.iris-picker .iris-slider,.iris-picker .iris-square-inner,.iris-picker .iris-palette{border-radius:3px;box-shadow:inset 0 0 5px rgba(0,0,0,.4);height:100%;width:12.5%;float:left;margin-right:5%}.iris-picker .iris-square{width:76%;margin-right:10%;position:relative}.iris-picker .iris-square-inner{width:auto;margin:0}.iris-ie-9 .iris-square,.iris-ie-9 .iris-slider,.iris-ie-9 .iris-square-inner,.iris-ie-9 .iris-palette{box-shadow:none;border-radius:0}.iris-ie-9 .iris-square,.iris-ie-9 .iris-slider,.iris-ie-9 .iris-palette{outline:1px solid rgba(0,0,0,.1)}.iris-ie-lt9 .iris-square,.iris-ie-lt9 .iris-slider,.iris-ie-lt9 .iris-square-inner,.iris-ie-lt9 .iris-palette{outline:1px solid #aaa}.iris-ie-lt9 .iris-square .ui-slider-handle{outline:1px solid #aaa;background-color:#fff;-ms-filter:"alpha(Opacity=30)"}.iris-ie-lt9 .iris-square .iris-square-handle{background:0;border:3px solid #fff;-ms-filter:"alpha(Opacity=50)"}.iris-picker .iris-strip{margin-right:0;position:relative}.iris-picker .iris-strip .ui-slider-handle{position:absolute;background:0;margin:0;right:-3px;left:-3px;border:4px solid #aaa;border-width:4px 3px;width:auto;height:6px;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.2);opacity:.9;z-index:5;cursor:ns-resize}.iris-strip .ui-slider-handle:before{content:" ";position:absolute;left:-2px;right:-2px;top:-3px;bottom:-3px;border:2px solid #fff;border-radius:3px}.iris-picker .iris-slider-offset{position:absolute;top:11px;left:0;right:0;bottom:-3px;width:auto;height:auto;background:transparent;border:0;border-radius:0}.iris-picker .iris-square-handle{background:transparent;border:5px solid #aaa;border-radius:50%;border-color:rgba(128,128,128,.5);box-shadow:none;width:12px;height:12px;position:absolute;left:-10px;top:-10px;cursor:move;opacity:1;z-index:10}.iris-picker .ui-state-focus .iris-square-handle{opacity:.8}.iris-picker .iris-square-handle:hover{border-color:#999}.iris-picker .iris-square-value:focus .iris-square-handle{box-shadow:0 0 2px rgba(0,0,0,.75);opacity:.8}.iris-picker .iris-square-handle:hover::after{border-color:#fff}.iris-picker .iris-square-handle::after{position:absolute;bottom:-4px;right:-4px;left:-4px;top:-4px;border:3px solid #f9f9f9;border-color:rgba(255,255,255,.8);border-radius:50%;content:" "}.iris-picker .iris-square-value{width:8px;height:8px;position:absolute}.iris-ie-lt9 .iris-square-value,.iris-mozilla .iris-square-value{width:1px;height:1px}.iris-palette-container{position:absolute;bottom:0;left:0;margin:0;padding:0}.iris-border .iris-palette-container{left:10px;bottom:10px}.iris-picker .iris-palette{margin:0;cursor:pointer}.iris-square-handle,.ui-slider-handle{border:0;outline:0}';
+
+	// Even IE9 dosen't support gradients. Elaborate sigh.
+	UA = navigator.userAgent.toLowerCase();
+	isIE = navigator.appName === 'Microsoft Internet Explorer';
+	IEVersion = isIE ? parseFloat( UA.match( /msie ([0-9]{1,}[\.0-9]{0,})/ )[1] ) : 0;
+	nonGradientIE = ( isIE && IEVersion < 10 );
+	gradientType = false;
+
+	// we don't bother with an unprefixed version, as it has a different syntax
+	vendorPrefixes = [ '-moz-', '-webkit-', '-o-', '-ms-' ];
+
+	// Bail for IE <= 7
+	if ( nonGradientIE && IEVersion <= 7 ) {
+		$.fn.iris = $.noop;
+		$.support.iris = false;
+		return;
+	}
+
+	$.support.iris = true;
+
+	function testGradientType() {
+		var el, base,
+			bgImageString = 'backgroundImage';
+
+		if ( nonGradientIE ) {
+			gradientType = 'filter';
+		}
+		else {
+			el = $( '<div id="iris-gradtest" />' );
+			base = 'linear-gradient(top,#fff,#000)';
+			$.each( vendorPrefixes, function( i, val ){
+				el.css( bgImageString, val + base );
+				if ( el.css( bgImageString ).match( 'gradient' ) ) {
+					gradientType = i;
+					return false;
+				}
+			});
+			// check for legacy webkit gradient syntax
+			if ( gradientType === false ) {
+				el.css( 'background', '-webkit-gradient(linear,0% 0%,0% 100%,from(#fff),to(#000))' );
+				if ( el.css( bgImageString ).match( 'gradient' ) ) {
+					gradientType = 'webkit';
+				}
+			}
+			el.remove();
+		}
+
+	}
+
+	/**
+	* Only for CSS3 gradients. oldIE will use a separate function.
+	*
+	* Accepts as many color stops as necessary from 2nd arg on, or 2nd
+	* arg can be an array of color stops
+	*
+	* @param  {string} origin Gradient origin - top or left, defaults to left.
+	* @return {string}        Appropriate CSS3 gradient string for use in
+	*/
+	function createGradient( origin, stops ) {
+		origin = ( origin === 'top' ) ? 'top' : 'left';
+		stops = $.isArray( stops ) ? stops : Array.prototype.slice.call( arguments, 1 );
+		if ( gradientType === 'webkit' ) {
+			return legacyWebkitGradient( origin, stops );
+		} else {
+			return vendorPrefixes[ gradientType ] + 'linear-gradient(' + origin + ', ' + stops.join(', ') + ')';
+		}
+	}
+
+	/**
+	* Stupid gradients for a stupid browser.
+	*/
+	function stupidIEGradient( origin, stops ) {
+		var type, self, lastIndex, filter, startPosProp, endPosProp, dimensionProp, template, html;
+
+		origin = ( origin === 'top' ) ? 'top' : 'left';
+		stops = $.isArray( stops ) ? stops : Array.prototype.slice.call( arguments, 1 );
+		// 8 hex: AARRGGBB
+		// GradientType: 0 vertical, 1 horizontal
+		type = ( origin === 'top' ) ? 0 : 1;
+		self = $( this );
+		lastIndex = stops.length - 1;
+		filter = 'filter';
+		startPosProp = ( type === 1 ) ? 'left' : 'top';
+		endPosProp = ( type === 1 ) ? 'right' : 'bottom';
+		dimensionProp = ( type === 1 ) ? 'height' : 'width';
+		template = '<div class="iris-ie-gradient-shim" style="position:absolute;' + dimensionProp + ':100%;' + startPosProp + ':%start%;' + endPosProp + ':%end%;' + filter + ':%filter%;" data-color:"%color%"></div>';
+		html = '';
+		// need a positioning context
+		if ( self.css('position') === 'static' ) {
+			self.css( {position: 'relative' } );
+		}
+
+		stops = fillColorStops( stops );
+		$.each(stops, function( i, startColor ) {
+			var endColor, endStop, filterVal;
+
+			// we want two at a time. if we're on the last pair, bail.
+			if ( i === lastIndex ) {
+				return false;
+			}
+
+			endColor = stops[ i + 1 ];
+			//if our pairs are at the same color stop, moving along.
+			if ( startColor.stop === endColor.stop ) {
+				return;
+			}
+
+			endStop = 100 - parseFloat( endColor.stop ) + '%';
+			startColor.octoHex = new Color( startColor.color ).toIEOctoHex();
+			endColor.octoHex = new Color( endColor.color ).toIEOctoHex();
+
+			filterVal = 'progid:DXImageTransform.Microsoft.Gradient(GradientType=' + type + ', StartColorStr=\'' + startColor.octoHex + '\', EndColorStr=\'' + endColor.octoHex + '\')';
+			html += template.replace( '%start%', startColor.stop ).replace( '%end%', endStop ).replace( '%filter%', filterVal );
+		});
+		self.find( '.iris-ie-gradient-shim' ).remove();
+		$( html ).prependTo( self );
+	}
+
+	function legacyWebkitGradient( origin, colorList ) {
+		var stops = [];
+		origin = ( origin === 'top' ) ? '0% 0%,0% 100%,' : '0% 100%,100% 100%,';
+		colorList = fillColorStops( colorList );
+		$.each( colorList, function( i, val ){
+			stops.push( 'color-stop(' + ( parseFloat( val.stop ) / 100 ) + ', ' + val.color + ')' );
+		});
+		return '-webkit-gradient(linear,' + origin + stops.join(',') + ')';
+	}
+
+	function fillColorStops( colorList ) {
+		var colors = [],
+			percs = [],
+			newColorList = [],
+			lastIndex = colorList.length - 1;
+
+		$.each( colorList, function( index, val ) {
+			var color = val,
+				perc = false,
+				match = val.match( /1?[0-9]{1,2}%$/ );
+
+			if ( match ) {
+				color = val.replace( /\s?1?[0-9]{1,2}%$/, '' );
+				perc = match.shift();
+			}
+			colors.push( color );
+			percs.push( perc );
+		});
+
+		// back fill first and last
+		if ( percs[0] === false ) {
+			percs[0] = '0%';
+		}
+
+		if ( percs[lastIndex] === false ) {
+			percs[lastIndex] = '100%';
+		}
+
+		percs = backFillColorStops( percs );
+
+		$.each( percs, function( i ){
+			newColorList[i] = { color: colors[i], stop: percs[i] };
+		});
+		return newColorList;
+	}
+
+	function backFillColorStops( stops ) {
+		var first = 0,
+			last = stops.length - 1,
+			i = 0,
+			foundFirst = false,
+			incr,
+			steps,
+			step,
+			firstVal;
+
+		if ( stops.length <= 2 || $.inArray( false, stops ) < 0 ) {
+			return stops;
+		}
+		while ( i < stops.length - 1 ) {
+			if ( ! foundFirst && stops[i] === false ) {
+				first = i - 1;
+				foundFirst = true;
+			} else if ( foundFirst && stops[i] !== false ) {
+				last = i;
+				i = stops.length;
+			}
+			i++;
+		}
+		steps = last - first;
+		firstVal = parseInt( stops[first].replace('%'), 10 );
+		incr = ( parseFloat( stops[last].replace('%') ) - firstVal ) / steps;
+		i = first + 1;
+		step = 1;
+		while ( i < last ) {
+			stops[i] = ( firstVal + ( step * incr ) ) + '%';
+			step++;
+			i++;
+		}
+		return backFillColorStops( stops );
+	}
+
+	$.fn.gradient = function() {
+		var args = arguments;
+		return this.each( function() {
+			// this'll be oldishIE
+			if ( nonGradientIE ) {
+				stupidIEGradient.apply( this, args );
+			} else {
+				// new hotness
+				$( this ).css( 'backgroundImage', createGradient.apply( this, args ) );
+			}
+		});
+	};
+
+	$.fn.raninbowGradient = function( origin, args ) {
+		var opts, template, i, steps;
+
+		origin = origin || 'top';
+		opts = $.extend( {}, { s: 100, l: 50 }, args );
+		template = 'hsl(%h%,' + opts.s + '%,' + opts.l + '%)';
+		i = 0;
+		steps = [];
+		while ( i <= 360 ) {
+			steps.push( template.replace('%h%', i) );
+			i += 30;
+		}
+		return this.each(function() {
+			$(this).gradient( origin, steps );
+		});
+	};
+
+	// the colorpicker widget def.
+	Iris = {
+		options: {
+			color: false,
+			mode: 'hsl',
+			controls: {
+				horiz: 's', // horizontal defaults to saturation
+				vert: 'l', // vertical defaults to lightness
+				strip: 'h' // right strip defaults to hue
+			},
+			hide: true, // hide the color picker by default
+			border: true, // draw a border around the collection of UI elements
+			target: false, // a DOM element / jQuery selector that the element will be appended within. Only used when called on an input.
+			width: 200, // the width of the collection of UI elements
+			palettes: false // show a palette of basic colors beneath the square.
+		},
+		_color: '',
+		_palettes: [ '#000', '#fff', '#d33', '#d93', '#ee2', '#81d742', '#1e73be', '#8224e3' ],
+		_inited: false,
+		_defaultHSLControls: {
+			horiz: 's',
+			vert: 'l',
+			strip: 'h'
+		},
+		_defaultHSVControls: {
+			horiz: 'h',
+			vert: 'v',
+			strip: 's'
+		},
+		_scale: {
+			h: 360,
+			s: 100,
+			l: 100,
+			v: 100
+		},
+		_create: function() {
+			var self = this,
+				el = self.element,
+				color = self.options.color || el.val();
+
+			if ( gradientType === false ) {
+				testGradientType();
+			}
+
+			if ( el.is( 'input' ) ) {
+				if ( self.options.target ) {
+					self.picker = $( _html ).appendTo( self.options.target );
+				} else {
+					self.picker = $( _html ).insertAfter( el );
+				}
+
+				self._addInputListeners( el );
+			} else {
+				el.append( _html );
+				self.picker = el.find( '.iris-picker' );
+			}
+
+			// Browsers / Versions
+			// Feature detection doesn't work for these, and $.browser is deprecated
+			if ( isIE ) {
+				if ( IEVersion === 9 ) {
+					self.picker.addClass( 'iris-ie-9' );
+				} else if ( IEVersion <= 8 ) {
+					self.picker.addClass( 'iris-ie-lt9' );
+				}
+			} else if ( UA.indexOf('compatible') < 0 && UA.indexOf('khtml') < 0 && UA.match( /mozilla/ ) ) {
+				self.picker.addClass( 'iris-mozilla' );
+			}
+
+			if ( self.options.palettes ) {
+				self._addPalettes();
+			}
+
+			self._color = new Color( color ).setHSpace( self.options.mode );
+			self.options.color = self._color.toString();
+
+			// prep 'em for re-use
+			self.controls = {
+				square:      self.picker.find( '.iris-square' ),
+				squareDrag:  self.picker.find( '.iris-square-value' ),
+				horiz:       self.picker.find( '.iris-square-horiz' ),
+				vert:        self.picker.find( '.iris-square-vert' ),
+				strip:       self.picker.find( '.iris-strip' ),
+				stripSlider: self.picker.find( '.iris-strip .iris-slider-offset' )
+			};
+
+			// small sanity check - if we chose hsv, change default controls away from hsl
+			if ( self.options.mode === 'hsv' && self._has('l', self.options.controls) ) {
+				self.options.controls = self._defaultHSVControls;
+			} else if ( self.options.mode === 'hsl' && self._has('v', self.options.controls) ) {
+				self.options.controls = self._defaultHSLControls;
+			}
+
+			// store it. HSL gets squirrely
+			self.hue = self._color.h();
+
+			if ( self.options.hide ) {
+				self.picker.hide();
+			}
+
+			if ( self.options.border ) {
+				self.picker.addClass( 'iris-border' );
+			}
+
+			self._initControls();
+			self.active = 'external';
+			self._dimensions();
+			self._change();
+		},
+		_has: function(needle, haystack) {
+			var ret = false;
+			$.each(haystack, function(i,v){
+				if ( needle === v ) {
+					ret = true;
+					// exit the loop
+					return false;
+				}
+			});
+			return ret;
+		},
+		_addPalettes: function () {
+			var container = $( '<div class="iris-palette-container" />' ),
+				palette = $( '<a class="iris-palette" tabindex="0" />' ),
+				colors = $.isArray( this.options.palettes ) ? this.options.palettes : this._palettes;
+
+			// do we have an existing container? Empty and reuse it.
+			if ( this.picker.find( '.iris-palette-container' ).length ) {
+				container = this.picker.find( '.iris-palette-container' ).detach().html( '' );
+			}
+
+			$.each(colors, function(index, val) {
+				palette.clone().data( 'color', val )
+					.css( 'backgroundColor', val ).appendTo( container )
+					.height( 10 ).width( 10 );
+			});
+
+			this.picker.append(container);
+		},
+		_paint: function() {
+			var self = this;
+			self._paintDimension( 'top', 'strip' );
+			self._paintDimension( 'top', 'vert' );
+			self._paintDimension( 'left', 'horiz' );
+		},
+		_paintDimension: function( origin, control ) {
+			var self = this,
+				c = self._color,
+				mode = self.options.mode,
+				color = self._getHSpaceColor(),
+				target = self.controls[ control ],
+				controlOpts = self.options.controls,
+				stops;
+
+			// don't paint the active control
+			if ( control === self.active || ( self.active === 'square' && control !== 'strip' ) ) {
+				return;
+			}
+
+			switch ( controlOpts[ control ] ) {
+				case 'h':
+					if ( mode === 'hsv' ) {
+						color = c.clone();
+						switch ( control ) {
+							case 'horiz':
+								color[controlOpts.vert](100);
+								break;
+							case 'vert':
+								color[controlOpts.horiz](100);
+								break;
+							case 'strip':
+								color.setHSpace('hsl');
+								break;
+						}
+						stops = color.toHsl();
+					} else {
+						if ( control === 'strip' ) {
+							stops = { s: color.s, l: color.l };
+						} else {
+							stops = { s: 100, l: color.l };
+						}
+					}
+
+					target.raninbowGradient( origin, stops );
+					break;
+				case 's':
+					if ( mode === 'hsv' ) {
+						if ( control === 'vert' ) {
+							stops = [ c.clone().a(0).s(0).toCSS('rgba'), c.clone().a(1).s(0).toCSS('rgba') ];
+						} else if ( control === 'strip' ) {
+							stops = [ c.clone().s(100).toCSS('hsl'), c.clone().s(0).toCSS('hsl') ];
+						} else if ( control === 'horiz' ) {
+							stops = [ '#fff', 'hsl(' + color.h + ',100%,50%)' ];
+						}
+					} else { // implicit mode === 'hsl'
+						if ( control === 'vert' && self.options.controls.horiz === 'h' ) {
+							stops = ['hsla(0, 0%, ' + color.l + '%, 0)', 'hsla(0, 0%, ' + color.l + '%, 1)'];
+						} else {
+							stops = ['hsl('+ color.h +',0%,50%)', 'hsl(' + color.h + ',100%,50%)'];
+						}
+					}
+
+
+					target.gradient( origin, stops );
+					break;
+				case 'l':
+					if ( control === 'strip' ) {
+						stops = ['hsl(' + color.h + ',100%,100%)', 'hsl(' + color.h + ', ' + color.s + '%,50%)', 'hsl('+ color.h +',100%,0%)'];
+					} else {
+						stops = ['#fff', 'rgba(255,255,255,0) 50%', 'rgba(0,0,0,0) 50%', 'rgba(0,0,0,1)'];
+					}
+					target.gradient( origin, stops );
+					break;
+				case 'v':
+						if ( control === 'strip' ) {
+							stops = [ c.clone().v(100).toCSS(), c.clone().v(0).toCSS() ];
+						} else {
+							stops = ['rgba(0,0,0,0)', '#000'];
+						}
+						target.gradient( origin, stops );
+					break;
+				default:
+					break;
+			}
+		},
+
+		_getHSpaceColor: function() {
+			return ( this.options.mode === 'hsv' ) ? this._color.toHsv() : this._color.toHsl();
+		},
+
+		_dimensions: function( reset ) {
+			// whatever size
+			var self = this,
+				opts = self.options,
+				controls = self.controls,
+				square = controls.square,
+				strip = self.picker.find( '.iris-strip' ),
+				squareWidth = '77.5%',
+				stripWidth = '12%',
+				totalPadding = 20,
+				innerWidth = opts.border ? opts.width - totalPadding : opts.width,
+				controlsHeight,
+				paletteCount = $.isArray( opts.palettes ) ? opts.palettes.length : self._palettes.length,
+				paletteMargin, paletteWidth, paletteContainerWidth;
+
+			if ( reset ) {
+				square.css( 'width', '' );
+				strip.css( 'width', '' );
+				self.picker.css( {width: '', height: ''} );
+			}
+
+			squareWidth = innerWidth * ( parseFloat( squareWidth ) / 100 );
+			stripWidth = innerWidth * ( parseFloat( stripWidth ) / 100 );
+			controlsHeight = opts.border ? squareWidth + totalPadding : squareWidth;
+
+			square.width( squareWidth ).height( squareWidth );
+			strip.height( squareWidth ).width( stripWidth );
+			self.picker.css( { width: opts.width, height: controlsHeight } );
+
+			if ( ! opts.palettes ) {
+				return self.picker.css( 'paddingBottom', '' );
+			}
+
+			// single margin at 2%
+			paletteMargin = squareWidth * 2 / 100;
+			paletteContainerWidth = squareWidth - ( ( paletteCount - 1 ) * paletteMargin );
+			paletteWidth = paletteContainerWidth / paletteCount;
+			self.picker.find('.iris-palette').each( function( i ) {
+				var margin = i === 0 ? 0 : paletteMargin;
+				$( this ).css({
+					width: paletteWidth,
+					height: paletteWidth,
+					marginLeft: margin
+				});
+			});
+			self.picker.css( 'paddingBottom', paletteWidth + paletteMargin );
+			strip.height( paletteWidth + paletteMargin + squareWidth );
+		},
+
+		_addInputListeners: function( input ) {
+			var self = this,
+				debounceTimeout = 100,
+				callback = function( event ){
+					var color = new Color( input.val() ),
+						val = input.val().replace( /^#/, '' );
+
+					input.removeClass( 'iris-error' );
+					// we gave a bad color
+					if ( color.error ) {
+						// don't error on an empty input - we want those allowed
+						if ( val !== '' ) {
+							input.addClass( 'iris-error' );
+						}
+					} else {
+						if ( color.toString() !== self._color.toString() ) {
+							// let's not do this on keyup for hex shortcodes
+							if ( ! ( event.type === 'keyup' && val.match( /^[0-9a-fA-F]{3}$/ ) ) ) {
+								self._setOption( 'color', color.toString() );
+							}
+						}
+					}
+				};
+
+			input.on( 'change', callback ).on( 'keyup', self._debounce( callback, debounceTimeout ) );
+
+			// If we initialized hidden, show on first focus. The rest is up to you.
+			if ( self.options.hide ) {
+				input.one( 'focus', function() {
+					self.show();
+				});
+			}
+		},
+
+		_initControls: function() {
+			var self = this,
+				controls = self.controls,
+				square = controls.square,
+				controlOpts = self.options.controls,
+				stripScale = self._scale[controlOpts.strip];
+
+			controls.stripSlider.slider({
+				orientation: 'vertical',
+				max: stripScale,
+				slide: function( event, ui ) {
+					self.active = 'strip';
+					// "reverse" for hue.
+					if ( controlOpts.strip === 'h' ) {
+						ui.value = stripScale - ui.value;
+					}
+
+					self._color[controlOpts.strip]( ui.value );
+					self._change.apply( self, arguments );
+				}
+			});
+
+			controls.squareDrag.draggable({
+				containment: controls.square.find( '.iris-square-inner' ),
+				zIndex: 1000,
+				cursor: 'move',
+				drag: function( event, ui ) {
+					self._squareDrag( event, ui );
+				},
+				start: function() {
+					square.addClass( 'iris-dragging' );
+					$(this).addClass( 'ui-state-focus' );
+				},
+				stop: function() {
+					square.removeClass( 'iris-dragging' );
+					$(this).removeClass( 'ui-state-focus' );
+				}
+			}).on( 'mousedown mouseup', function( event ) {
+				var focusClass = 'ui-state-focus';
+				event.preventDefault();
+				if (event.type === 'mousedown' ) {
+					self.picker.find( '.' + focusClass ).removeClass( focusClass ).blur();
+					$(this).addClass( focusClass ).focus();
+				} else {
+					$(this).removeClass( focusClass );
+				}
+			}).on( 'keydown', function( event ) {
+				var container = controls.square,
+					draggable = controls.squareDrag,
+					position = draggable.position(),
+					distance = self.options.width / 100; // Distance in pixels the draggable should be moved: 1 "stop"
+
+				// make alt key go "10"
+				if ( event.altKey ) {
+					distance *= 10;
+				}
+
+				// Reposition if one of the directional keys is pressed
+				switch ( event.keyCode ) {
+					case 37: position.left -= distance; break; // Left
+					case 38: position.top  -= distance; break; // Up
+					case 39: position.left += distance; break; // Right
+					case 40: position.top  += distance; break; // Down
+					default: return true; // Exit and bubble
+				}
+
+				// Keep draggable within container
+				position.left = Math.max( 0, Math.min( position.left, container.width() ) );
+				position.top =  Math.max( 0, Math.min( position.top, container.height() ) );
+
+				draggable.css(position);
+				self._squareDrag( event, { position: position });
+				event.preventDefault();
+			});
+
+			// allow clicking on the square to move there and keep dragging
+			square.mousedown( function( event ) {
+				var squareOffset, pos;
+				// only left click
+				if ( event.which !== 1 ) {
+					return;
+				}
+
+				// prevent bubbling from the handle: no infinite loops
+				if ( ! $( event.target ).is( 'div' ) ) {
+					return;
+				}
+
+				squareOffset = self.controls.square.offset();
+				pos = {
+						top: event.pageY - squareOffset.top,
+						left: event.pageX - squareOffset.left
+				};
+				event.preventDefault();
+				self._squareDrag( event, { position: pos } );
+				event.target = self.controls.squareDrag.get(0);
+				self.controls.squareDrag.css( pos ).trigger( event );
+			});
+
+			// palettes
+			if ( self.options.palettes ) {
+				self._paletteListeners();
+			}
+		},
+
+		_paletteListeners: function() {
+			var self = this;
+			self.picker.find('.iris-palette-container').on('click.palette', '.iris-palette', function() {
+				self._color.fromCSS( $(this).data('color') );
+				self.active = 'external';
+				self._change();
+			}).on( 'keydown.palette', '.iris-palette', function( event ) {
+				if ( ! ( event.keyCode === 13 || event.keyCode === 32 ) ) {
+					return true;
+				}
+				event.stopPropagation();
+				$( this ).click();
+			});
+		},
+
+		_squareDrag: function( event, ui ) {
+			var self = this,
+				controlOpts = self.options.controls,
+				dimensions = self._squareDimensions(),
+				vertVal = Math.round( ( dimensions.h - ui.position.top ) / dimensions.h * self._scale[controlOpts.vert] ),
+				horizVal = self._scale[controlOpts.horiz] - Math.round( ( dimensions.w - ui.position.left ) / dimensions.w * self._scale[controlOpts.horiz] );
+
+			self._color[controlOpts.horiz]( horizVal )[controlOpts.vert]( vertVal );
+
+			self.active = 'square';
+			self._change.apply( self, arguments );
+		},
+
+		_setOption: function( key, value ) {
+			var self = this,
+				oldValue = self.options[key],
+				doDimensions = false,
+				hexLessColor,
+				newColor,
+				method;
+
+			// ensure the new value is set. We can reset to oldValue if some check wasn't met.
+			self.options[key] = value;
+
+			switch(key) {
+				case 'color':
+					// cast to string in case we have a number
+					value = '' + value;
+					hexLessColor = value.replace( /^#/, '' );
+					newColor = new Color( value ).setHSpace( self.options.mode );
+					if ( newColor.error ) {
+						self.options[key] = oldValue;
+					} else {
+						self._color = newColor;
+						self.options.color = self.options[key] = self._color.toString();
+						self.active = 'external';
+						self._change();
+					}
+					break;
+				case 'palettes':
+					doDimensions = true;
+
+					if ( value ) {
+						self._addPalettes();
+					} else {
+						self.picker.find('.iris-palette-container').remove();
+					}
+
+					// do we need to add events?
+					if ( ! oldValue ) {
+						self._paletteListeners();
+					}
+					break;
+				case 'width':
+					doDimensions = true;
+					break;
+				case 'border':
+					doDimensions = true;
+					method = value ? 'addClass' : 'removeClass';
+					self.picker[method]('iris-border');
+					break;
+				case 'mode':
+				case 'controls':
+					// if nothing's changed, let's bail, since this causes re-rendering the whole widget
+					if ( oldValue === value ) {
+						return;
+					}
+
+					// we're using these poorly named variables because they're already scoped.
+					// method is the element that Iris was called on. oldValue will be the options
+					method = self.element;
+					oldValue = self.options;
+					oldValue.hide = ! self.picker.is( ':visible' );
+					self.destroy();
+					self.picker.remove();
+					return $(self.element).iris(oldValue);
+			}
+
+			// Do we need to recalc dimensions?
+			if ( doDimensions ) {
+				self._dimensions(true);
+			}
+		},
+
+		_squareDimensions: function( forceRefresh ) {
+			var square = this.controls.square,
+				dimensions,
+				control;
+
+			if ( forceRefresh !== undef && square.data('dimensions') ) {
+				return square.data('dimensions');
+			}
+
+			control = this.controls.squareDrag;
+			dimensions = {
+				w: square.width(),
+				h: square.height()
+			};
+			square.data( 'dimensions', dimensions );
+			return dimensions;
+		},
+
+		_isNonHueControl: function( active, type ) {
+			if ( active === 'square' && this.options.controls.strip === 'h' ) {
+				return true;
+			} else if ( type === 'external' || ( type === 'h' && active === 'strip' ) ) {
+				return false;
+			}
+
+			return true;
+		},
+
+		_change: function() {
+			var self = this,
+				controls = self.controls,
+				color = self._getHSpaceColor(),
+				actions = [ 'square', 'strip' ],
+				controlOpts = self.options.controls,
+				type = controlOpts[self.active] || 'external',
+				oldHue = self.hue;
+
+			if ( self.active === 'strip' ) {
+				// take no action on any of the square sliders if we adjusted the strip
+				actions = [];
+			} else if ( self.active !== 'external' ) {
+				// for non-strip, non-external, strip should never change
+				actions.pop(); // conveniently the last item
+			}
+
+			$.each( actions, function(index, item) {
+				var value, dimensions, cssObj;
+				if ( item !== self.active ) {
+					switch ( item ) {
+						case 'strip':
+							// reverse for hue
+							value = ( controlOpts.strip === 'h' ) ? self._scale[controlOpts.strip] - color[controlOpts.strip] : color[controlOpts.strip];
+							controls.stripSlider.slider( 'value', value );
+							break;
+						case 'square':
+							dimensions = self._squareDimensions();
+							cssObj = {
+								left: color[controlOpts.horiz] / self._scale[controlOpts.horiz] * dimensions.w,
+								top: dimensions.h - ( color[controlOpts.vert] / self._scale[controlOpts.vert] * dimensions.h )
+							};
+
+							self.controls.squareDrag.css( cssObj );
+							break;
+					}
+				}
+			});
+
+			// Ensure that we don't change hue if we triggered a hue reset
+			if ( color.h !== oldHue && self._isNonHueControl( self.active, type ) ) {
+				self._color.h(oldHue);
+			}
+
+			// store hue for repeating above check next time
+			self.hue = self._color.h();
+
+			self.options.color = self._color.toString();
+
+			// only run after the first time
+			if ( self._inited ) {
+				self._trigger( 'change', { type: self.active }, { color: self._color } );
+			}
+
+			if ( self.element.is( ':input' ) && ! self._color.error ) {
+				self.element.removeClass( 'iris-error' );
+				if ( self.element.val() !== self._color.toString() ) {
+					self.element.val( self._color.toString() );
+				}
+			}
+
+			self._paint();
+			self._inited = true;
+			self.active = false;
+		},
+		// taken from underscore.js _.debounce method
+		_debounce: function( func, wait, immediate ) {
+			var timeout, result;
+			return function() {
+				var context = this,
+					args = arguments,
+					later,
+					callNow;
+
+				later = function() {
+					timeout = null;
+					if ( ! immediate) {
+						result = func.apply( context, args );
+					}
+				};
+
+				callNow = immediate && !timeout;
+				clearTimeout( timeout );
+				timeout = setTimeout( later, wait );
+				if ( callNow ) {
+					result = func.apply( context, args );
+				}
+				return result;
+			};
+		},
+		show: function() {
+			this.picker.show();
+		},
+		hide: function() {
+			this.picker.hide();
+		},
+		toggle: function() {
+			this.picker.toggle();
+		},
+		color: function(newColor) {
+			if ( newColor === true ) {
+				return this._color.clone();
+			} else if ( newColor === undef ) {
+				return this._color.toString();
+			}
+			this.option('color', newColor);
+		}
+	};
+	// initialize the widget
+	$.widget( 'a8c.iris', Iris );
+	// add CSS
+	$( '<style id="iris-css">' + _css + '</style>' ).appendTo( 'head' );
+
+}( jQuery ));
+/*! Color.js - v0.9.11 - 2013-08-09
+* https://github.com/Automattic/Color.js
+* Copyright (c) 2013 Matt Wiebe; Licensed GPLv2 */
+(function(global, undef) {
+
+	var Color = function( color, type ) {
+		if ( ! ( this instanceof Color ) )
+			return new Color( color, type );
+
+		return this._init( color, type );
+	};
+
+	Color.fn = Color.prototype = {
+		_color: 0,
+		_alpha: 1,
+		error: false,
+		// for preserving hue/sat in fromHsl().toHsl() flows
+		_hsl: { h: 0, s: 0, l: 0 },
+		// for preserving hue/sat in fromHsv().toHsv() flows
+		_hsv: { h: 0, s: 0, v: 0 },
+		// for setting hsl or hsv space - needed for .h() & .s() functions to function properly
+		_hSpace: 'hsl',
+		_init: function( color ) {
+			var func = 'noop';
+			switch ( typeof color ) {
+					case 'object':
+						// alpha?
+						if ( color.a !== undef )
+							this.a( color.a );
+						func = ( color.r !== undef ) ? 'fromRgb' :
+							( color.l !== undef ) ? 'fromHsl' :
+							( color.v !== undef ) ? 'fromHsv' : func;
+						return this[func]( color );
+					case 'string':
+						return this.fromCSS( color );
+					case 'number':
+						return this.fromInt( parseInt( color, 10 ) );
+			}
+			return this;
+		},
+
+		_error: function() {
+			this.error = true;
+			return this;
+		},
+
+		clone: function() {
+			var newColor = new Color( this.toInt() ),
+				copy = ['_alpha', '_hSpace', '_hsl', '_hsv', 'error'];
+			for ( var i = copy.length - 1; i >= 0; i-- ) {
+				newColor[ copy[i] ] = this[ copy[i] ];
+			}
+			return newColor;
+		},
+
+		setHSpace: function( space ) {
+			this._hSpace = ( space === 'hsv' ) ? space : 'hsl';
+			return this;
+		},
+
+		noop: function() {
+			return this;
+		},
+
+		fromCSS: function( color ) {
+			var list,
+				leadingRE = /^(rgb|hs(l|v))a?\(/;
+			this.error = false;
+
+			// whitespace and semicolon trim
+			color = color.replace(/^\s+/, '').replace(/\s+$/, '').replace(/;$/, '');
+
+			if ( color.match(leadingRE) && color.match(/\)$/) ) {
+				list = color.replace(/(\s|%)/g, '').replace(leadingRE, '').replace(/,?\);?$/, '').split(',');
+
+				if ( list.length < 3 )
+					return this._error();
+
+				if ( list.length === 4 ) {
+					this.a( parseFloat( list.pop() ) );
+					// error state has been set to true in .a() if we passed NaN
+					if ( this.error )
+						return this;
+				}
+
+				for (var i = list.length - 1; i >= 0; i--) {
+					list[i] = parseInt(list[i], 10);
+					if ( isNaN( list[i] ) )
+						return this._error();
+				}
+
+				if ( color.match(/^rgb/) ) {
+					return this.fromRgb( {
+						r: list[0],
+						g: list[1],
+						b: list[2]
+					} );
+				} else if ( color.match(/^hsv/) ) {
+					return this.fromHsv( {
+						h: list[0],
+						s: list[1],
+						v: list[2]
+					} );
+				} else {
+					return this.fromHsl( {
+						h: list[0],
+						s: list[1],
+						l: list[2]
+					} );
+				}
+			} else {
+				// must be hex amirite?
+				return this.fromHex( color );
+			}
+		},
+
+		fromRgb: function( rgb, preserve ) {
+			if ( typeof rgb !== 'object' || rgb.r === undef || rgb.g === undef || rgb.b === undef )
+				return this._error();
+
+			this.error = false;
+			return this.fromInt( parseInt( ( rgb.r << 16 ) + ( rgb.g << 8 ) + rgb.b, 10 ), preserve );
+		},
+
+		fromHex: function( color ) {
+			color = color.replace(/^#/, '').replace(/^0x/, '');
+			if ( color.length === 3 ) {
+				color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
+			}
+
+			// rough error checking - this is where things go squirrely the most
+			this.error = ! /^[0-9A-F]{6}$/i.test( color );
+			return this.fromInt( parseInt( color, 16 ) );
+		},
+
+		fromHsl: function( hsl ) {
+			var r, g, b, q, p, h, s, l;
+
+			if ( typeof hsl !== 'object' || hsl.h === undef || hsl.s === undef || hsl.l === undef )
+				return this._error();
+
+			this._hsl = hsl; // store it
+			this._hSpace = 'hsl'; // implicit
+			h = hsl.h / 360; s = hsl.s / 100; l = hsl.l / 100;
+			if ( s === 0 ) {
+				r = g = b = l; // achromatic
+			}
+			else {
+				q = l < 0.5 ? l * ( 1 + s ) : l + s - l * s;
+				p = 2 * l - q;
+				r = this.hue2rgb( p, q, h + 1/3 );
+				g = this.hue2rgb( p, q, h );
+				b = this.hue2rgb( p, q, h - 1/3 );
+			}
+			return this.fromRgb( {
+				r: r * 255,
+				g: g * 255,
+				b: b * 255
+			}, true ); // true preserves hue/sat
+		},
+
+		fromHsv: function( hsv ) {
+			var h, s, v, r, g, b, i, f, p, q, t;
+			if ( typeof hsv !== 'object' || hsv.h === undef || hsv.s === undef || hsv.v === undef )
+				return this._error();
+
+			this._hsv = hsv; // store it
+			this._hSpace = 'hsv'; // implicit
+
+			h = hsv.h / 360; s = hsv.s / 100; v = hsv.v / 100;
+			i = Math.floor( h * 6 );
+			f = h * 6 - i;
+			p = v * ( 1 - s );
+			q = v * ( 1 - f * s );
+			t = v * ( 1 - ( 1 - f ) * s );
+
+			switch( i % 6 ) {
+				case 0:
+					r = v; g = t; b = p;
+					break;
+				case 1:
+					r = q; g = v; b = p;
+					break;
+				case 2:
+					r = p; g = v; b = t;
+					break;
+				case 3:
+					r = p; g = q; b = v;
+					break;
+				case 4:
+					r = t; g = p; b = v;
+					break;
+				case 5:
+					r = v; g = p; b = q;
+					break;
+			}
+
+			return this.fromRgb( {
+				r: r * 255,
+				g: g * 255,
+				b: b * 255
+			}, true ); // true preserves hue/sat
+
+		},
+		// everything comes down to fromInt
+		fromInt: function( color, preserve ) {
+			this._color = parseInt( color, 10 );
+
+			if ( isNaN( this._color ) )
+				this._color = 0;
+
+			// let's coerce things
+			if ( this._color > 16777215 )
+				this._color = 16777215;
+			else if ( this._color < 0 )
+				this._color = 0;
+
+			// let's not do weird things
+			if ( preserve === undef ) {
+				this._hsv.h = this._hsv.s = this._hsl.h = this._hsl.s = 0;
+			}
+			// EVENT GOES HERE
+			return this;
+		},
+
+		hue2rgb: function( p, q, t ) {
+			if ( t < 0 ) {
+				t += 1;
+			}
+			if ( t > 1 ) {
+				t -= 1;
+			}
+			if ( t < 1/6 ) {
+				return p + ( q - p ) * 6 * t;
+			}
+			if ( t < 1/2 ) {
+				return q;
+			}
+			if ( t < 2/3 ) {
+				return p + ( q - p ) * ( 2/3 - t ) * 6;
+			}
+			return p;
+		},
+
+		toString: function() {
+			var hex = parseInt( this._color, 10 ).toString( 16 );
+			if ( this.error )
+				return '';
+			// maybe left pad it
+			if ( hex.length < 6 ) {
+				for (var i = 6 - hex.length - 1; i >= 0; i--) {
+					hex = '0' + hex;
+				}
+			}
+			return '#' + hex;
+		},
+
+		toCSS: function( type, alpha ) {
+			type = type || 'hex';
+			alpha = parseFloat( alpha || this._alpha );
+			switch ( type ) {
+				case 'rgb':
+				case 'rgba':
+					var rgb = this.toRgb();
+					if ( alpha < 1 ) {
+						return "rgba( " + rgb.r + ", " + rgb.g + ", " + rgb.b + ", " + alpha + " )";
+					}
+					else {
+						return "rgb( " + rgb.r + ", " + rgb.g + ", " + rgb.b + " )";
+					}
+					break;
+				case 'hsl':
+				case 'hsla':
+					var hsl = this.toHsl();
+					if ( alpha < 1 ) {
+						return "hsla( " + hsl.h + ", " + hsl.s + "%, " + hsl.l + "%, " + alpha + " )";
+					}
+					else {
+						return "hsl( " + hsl.h + ", " + hsl.s + "%, " + hsl.l + "% )";
+					}
+					break;
+				default:
+					return this.toString();
+			}
+		},
+
+		toRgb: function() {
+			return {
+				r: 255 & ( this._color >> 16 ),
+				g: 255 & ( this._color >> 8 ),
+				b: 255 & ( this._color )
+			};
+		},
+
+		toHsl: function() {
+			var rgb = this.toRgb();
+			var r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255;
+			var max = Math.max( r, g, b ), min = Math.min( r, g, b );
+			var h, s, l = ( max + min ) / 2;
+
+			if ( max === min ) {
+				h = s = 0; // achromatic
+			} else {
+				var d = max - min;
+				s = l > 0.5 ? d / ( 2 - max - min ) : d / ( max + min );
+				switch ( max ) {
+					case r: h = ( g - b ) / d + ( g < b ? 6 : 0 );
+						break;
+					case g: h = ( b - r ) / d + 2;
+						break;
+					case b: h = ( r - g ) / d + 4;
+						break;
+				}
+				h /= 6;
+			}
+
+			// maintain hue & sat if we've been manipulating things in the HSL space.
+			h = Math.round( h * 360 );
+			if ( h === 0 && this._hsl.h !== h ) {
+				h = this._hsl.h;
+			}
+			s = Math.round( s * 100 );
+			if ( s === 0 && this._hsl.s ) {
+				s = this._hsl.s;
+			}
+
+			return {
+				h: h,
+				s: s,
+				l: Math.round( l * 100 )
+			};
+
+		},
+
+		toHsv: function() {
+			var rgb = this.toRgb();
+			var r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255;
+			var max = Math.max( r, g, b ), min = Math.min( r, g, b );
+			var h, s, v = max;
+			var d = max - min;
+			s = max === 0 ? 0 : d / max;
+
+			if ( max === min ) {
+				h = s = 0; // achromatic
+			} else {
+				switch( max ){
+					case r:
+						h = ( g - b ) / d + ( g < b ? 6 : 0 );
+						break;
+					case g:
+						h = ( b - r ) / d + 2;
+						break;
+					case b:
+						h = ( r - g ) / d + 4;
+						break;
+				}
+				h /= 6;
+			}
+
+			// maintain hue & sat if we've been manipulating things in the HSV space.
+			h = Math.round( h * 360 );
+			if ( h === 0 && this._hsv.h !== h ) {
+				h = this._hsv.h;
+			}
+			s = Math.round( s * 100 );
+			if ( s === 0 && this._hsv.s ) {
+				s = this._hsv.s;
+			}
+
+			return {
+				h: h,
+				s: s,
+				v: Math.round( v * 100 )
+			};
+		},
+
+		toInt: function() {
+			return this._color;
+		},
+
+		toIEOctoHex: function() {
+			// AARRBBGG
+			var hex = this.toString();
+			var AA = parseInt( 255 * this._alpha, 10 ).toString(16);
+			if ( AA.length === 1 ) {
+				AA = '0' + AA;
+			}
+			return '#' + AA + hex.replace(/^#/, '' );
+		},
+
+		toLuminosity: function() {
+			var rgb = this.toRgb();
+			return 0.2126 * Math.pow( rgb.r / 255, 2.2 ) + 0.7152 * Math.pow( rgb.g / 255, 2.2 ) + 0.0722 * Math.pow( rgb.b / 255, 2.2);
+		},
+
+		getDistanceLuminosityFrom: function( color ) {
+			if ( ! ( color instanceof Color ) ) {
+				throw 'getDistanceLuminosityFrom requires a Color object';
+			}
+			var lum1 = this.toLuminosity();
+			var lum2 = color.toLuminosity();
+			if ( lum1 > lum2 ) {
+				return ( lum1 + 0.05 ) / ( lum2 + 0.05 );
+			}
+			else {
+				return ( lum2 + 0.05 ) / ( lum1 + 0.05 );
+			}
+		},
+
+		getMaxContrastColor: function() {
+			var lum = this.toLuminosity();
+			var hex = ( lum >= 0.5 ) ? '000000' : 'ffffff';
+			return new Color( hex );
+		},
+
+		getReadableContrastingColor: function( bgColor, minContrast ) {
+			if ( ! bgColor instanceof Color ) {
+				return this;
+			}
+
+			// you shouldn't use less than 5, but you might want to.
+			var targetContrast = ( minContrast === undef ) ? 5 : minContrast;
+			// working things
+			var contrast = bgColor.getDistanceLuminosityFrom( this );
+			var maxContrastColor = bgColor.getMaxContrastColor();
+			var maxContrast = maxContrastColor.getDistanceLuminosityFrom( bgColor );
+
+			// if current max contrast is less than the target contrast, we had wishful thinking.
+			// still, go max
+			if ( maxContrast <= targetContrast ) {
+				return maxContrastColor;
+			}
+			// or, we might already have sufficient contrast
+			else if ( contrast >= targetContrast ) {
+				return this;
+			}
+
+			var incr = ( 0 === maxContrastColor.toInt() ) ? -1 : 1;
+			while ( contrast < targetContrast ) {
+				this.l( incr, true ); // 2nd arg turns this into an incrementer
+				contrast = this.getDistanceLuminosityFrom( bgColor );
+				// infininite loop prevention: you never know.
+				if ( this._color === 0 || this._color === 16777215 ) {
+					break;
+				}
+			}
+
+			return this;
+
+		},
+
+		a: function( val ) {
+			if ( val === undef )
+				return this._alpha;
+
+			var a = parseFloat( val );
+
+			if ( isNaN( a ) )
+				return this._error();
+
+			this._alpha = a;
+			return this;
+		},
+
+		// TRANSFORMS
+
+		darken: function( amount ) {
+			amount = amount || 5;
+			return this.l( - amount, true );
+		},
+
+		lighten: function( amount ) {
+			amount = amount || 5;
+			return this.l( amount, true );
+		},
+
+		saturate: function( amount ) {
+			amount = amount || 15;
+			return this.s( amount, true );
+		},
+
+		desaturate: function( amount ) {
+			amount = amount || 15;
+			return this.s( - amount, true );
+		},
+
+		toGrayscale: function() {
+			return this.setHSpace('hsl').s( 0 );
+		},
+
+		getComplement: function() {
+			return this.h( 180, true );
+		},
+
+		getSplitComplement: function( step ) {
+			step = step || 1;
+			var incr = 180 + ( step * 30 );
+			return this.h( incr, true );
+		},
+
+		getAnalog: function( step ) {
+			step = step || 1;
+			var incr = step * 30;
+			return this.h( incr, true );
+		},
+
+		getTetrad: function( step ) {
+			step = step || 1;
+			var incr = step * 60;
+			return this.h( incr, true );
+		},
+
+		getTriad: function( step ) {
+			step = step || 1;
+			var incr = step * 120;
+			return this.h( incr, true );
+		},
+
+		_partial: function( key ) {
+			var prop = shortProps[key];
+			return function( val, incr ) {
+				var color = this._spaceFunc('to', prop.space);
+
+				// GETTER
+				if ( val === undef )
+					return color[key];
+
+				// INCREMENT
+				if ( incr === true )
+					val = color[key] + val;
+
+				// MOD & RANGE
+				if ( prop.mod )
+					val = val % prop.mod;
+				if ( prop.range )
+					val = ( val < prop.range[0] ) ? prop.range[0] : ( val > prop.range[1] ) ? prop.range[1] : val;
+
+				// NEW VALUE
+				color[key] = val;
+
+				return this._spaceFunc('from', prop.space, color);
+			};
+		},
+
+		_spaceFunc: function( dir, s, val ) {
+			var space = s || this._hSpace,
+				funcName = dir + space.charAt(0).toUpperCase() + space.substr(1);
+			return this[funcName](val);
+		}
+	};
+
+	var shortProps = {
+		h: {
+			mod: 360
+		},
+		s: {
+			range: [0,100]
+		},
+		l: {
+			space: 'hsl',
+			range: [0,100]
+		},
+		v: {
+			space: 'hsv',
+			range: [0,100]
+		},
+		r: {
+			space: 'rgb',
+			range: [0,255]
+		},
+		g: {
+			space: 'rgb',
+			range: [0,255]
+		},
+		b: {
+			space: 'rgb',
+			range: [0,255]
+		}
+	};
+
+	for ( var key in shortProps ) {
+		if ( shortProps.hasOwnProperty( key ) )
+			Color.fn[key] = Color.fn._partial(key);
+	}
+
+	// play nicely with Node + browser
+	if ( typeof exports === 'object' )
+		module.exports = Color;
+	else
+		global.Color = Color;
+
+}(this));
diff --git a/lib/plugins/styling/lang/en/intro.txt b/lib/plugins/styling/lang/en/intro.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4ea55172fdaa3ee383b2fbf78e294210d35a4f45
--- /dev/null
+++ b/lib/plugins/styling/lang/en/intro.txt
@@ -0,0 +1,2 @@
+This tool allows you to change certain style settings of your currently selected template.
+All changes are stored in a local configuration file and are upgrade safe.
\ No newline at end of file
diff --git a/lib/plugins/styling/lang/en/lang.php b/lib/plugins/styling/lang/en/lang.php
new file mode 100644
index 0000000000000000000000000000000000000000..e0011eb83577e91f96d6198572c76bfb1730743d
--- /dev/null
+++ b/lib/plugins/styling/lang/en/lang.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * English language file for styling plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// menu entry for admin plugins
+$lang['menu'] = 'Template Style Settings';
+
+$lang['js']['loader'] = 'Preview is loading...<br />if this does not goes away, your values may be faulty';
+$lang['js']['popup'] = 'Open as a popup';
+
+// custom language strings for the plugin
+$lang['error'] = 'Sorry, this template does not support this functionality.';
+
+$lang['btn_preview'] = 'Preview changes';
+$lang['btn_save']    = 'Save changes';
+$lang['btn_reset']   = 'Reset current changes';
+$lang['btn_revert']  = 'Revert styles back to template\'s default';
+
+// default guaranteed placeholders
+$lang['__text__']           = 'Main text color';
+$lang['__background__']     = 'Main background color';
+$lang['__text_alt__']       = 'Alternative text color';
+$lang['__background_alt__'] = 'Alternative background color';
+$lang['__text_neu__']       = 'Neutral text color';
+$lang['__background_neu__'] = 'Neutral background color';
+$lang['__border__']         = 'Border color';
+$lang['__highlight__']      = 'Highlight color (for search results mainly)';
+
+
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/lib/plugins/styling/plugin.info.txt b/lib/plugins/styling/plugin.info.txt
new file mode 100644
index 0000000000000000000000000000000000000000..cdf01ee6f5e9bcd1b2d02bdbd4ce952c0fd4ad8f
--- /dev/null
+++ b/lib/plugins/styling/plugin.info.txt
@@ -0,0 +1,7 @@
+base   styling
+author Andreas Gohr
+email  andi@splitbrain.org
+date   2015-05-16
+name   styling plugin
+desc   Allows to edit style.ini replacements
+url    https://www.dokuwiki.org/plugin:styling
diff --git a/lib/plugins/styling/popup.php b/lib/plugins/styling/popup.php
new file mode 100644
index 0000000000000000000000000000000000000000..964b19e296a1d2217591e5dd3f76d3d0003168f8
--- /dev/null
+++ b/lib/plugins/styling/popup.php
@@ -0,0 +1,30 @@
+<?php
+if(!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__) . '/../../../');
+require_once(DOKU_INC . 'inc/init.php');
+//close session
+session_write_close();
+header('Content-Type: text/html; charset=utf-8');
+header('X-UA-Compatible: IE=edge,chrome=1');
+
+/** @var admin_plugin_styling $plugin */
+$plugin = plugin_load('admin', 'styling');
+if(!auth_isadmin()) die('only admins allowed');
+$plugin->ispopup = true;
+
+// handle posts
+$plugin->handle();
+
+// output plugin in a very minimal template:
+?><!DOCTYPE html>
+<html lang="<?php echo $conf['lang'] ?>" dir="<?php echo $lang['direction'] ?>">
+<head>
+    <meta charset="utf-8" />
+    <title><?php echo $plugin->getLang('menu') ?></title>
+    <?php tpl_metaheaders(false) ?>
+    <meta name="viewport" content="width=device-width,initial-scale=1" />
+    <?php echo tpl_favicon(array('favicon')) ?>
+</head>
+<body class="dokuwiki">
+    <?php $plugin->html() ?>
+</body>
+</html>
diff --git a/lib/plugins/styling/script.js b/lib/plugins/styling/script.js
new file mode 100644
index 0000000000000000000000000000000000000000..074c8dc40296e87004a597fa58ecdc2a39370bd8
--- /dev/null
+++ b/lib/plugins/styling/script.js
@@ -0,0 +1,97 @@
+/* DOKUWIKI:include_once iris.js */
+
+jQuery(function () {
+
+    /**
+     * Function to reload the preview styles in the main window
+     *
+     * @param {Window} target the main window
+     */
+    function applyPreview(target) {
+        // remove style
+        var $style = target.jQuery('link[rel=stylesheet][href*="lib/exe/css.php"]');
+        $style.attr('href', '');
+
+        // append the loader screen
+        var $loader = target.jQuery('#plugin__styling_loader');
+        if (!$loader.length) {
+            $loader = target.jQuery('<div id="plugin__styling_loader">' + LANG.plugins.styling.loader + '</div>');
+            $loader.css({
+                'position':         'absolute',
+                'width':            '100%',
+                'height':           '100%',
+                'top':              0,
+                'left':             0,
+                'z-index':          5000,
+                'background-color': '#fff',
+                'opacity':          '0.7',
+                'color':            '#000',
+                'font-size':        '2.5em',
+                'text-align':       'center',
+                'line-height':      1.5,
+                'padding-top':      '2em'
+            });
+            target.jQuery('body').append($loader);
+        }
+
+        // load preview in main window (timeout works around chrome updating CSS weirdness)
+        setTimeout(function () {
+            var now = new Date().getTime();
+            $style.attr('href', DOKU_BASE + 'lib/exe/css.php?preview=1&tseed=' + now);
+        }, 500);
+    }
+
+    var doreload = 1;
+    var $styling_plugin = jQuery('#plugin__styling');
+
+    // if we are not on the plugin page (either main or popup)
+    if (!$styling_plugin.length) {
+        // handle the preview cookie
+        if(DokuCookie.getValue('styling_plugin') == 1) {
+            applyPreview(window);
+        }
+        return; // nothing more to do here
+    }
+
+    /* ---- from here on we're in the popup or admin page ---- */
+
+    // add the color picker
+    $styling_plugin.find('.color').iris({});
+
+    // add button on main page
+    if (!$styling_plugin.hasClass('ispopup')) {
+        var $form = $styling_plugin.find('form.styling').first();
+        var $btn = jQuery('<button>' + LANG.plugins.styling.popup + '</button>');
+        $form.prepend($btn);
+
+        $btn.click(function (e) {
+            var windowFeatures = "menubar=no,location=no,resizable=yes,scrollbars=yes,status=false,width=500,height=500";
+            window.open(DOKU_BASE + 'lib/plugins/styling/popup.php', 'styling_popup', windowFeatures);
+            e.preventDefault();
+            e.stopPropagation();
+        }).wrap('<p></p>');
+        return; // we exit here if this is not the popup
+    }
+
+    /* ---- from here on we're in the popup only ---- */
+
+    // reload the main page on close
+    window.onunload = function(e) {
+        if(doreload) {
+            window.opener.DokuCookie.setValue('styling_plugin', 0);
+            window.opener.document.location.reload();
+        }
+        return null;
+    };
+
+    // don't reload on our own buttons
+    jQuery(':button').click(function(e){
+        doreload = false;
+    });
+
+    // on first load apply preview
+    applyPreview(window.opener);
+
+    // enable the preview cookie
+    window.opener.DokuCookie.setValue('styling_plugin', 1);
+});
diff --git a/lib/plugins/styling/style.less b/lib/plugins/styling/style.less
new file mode 100644
index 0000000000000000000000000000000000000000..be0e16a5b97baf2706b9bf78e63de895bb2257e1
--- /dev/null
+++ b/lib/plugins/styling/style.less
@@ -0,0 +1,13 @@
+#plugin__styling {
+    button.primary {
+        font-weight: bold;
+    }
+
+    [dir=rtl] & table input {
+        text-align: right;
+    }
+}
+
+#plugin__styling_loader {
+    display: none;
+}
diff --git a/lib/tpl/dokuwiki/css/_admin.css b/lib/tpl/dokuwiki/css/_admin.css
index a9518d0edad78816caabd780e9bff409796ef1ec..bdde006e0cbcea996cd9b2865d89a324c36c003d 100644
--- a/lib/tpl/dokuwiki/css/_admin.css
+++ b/lib/tpl/dokuwiki/css/_admin.css
@@ -39,6 +39,9 @@
 .dokuwiki ul.admin_tasks li.admin_config {
     background-image: url(../../images/admin/config.png);
 }
+.dokuwiki ul.admin_tasks li.admin_styling {
+    background-image: url(../../images/admin/styling.png);
+}
 .dokuwiki ul.admin_tasks li.admin_revert {
     background-image: url(../../images/admin/revert.png);
 }
diff --git a/lib/tpl/dokuwiki/lang/en/lang.php b/lib/tpl/dokuwiki/lang/en/lang.php
new file mode 100644
index 0000000000000000000000000000000000000000..b7b3e7fa114604a0bb9c32749884e4a25ba0d0ed
--- /dev/null
+++ b/lib/tpl/dokuwiki/lang/en/lang.php
@@ -0,0 +1,12 @@
+<?php
+
+// style.ini values
+
+$lang['__background_site__'] = 'Color for the very background (behind the content box)';
+$lang['__link__']     = 'The general link color';
+$lang['__existing__'] = 'The color for links to existing pages';
+$lang['__missing__']  = 'The color for links to non-existing pages';
+$lang['__site_width__']    = 'The width of the full site (can be any length unit: %, px, em, ...)';
+$lang['__sidebar_width__'] = 'The width of the sidebar, if any (can be any length unit: %, px, em, ...)';
+$lang['__tablet_width__']  = 'Below screensizes of this width, the site switches to tablet mode';
+$lang['__phone_width__']   = 'Below screensizes of this width, the site switches to phone mode';
diff --git a/lib/tpl/dokuwiki/lang/en/style.txt b/lib/tpl/dokuwiki/lang/en/style.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7bf3e1a829247329bdb816a2aef093474ac19aac
--- /dev/null
+++ b/lib/tpl/dokuwiki/lang/en/style.txt
@@ -0,0 +1,4 @@
+If you want to adjust the logo, simply use the Media Manager to upload a ''logo.png'' into the ''wiki'' or the root namespace and it
+will be automatically used. You can also upload a ''favicon.ico'' there. If you use a closed
+wiki it is recommended to make the ''wiki'' (or root) namespace world readable in the ACL settings or
+your logo is not shown to not logged in users.